diff --git a/build/litegraph.js b/build/litegraph.js index d1b8b195c..6c8c2fcd7 100644 --- a/build/litegraph.js +++ b/build/litegraph.js @@ -1,7467 +1,7619 @@ //packer version -(function(global){ -// ************************************************************* -// LiteGraph CLASS ******* -// ************************************************************* - -/* FYI: links are stored in graph.links with this structure per object -{ - id: number - type: string, - origin_id: number, - origin_slot: number, - target_id: number, - target_slot: number, - data: * -}; -*/ - -/** -* The Global Scope. It contains all the registered node classes. -* -* @class LiteGraph -* @constructor -*/ - -var LiteGraph = global.LiteGraph = { - - NODE_TITLE_HEIGHT: 20, - NODE_SLOT_HEIGHT: 15, - NODE_WIDGET_HEIGHT: 20, - NODE_WIDTH: 140, - NODE_MIN_WIDTH: 50, - NODE_COLLAPSED_RADIUS: 10, - NODE_COLLAPSED_WIDTH: 80, - CANVAS_GRID_SIZE: 10, - NODE_TITLE_COLOR: "#999", - NODE_TEXT_SIZE: 14, - NODE_TEXT_COLOR: "#AAA", - NODE_SUBTEXT_SIZE: 12, - NODE_DEFAULT_COLOR: "#333", - NODE_DEFAULT_BGCOLOR: "#444", - NODE_DEFAULT_BOXCOLOR: "#888", - NODE_DEFAULT_SHAPE: "box", - MAX_NUMBER_OF_NODES: 1000, //avoid infinite loops - DEFAULT_POSITION: [100,100],//default node position - node_images_path: "", - - VALID_SHAPES: ["default","box","round","card"], //,"circle" - - BOX_SHAPE: 1, - ROUND_SHAPE: 2, - CIRCLE_SHAPE: 3, - CARD_SHAPE: 4, - - //enums - INPUT: 1, - OUTPUT: 2, - - EVENT: -1, //for outputs - ACTION: -1, //for inputs - - ALWAYS: 0, - ON_EVENT: 1, - NEVER: 2, - ON_TRIGGER: 3, - - NORMAL_TITLE: 0, - NO_TITLE: 1, - TRANSPARENT_TITLE: 2, - AUTOHIDE_TITLE: 3, - - proxy: null, //used to redirect calls - - debug: false, - throw_errors: true, - allow_scripts: true, - registered_node_types: {}, //nodetypes by string - node_types_by_file_extension: {}, //used for droping files in the canvas - Nodes: {}, //node types by classname - - /** - * Register a node class so it can be listed when the user wants to create a new one - * @method registerNodeType - * @param {String} type name of the node and path - * @param {Class} base_class class containing the structure of a node - */ - - registerNodeType: function(type, base_class) - { - if(!base_class.prototype) - throw("Cannot register a simple object, it must be a class with a prototype"); - base_class.type = type; - - if(LiteGraph.debug) - console.log("Node registered: " + type); - - var categories = type.split("/"); - var classname = base_class.name; - - var pos = type.lastIndexOf("/"); - base_class.category = type.substr(0,pos); - - if(!base_class.title) - base_class.title = classname; - //info.name = name.substr(pos+1,name.length - pos); - - //extend class - if(base_class.prototype) //is a class - for(var i in LGraphNode.prototype) - if(!base_class.prototype[i]) - base_class.prototype[i] = LGraphNode.prototype[i]; - - Object.defineProperty( base_class.prototype, "shape",{ - set: function(v) { - switch(v) - { - case "default": delete this._shape; break; - case "box": this._shape = LiteGraph.BOX_SHAPE; break; - case "round": this._shape = LiteGraph.ROUND_SHAPE; break; - case "circle": this._shape = LiteGraph.CIRCLE_SHAPE; break; - case "card": this._shape = LiteGraph.CARD_SHAPE; break; - default: - this._shape = v; - } - }, - get: function(v) - { - return this._shape; - }, - enumerable: true - }); - - this.registered_node_types[ type ] = base_class; - if(base_class.constructor.name) - this.Nodes[ classname ] = base_class; - - //warnings - if(base_class.prototype.onPropertyChange) - console.warn("LiteGraph node class " + type + " has onPropertyChange method, it must be called onPropertyChanged with d at the end"); - - if( base_class.supported_extensions ) - { - for(var i in base_class.supported_extensions ) - this.node_types_by_file_extension[ base_class.supported_extensions[i].toLowerCase() ] = base_class; - } - }, - - /** - * Create a new node type by passing a function, it wraps it with a propper class and generates inputs according to the parameters of the function. - * Useful to wrap simple methods that do not require properties, and that only process some input to generate an output. - * @method wrapFunctionAsNode - * @param {String} name node name with namespace (p.e.: 'math/sum') - * @param {Function} func - * @param {Array} param_types [optional] an array containing the type of every parameter, otherwise parameters will accept any type - * @param {String} return_type [optional] string with the return type, otherwise it will be generic - */ - wrapFunctionAsNode: function( name, func, param_types, return_type ) - { - var params = Array(func.length); - var code = ""; - var names = LiteGraph.getParameterNames( func ); - for(var i = 0; i < names.length; ++i) - code += "this.addInput('"+names[i]+"',"+(param_types && param_types[i] ? "'" + param_types[i] + "'" : "0") + ");\n"; - code += "this.addOutput('out',"+( return_type ? "'" + return_type + "'" : 0 )+");\n"; - var classobj = Function(code); - classobj.title = name.split("/").pop(); - classobj.desc = "Generated from " + func.name; - classobj.prototype.onExecute = function onExecute() - { - for(var i = 0; i < params.length; ++i) - params[i] = this.getInputData(i); - var r = func.apply( this, params ); - this.setOutputData(0,r); - } - this.registerNodeType( name, classobj ); - }, - - /** - * Adds this method to all nodetypes, existing and to be created - * (You can add it to LGraphNode.prototype but then existing node types wont have it) - * @method addNodeMethod - * @param {Function} func - */ - addNodeMethod: function( name, func ) - { - LGraphNode.prototype[name] = func; - for(var i in this.registered_node_types) - { - var type = this.registered_node_types[i]; - if(type.prototype[name]) - type.prototype["_" + name] = type.prototype[name]; //keep old in case of replacing - type.prototype[name] = func; - } - }, - - /** - * Create a node of a given type with a name. The node is not attached to any graph yet. - * @method createNode - * @param {String} type full name of the node class. p.e. "math/sin" - * @param {String} name a name to distinguish from other nodes - * @param {Object} options to set options - */ - - createNode: function( type, title, options ) - { - var base_class = this.registered_node_types[type]; - if (!base_class) - { - if(LiteGraph.debug) - console.log("GraphNode type \"" + type + "\" not registered."); - return null; - } - - var prototype = base_class.prototype || base_class; - - title = title || base_class.title || type; - - var node = new base_class( title ); - node.type = type; - - if(!node.title && title) node.title = title; - if(!node.properties) node.properties = {}; - if(!node.properties_info) node.properties_info = []; - if(!node.flags) node.flags = {}; - if(!node.size) node.size = node.computeSize(); - if(!node.pos) node.pos = LiteGraph.DEFAULT_POSITION.concat(); - if(!node.mode) node.mode = LiteGraph.ALWAYS; - - //extra options - if(options) - { - for(var i in options) - node[i] = options[i]; - } - - return node; - }, - - /** - * Returns a registered node type with a given name - * @method getNodeType - * @param {String} type full name of the node class. p.e. "math/sin" - * @return {Class} the node class - */ - - getNodeType: function(type) - { - return this.registered_node_types[type]; - }, - - - /** - * Returns a list of node types matching one category - * @method getNodeType - * @param {String} category category name - * @return {Array} array with all the node classes - */ - - getNodeTypesInCategory: function( category, filter ) - { - var r = []; - for(var i in this.registered_node_types) - { - var type = this.registered_node_types[i]; - if(filter && type.filter && type.filter != filter) - continue; - - if(category == "" ) - { - if (type.category == null) - r.push(type); - } - else if (type.category == category) - r.push(type); - } - - return r; - }, - - /** - * Returns a list with all the node type categories - * @method getNodeTypesCategories - * @return {Array} array with all the names of the categories - */ - - getNodeTypesCategories: function() - { - var categories = {"":1}; - for(var i in this.registered_node_types) - if(this.registered_node_types[i].category && !this.registered_node_types[i].skip_list) - categories[ this.registered_node_types[i].category ] = 1; - var result = []; - for(var i in categories) - result.push(i); - return result; - }, - - //debug purposes: reloads all the js scripts that matches a wilcard - reloadNodes: function (folder_wildcard) - { - var tmp = document.getElementsByTagName("script"); - //weird, this array changes by its own, so we use a copy - var script_files = []; - for(var i in tmp) - script_files.push(tmp[i]); - - - var docHeadObj = document.getElementsByTagName("head")[0]; - folder_wildcard = document.location.href + folder_wildcard; - - for(var i in script_files) - { - var src = script_files[i].src; - if( !src || src.substr(0,folder_wildcard.length ) != folder_wildcard) - continue; - - try - { - if(LiteGraph.debug) - console.log("Reloading: " + src); - var dynamicScript = document.createElement("script"); - dynamicScript.type = "text/javascript"; - dynamicScript.src = src; - docHeadObj.appendChild(dynamicScript); - docHeadObj.removeChild(script_files[i]); - } - catch (err) - { - if(LiteGraph.throw_errors) - throw err; - if(LiteGraph.debug) - console.log("Error while reloading " + src); - } - } - - if(LiteGraph.debug) - console.log("Nodes reloaded"); - }, - - //separated just to improve if it doesnt work - cloneObject: function(obj, target) - { - if(obj == null) return null; - var r = JSON.parse( JSON.stringify( obj ) ); - if(!target) return r; - - for(var i in r) - target[i] = r[i]; - return target; - }, - - isValidConnection: function( type_a, type_b ) - { - if( !type_a || //generic output - !type_b || //generic input - type_a == type_b || //same type (is valid for triggers) - type_a == LiteGraph.EVENT && type_b == LiteGraph.ACTION ) - return true; - - // Enforce string type to handle toLowerCase call (-1 number not ok) - type_a = String(type_a); - type_b = String(type_b); - type_a = type_a.toLowerCase(); - type_b = type_b.toLowerCase(); - - // For nodes supporting multiple connection types - if( type_a.indexOf(",") == -1 && type_b.indexOf(",") == -1 ) - return type_a == type_b; - - // Check all permutations to see if one is valid - var supported_types_a = type_a.split(","); - var supported_types_b = type_b.split(","); - for(var i = 0; i < supported_types_a.length; ++i) - for(var j = 0; j < supported_types_b.length; ++j) - if( supported_types_a[i] == supported_types_b[j] ) - return true; - - return false; - } -}; - -//timer that works everywhere -if(typeof(performance) != "undefined") - LiteGraph.getTime = performance.now.bind(performance); -else if(typeof(Date) != "undefined" && Date.now) - LiteGraph.getTime = Date.now.bind(Date); -else if(typeof(process) != "undefined") - LiteGraph.getTime = function(){ - var t = process.hrtime(); - return t[0]*0.001 + t[1]*(1e-6); - } -else - LiteGraph.getTime = function getTime() { return (new Date).getTime(); } - - - - - - -//********************************************************************************* -// LGraph CLASS -//********************************************************************************* - -/** -* LGraph is the class that contain a full graph. We instantiate one and add nodes to it, and then we can run the execution loop. -* -* @class LGraph -* @constructor -* @param {Object} o data from previous serialization [optional] -*/ - -function LGraph( o ) -{ - if (LiteGraph.debug) - console.log("Graph created"); - this.list_of_graphcanvas = null; - this.clear(); - - if(o) - this.configure(o); -} - -global.LGraph = LiteGraph.LGraph = LGraph; - -//default supported types -LGraph.supported_types = ["number","string","boolean"]; - -//used to know which types of connections support this graph (some graphs do not allow certain types) -LGraph.prototype.getSupportedTypes = function() { return this.supported_types || LGraph.supported_types; } - -LGraph.STATUS_STOPPED = 1; -LGraph.STATUS_RUNNING = 2; - -/** -* Removes all nodes from this graph -* @method clear -*/ - -LGraph.prototype.clear = function() -{ - this.stop(); - this.status = LGraph.STATUS_STOPPED; - - this.last_node_id = 0; - this.last_link_id = 0; - - this._version = -1; //used to detect changes - - //nodes - this._nodes = []; - this._nodes_by_id = {}; - this._nodes_in_order = null; //nodes that are executable sorted in execution order - this._nodes_executable = null; //nodes that contain onExecute - - //other scene stuff - this._groups = []; - - //links - this.links = {}; //container with all the links - - //iterations - this.iteration = 0; - - //custom data - this.config = {}; - - //timing - this.globaltime = 0; - this.runningtime = 0; - this.fixedtime = 0; - this.fixedtime_lapse = 0.01; - this.elapsed_time = 0.01; - this.last_update_time = 0; - this.starttime = 0; - - this.catch_errors = true; - - //subgraph_data - this.global_inputs = {}; - this.global_outputs = {}; - - //notify canvas to redraw - this.change(); - - this.sendActionToCanvas("clear"); -} - -/** -* Attach Canvas to this graph -* @method attachCanvas -* @param {GraphCanvas} graph_canvas -*/ - -LGraph.prototype.attachCanvas = function(graphcanvas) -{ - if(graphcanvas.constructor != LGraphCanvas) - throw("attachCanvas expects a LGraphCanvas instance"); - if(graphcanvas.graph && graphcanvas.graph != this) - graphcanvas.graph.detachCanvas( graphcanvas ); - - graphcanvas.graph = this; - if(!this.list_of_graphcanvas) - this.list_of_graphcanvas = []; - this.list_of_graphcanvas.push(graphcanvas); -} - -/** -* Detach Canvas from this graph -* @method detachCanvas -* @param {GraphCanvas} graph_canvas -*/ -LGraph.prototype.detachCanvas = function(graphcanvas) -{ - if(!this.list_of_graphcanvas) - return; - - var pos = this.list_of_graphcanvas.indexOf( graphcanvas ); - if(pos == -1) - return; - graphcanvas.graph = null; - this.list_of_graphcanvas.splice(pos,1); -} - -/** -* Starts running this graph every interval milliseconds. -* @method start -* @param {number} interval amount of milliseconds between executions, default is 1 -*/ - -LGraph.prototype.start = function(interval) -{ - if( this.status == LGraph.STATUS_RUNNING ) - return; - this.status = LGraph.STATUS_RUNNING; - - if(this.onPlayEvent) - this.onPlayEvent(); - - this.sendEventToAllNodes("onStart"); - - //launch - this.starttime = LiteGraph.getTime(); - this.last_update_time = this.starttime; - interval = interval || 1; - var that = this; - - this.execution_timer_id = setInterval( function() { - //execute - that.runStep(1, !this.catch_errors ); - },interval); -} - -/** -* Stops the execution loop of the graph -* @method stop execution -*/ - -LGraph.prototype.stop = function() -{ - if( this.status == LGraph.STATUS_STOPPED ) - return; - - this.status = LGraph.STATUS_STOPPED; - - if(this.onStopEvent) - this.onStopEvent(); - - if(this.execution_timer_id != null) - clearInterval(this.execution_timer_id); - this.execution_timer_id = null; - - this.sendEventToAllNodes("onStop"); -} - -/** -* Run N steps (cycles) of the graph -* @method runStep -* @param {number} num number of steps to run, default is 1 -*/ - -LGraph.prototype.runStep = function( num, do_not_catch_errors ) -{ - num = num || 1; - - var start = LiteGraph.getTime(); - this.globaltime = 0.001 * (start - this.starttime); - - var nodes = this._nodes_executable ? this._nodes_executable : this._nodes; - if(!nodes) - return; - - if( do_not_catch_errors ) - { - //iterations - for(var i = 0; i < num; i++) - { - for( var j = 0, l = nodes.length; j < l; ++j ) - { - var node = nodes[j]; - if( node.mode == LiteGraph.ALWAYS && node.onExecute ) - node.onExecute(); - } - - this.fixedtime += this.fixedtime_lapse; - if( this.onExecuteStep ) - this.onExecuteStep(); - } - - if( this.onAfterExecute ) - this.onAfterExecute(); - } - else - { - try - { - //iterations - for(var i = 0; i < num; i++) - { - for( var j = 0, l = nodes.length; j < l; ++j ) - { - var node = nodes[j]; - if( node.mode == LiteGraph.ALWAYS && node.onExecute ) - node.onExecute(); - } - - this.fixedtime += this.fixedtime_lapse; - if( this.onExecuteStep ) - this.onExecuteStep(); - } - - if( this.onAfterExecute ) - this.onAfterExecute(); - this.errors_in_execution = false; - } - catch (err) - { - this.errors_in_execution = true; - if(LiteGraph.throw_errors) - throw err; - if(LiteGraph.debug) - console.log("Error during execution: " + err); - this.stop(); - } - } - - var now = LiteGraph.getTime(); - var elapsed = now - start; - if (elapsed == 0) - elapsed = 1; - this.execution_time = 0.001 * elapsed; - this.globaltime += 0.001 * elapsed; - this.iteration += 1; - this.elapsed_time = (now - this.last_update_time) * 0.001; - this.last_update_time = now; -} - -/** -* Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than -* nodes with only inputs. -* @method updateExecutionOrder -*/ -LGraph.prototype.updateExecutionOrder = function() -{ - this._nodes_in_order = this.computeExecutionOrder( false ); - this._nodes_executable = []; - for(var i = 0; i < this._nodes_in_order.length; ++i) - if( this._nodes_in_order[i].onExecute ) - this._nodes_executable.push( this._nodes_in_order[i] ); -} - -//This is more internal, it computes the order and returns it -LGraph.prototype.computeExecutionOrder = function( only_onExecute, set_level ) -{ - var L = []; - var S = []; - var M = {}; - var visited_links = {}; //to avoid repeating links - var remaining_links = {}; //to a - - //search for the nodes without inputs (starting nodes) - for (var i = 0, l = this._nodes.length; i < l; ++i) - { - var node = this._nodes[i]; - if( only_onExecute && !node.onExecute ) - continue; - - M[node.id] = node; //add to pending nodes - - var num = 0; //num of input connections - if(node.inputs) - for(var j = 0, l2 = node.inputs.length; j < l2; j++) - if(node.inputs[j] && node.inputs[j].link != null) - num += 1; - - if(num == 0) //is a starting node - { - S.push(node); - if(set_level) - node._level = 1; - } - else //num of input links - { - if(set_level) - node._level = 0; - remaining_links[node.id] = num; - } - } - - while(true) - { - if(S.length == 0) - break; - - //get an starting node - var node = S.shift(); - L.push(node); //add to ordered list - delete M[node.id]; //remove from the pending nodes - - if(!node.outputs) - continue; - - //for every output - for(var i = 0; i < node.outputs.length; i++) - { - var output = node.outputs[i]; - //not connected - if(output == null || output.links == null || output.links.length == 0) - continue; - - //for every connection - for(var j = 0; j < output.links.length; j++) - { - var link_id = output.links[j]; - var link = this.links[link_id]; - if(!link) - continue; - - //already visited link (ignore it) - if(visited_links[ link.id ]) - continue; - - var target_node = this.getNodeById( link.target_id ); - if(target_node == null) - { - visited_links[ link.id ] = true; - continue; - } - - if(set_level && (!target_node._level || target_node._level <= node._level)) - target_node._level = node._level + 1; - - visited_links[link.id] = true; //mark as visited - remaining_links[target_node.id] -= 1; //reduce the number of links remaining - if (remaining_links[ target_node.id ] == 0) - S.push(target_node); //if no more links, then add to starters array - } - } - } - - //the remaining ones (loops) - for(var i in M) - L.push( M[i] ); - - if( L.length != this._nodes.length && LiteGraph.debug ) - console.warn("something went wrong, nodes missing"); - - var l = L.length; - - //save order number in the node - for(var i = 0; i < l; ++i) - L[i].order = i; - - //sort now by priority - L = L.sort(function(A,B){ - var Ap = A.constructor.priority || A.priority || 0; - var Bp = B.constructor.priority || B.priority || 0; - if(Ap == Bp) - return A.order - B.order; - return Ap - Bp; - }); - - //save order number in the node, again... - for(var i = 0; i < l; ++i) - L[i].order = i; - - return L; -} - -/** -* Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively. -* It doesnt include the node itself -* @method getAncestors -* @return {Array} an array with all the LGraphNodes that affect this node, in order of execution -*/ -LGraph.prototype.getAncestors = function( node ) -{ - var ancestors = []; - var pending = [node]; - var visited = {}; - - while (pending.length) - { - var current = pending.shift(); - if(!current.inputs) - continue; - if( !visited[ current.id ] && current != node ) - { - visited[ current.id ] = true; - ancestors.push( current ); - } - - for(var i = 0; i < current.inputs.length;++i) - { - var input = current.getInputNode(i); - if( input && ancestors.indexOf( input ) == -1) - { - pending.push( input ); - } - } - } - - ancestors.sort(function(a,b){ return a.order - b.order;}); - return ancestors; -} - -/** -* Positions every node in a more readable manner -* @method arrange -*/ -LGraph.prototype.arrange = function( margin ) -{ - margin = margin || 40; - - var nodes = this.computeExecutionOrder( false, true ); - var columns = []; - for(var i = 0; i < nodes.length; ++i) - { - var node = nodes[i]; - var col = node._level || 1; - if(!columns[col]) - columns[col] = []; - columns[col].push( node ); - } - - var x = margin; - - for(var i = 0; i < columns.length; ++i) - { - var column = columns[i]; - if(!column) - continue; - var max_size = 100; - var y = margin; - for(var j = 0; j < column.length; ++j) - { - var node = column[j]; - node.pos[0] = x; - node.pos[1] = y; - if(node.size[0] > max_size) - max_size = node.size[0]; - y += node.size[1] + margin; - } - x += max_size + margin; - } - - this.setDirtyCanvas(true,true); -} - - -/** -* Returns the amount of time the graph has been running in milliseconds -* @method getTime -* @return {number} number of milliseconds the graph has been running -*/ -LGraph.prototype.getTime = function() -{ - return this.globaltime; -} - -/** -* Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant -* @method getFixedTime -* @return {number} number of milliseconds the graph has been running -*/ - -LGraph.prototype.getFixedTime = function() -{ - return this.fixedtime; -} - -/** -* Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct -* if the nodes are using graphical actions -* @method getElapsedTime -* @return {number} number of milliseconds it took the last cycle -*/ - -LGraph.prototype.getElapsedTime = function() -{ - return this.elapsed_time; -} - -/** -* Sends an event to all the nodes, useful to trigger stuff -* @method sendEventToAllNodes -* @param {String} eventname the name of the event (function to be called) -* @param {Array} params parameters in array format -*/ - -LGraph.prototype.sendEventToAllNodes = function( eventname, params, mode ) -{ - mode = mode || LiteGraph.ALWAYS; - - var nodes = this._nodes_in_order ? this._nodes_in_order : this._nodes; - if(!nodes) - return; - - for( var j = 0, l = nodes.length; j < l; ++j ) - { - var node = nodes[j]; - if(node[eventname] && node.mode == mode ) - { - if(params === undefined) - node[eventname](); - else if(params && params.constructor === Array) - node[eventname].apply( node, params ); - else - node[eventname](params); - } - } -} - -LGraph.prototype.sendActionToCanvas = function(action, params) -{ - if(!this.list_of_graphcanvas) - return; - - for(var i = 0; i < this.list_of_graphcanvas.length; ++i) - { - var c = this.list_of_graphcanvas[i]; - if( c[action] ) - c[action].apply(c, params); - } -} - -/** -* Adds a new node instasnce to this graph -* @method add -* @param {LGraphNode} node the instance of the node -*/ - -LGraph.prototype.add = function( node, skip_compute_order) -{ - if(!node) - return; - - //groups - if( node.constructor === LGraphGroup ) - { - this._groups.push( node ); - this.setDirtyCanvas(true); - this.change(); - node.graph = this; - this._version++; - return; - } - - //nodes - if(node.id != -1 && this._nodes_by_id[node.id] != null) - { - console.warn("LiteGraph: there is already a node with this ID, changing it"); - node.id = ++this.last_node_id; - } - - if(this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) - throw("LiteGraph: max number of nodes in a graph reached"); - - //give him an id - if(node.id == null || node.id == -1) - node.id = ++this.last_node_id; - else if (this.last_node_id < node.id) - this.last_node_id = node.id; - - - node.graph = this; - this._version++; - - this._nodes.push(node); - this._nodes_by_id[node.id] = node; - - if(node.onAdded) - node.onAdded( this ); - - if(this.config.align_to_grid) - node.alignToGrid(); - - if(!skip_compute_order) - this.updateExecutionOrder(); - - if(this.onNodeAdded) - this.onNodeAdded(node); - - - this.setDirtyCanvas(true); - this.change(); - - return node; //to chain actions -} - -/** -* Removes a node from the graph -* @method remove -* @param {LGraphNode} node the instance of the node -*/ - -LGraph.prototype.remove = function(node) -{ - if(node.constructor === LiteGraph.LGraphGroup) - { - var index = this._groups.indexOf(node); - if(index != -1) - this._groups.splice(index,1); - node.graph = null; - this._version++; - this.setDirtyCanvas(true,true); - this.change(); - return; - } - - if(this._nodes_by_id[node.id] == null) - return; //not found - - if(node.ignore_remove) - return; //cannot be removed - - //disconnect inputs - if(node.inputs) - for(var i = 0; i < node.inputs.length; i++) - { - var slot = node.inputs[i]; - if(slot.link != null) - node.disconnectInput(i); - } - - //disconnect outputs - if(node.outputs) - for(var i = 0; i < node.outputs.length; i++) - { - var slot = node.outputs[i]; - if(slot.links != null && slot.links.length) - node.disconnectOutput(i); - } - - //node.id = -1; //why? - - //callback - if(node.onRemoved) - node.onRemoved(); - - node.graph = null; - this._version++; - - //remove from canvas render - if(this.list_of_graphcanvas) - { - for(var i = 0; i < this.list_of_graphcanvas.length; ++i) - { - var canvas = this.list_of_graphcanvas[i]; - if(canvas.selected_nodes[node.id]) - delete canvas.selected_nodes[node.id]; - if(canvas.node_dragged == node) - canvas.node_dragged = null; - } - } - - //remove from containers - var pos = this._nodes.indexOf(node); - if(pos != -1) - this._nodes.splice(pos,1); - delete this._nodes_by_id[node.id]; - - if(this.onNodeRemoved) - this.onNodeRemoved(node); - - this.setDirtyCanvas(true,true); - this.change(); - - this.updateExecutionOrder(); -} - -/** -* Returns a node by its id. -* @method getNodeById -* @param {Number} id -*/ - -LGraph.prototype.getNodeById = function( id ) -{ - if( id == null ) - return null; - return this._nodes_by_id[ id ]; -} - -/** -* Returns a list of nodes that matches a class -* @method findNodesByClass -* @param {Class} classObject the class itself (not an string) -* @return {Array} a list with all the nodes of this type -*/ - -LGraph.prototype.findNodesByClass = function(classObject) -{ - var r = []; - for(var i = 0, l = this._nodes.length; i < l; ++i) - if(this._nodes[i].constructor === classObject) - r.push(this._nodes[i]); - return r; -} - -/** -* Returns a list of nodes that matches a type -* @method findNodesByType -* @param {String} type the name of the node type -* @return {Array} a list with all the nodes of this type -*/ - -LGraph.prototype.findNodesByType = function(type) -{ - var type = type.toLowerCase(); - var r = []; - for(var i = 0, l = this._nodes.length; i < l; ++i) - if(this._nodes[i].type.toLowerCase() == type ) - r.push(this._nodes[i]); - return r; -} - -/** -* Returns a list of nodes that matches a name -* @method findNodesByTitle -* @param {String} name the name of the node to search -* @return {Array} a list with all the nodes with this name -*/ - -LGraph.prototype.findNodesByTitle = function(title) -{ - var result = []; - for(var i = 0, l = this._nodes.length; i < l; ++i) - if(this._nodes[i].title == title) - result.push(this._nodes[i]); - return result; -} - -/** -* Returns the top-most node in this position of the canvas -* @method getNodeOnPos -* @param {number} x the x coordinate in canvas space -* @param {number} y the y coordinate in canvas space -* @param {Array} nodes_list a list with all the nodes to search from, by default is all the nodes in the graph -* @return {LGraphNode} the node at this position or null -*/ -LGraph.prototype.getNodeOnPos = function(x,y, nodes_list) -{ - nodes_list = nodes_list || this._nodes; - for (var i = nodes_list.length - 1; i >= 0; i--) - { - var n = nodes_list[i]; - if(n.isPointInside( x, y, 2 )) - return n; - } - return null; -} - -/** -* Returns the top-most group in that position -* @method getGroupOnPos -* @param {number} x the x coordinate in canvas space -* @param {number} y the y coordinate in canvas space -* @return {LGraphGroup} the group or null -*/ -LGraph.prototype.getGroupOnPos = function(x,y) -{ - for (var i = this._groups.length - 1; i >= 0; i--) - { - var g = this._groups[i]; - if(g.isPointInside( x, y, 2 )) - return g; - } - return null; -} - -// ********** GLOBALS ***************** - -/** -* Tell this graph it has a global graph input of this type -* @method addGlobalInput -* @param {String} name -* @param {String} type -* @param {*} value [optional] -*/ -LGraph.prototype.addGlobalInput = function(name, type, value) -{ - this.global_inputs[name] = { name: name, type: type, value: value }; - this._version++; - - if(this.onGlobalInputAdded) - this.onGlobalInputAdded(name, type); - - if(this.onGlobalsChange) - this.onGlobalsChange(); -} - -/** -* Assign a data to the global graph input -* @method setGlobalInputData -* @param {String} name -* @param {*} data -*/ -LGraph.prototype.setGlobalInputData = function(name, data) -{ - var input = this.global_inputs[name]; - if (!input) - return; - input.value = data; -} - -/** -* Assign a data to the global graph input (same as setGlobalInputData) -* @method setInputData -* @param {String} name -* @param {*} data -*/ -LGraph.prototype.setInputData = LGraph.prototype.setGlobalInputData; - - -/** -* Returns the current value of a global graph input -* @method getGlobalInputData -* @param {String} name -* @return {*} the data -*/ -LGraph.prototype.getGlobalInputData = function(name) -{ - var input = this.global_inputs[name]; - if (!input) - return null; - return input.value; -} - -/** -* Changes the name of a global graph input -* @method renameGlobalInput -* @param {String} old_name -* @param {String} new_name -*/ -LGraph.prototype.renameGlobalInput = function(old_name, name) -{ - if(name == old_name) - return; - - if(!this.global_inputs[old_name]) - return false; - - if(this.global_inputs[name]) - { - console.error("there is already one input with that name"); - return false; - } - - this.global_inputs[name] = this.global_inputs[old_name]; - delete this.global_inputs[old_name]; - this._version++; - - if(this.onGlobalInputRenamed) - this.onGlobalInputRenamed(old_name, name); - - if(this.onGlobalsChange) - this.onGlobalsChange(); -} - -/** -* Changes the type of a global graph input -* @method changeGlobalInputType -* @param {String} name -* @param {String} type -*/ -LGraph.prototype.changeGlobalInputType = function(name, type) -{ - if(!this.global_inputs[name]) - return false; - - if(this.global_inputs[name].type == type || this.global_inputs[name].type.toLowerCase() == type.toLowerCase() ) - return; - - this.global_inputs[name].type = type; - this._version++; - if(this.onGlobalInputTypeChanged) - this.onGlobalInputTypeChanged(name, type); -} - -/** -* Removes a global graph input -* @method removeGlobalInput -* @param {String} name -* @param {String} type -*/ -LGraph.prototype.removeGlobalInput = function(name) -{ - if(!this.global_inputs[name]) - return false; - - delete this.global_inputs[name]; - this._version++; - - if(this.onGlobalInputRemoved) - this.onGlobalInputRemoved(name); - - if(this.onGlobalsChange) - this.onGlobalsChange(); - return true; -} - -/** -* Creates a global graph output -* @method addGlobalOutput -* @param {String} name -* @param {String} type -* @param {*} value -*/ -LGraph.prototype.addGlobalOutput = function(name, type, value) -{ - this.global_outputs[name] = { name: name, type: type, value: value }; - this._version++; - - if(this.onGlobalOutputAdded) - this.onGlobalOutputAdded(name, type); - - if(this.onGlobalsChange) - this.onGlobalsChange(); -} - -/** -* Assign a data to the global output -* @method setGlobalOutputData -* @param {String} name -* @param {String} value -*/ -LGraph.prototype.setGlobalOutputData = function(name, value) -{ - var output = this.global_outputs[ name ]; - if (!output) - return; - output.value = value; -} - -/** -* Returns the current value of a global graph output -* @method getGlobalOutputData -* @param {String} name -* @return {*} the data -*/ -LGraph.prototype.getGlobalOutputData = function(name) -{ - var output = this.global_outputs[name]; - if (!output) - return null; - return output.value; -} - -/** -* Returns the current value of a global graph output (sames as getGlobalOutputData) -* @method getOutputData -* @param {String} name -* @return {*} the data -*/ -LGraph.prototype.getOutputData = LGraph.prototype.getGlobalOutputData; - - -/** -* Renames a global graph output -* @method renameGlobalOutput -* @param {String} old_name -* @param {String} new_name -*/ -LGraph.prototype.renameGlobalOutput = function(old_name, name) -{ - if(!this.global_outputs[old_name]) - return false; - - if(this.global_outputs[name]) - { - console.error("there is already one output with that name"); - return false; - } - - this.global_outputs[name] = this.global_outputs[old_name]; - delete this.global_outputs[old_name]; - this._version++; - - if(this.onGlobalOutputRenamed) - this.onGlobalOutputRenamed(old_name, name); - - if(this.onGlobalsChange) - this.onGlobalsChange(); -} - -/** -* Changes the type of a global graph output -* @method changeGlobalOutputType -* @param {String} name -* @param {String} type -*/ -LGraph.prototype.changeGlobalOutputType = function(name, type) -{ - if(!this.global_outputs[name]) - return false; - - if(this.global_outputs[name].type.toLowerCase() == type.toLowerCase() ) - return; - - this.global_outputs[name].type = type; - this._version++; - if(this.onGlobalOutputTypeChanged) - this.onGlobalOutputTypeChanged(name, type); -} - -/** -* Removes a global graph output -* @method removeGlobalOutput -* @param {String} name -*/ -LGraph.prototype.removeGlobalOutput = function(name) -{ - if(!this.global_outputs[name]) - return false; - delete this.global_outputs[name]; - this._version++; - - if(this.onGlobalOutputRemoved) - this.onGlobalOutputRemoved(name); - - if(this.onGlobalsChange) - this.onGlobalsChange(); - return true; -} - -LGraph.prototype.triggerInput = function(name,value) -{ - var nodes = this.findNodesByTitle(name); - for(var i = 0; i < nodes.length; ++i) - nodes[i].onTrigger(value); -} - -LGraph.prototype.setCallback = function(name,func) -{ - var nodes = this.findNodesByTitle(name); - for(var i = 0; i < nodes.length; ++i) - nodes[i].setTrigger(func); -} - - -LGraph.prototype.connectionChange = function( node ) -{ - this.updateExecutionOrder(); - if( this.onConnectionChange ) - this.onConnectionChange( node ); - this._version++; - this.sendActionToCanvas("onConnectionChange"); -} - -/** -* returns if the graph is in live mode -* @method isLive -*/ - -LGraph.prototype.isLive = function() -{ - if(!this.list_of_graphcanvas) - return false; - - for(var i = 0; i < this.list_of_graphcanvas.length; ++i) - { - var c = this.list_of_graphcanvas[i]; - if(c.live_mode) - return true; - } - return false; -} - -/* Called when something visually changed (not the graph!) */ -LGraph.prototype.change = function() -{ - if(LiteGraph.debug) - console.log("Graph changed"); - this.sendActionToCanvas("setDirty",[true,true]); - if(this.on_change) - this.on_change(this); -} - -LGraph.prototype.setDirtyCanvas = function(fg,bg) -{ - this.sendActionToCanvas("setDirty",[fg,bg]); -} - -//save and recover app state *************************************** -/** -* Creates a Object containing all the info about this graph, it can be serialized -* @method serialize -* @return {Object} value of the node -*/ -LGraph.prototype.serialize = function() -{ - var nodes_info = []; - for(var i = 0, l = this._nodes.length; i < l; ++i) - nodes_info.push( this._nodes[i].serialize() ); - - //pack link info into a non-verbose format - var links = []; - for(var i in this.links) //links is an OBJECT - { - var link = this.links[i]; - links.push([ link.id, link.origin_id, link.origin_slot, link.target_id, link.target_slot, link.type ]); - } - - var groups_info = []; - for(var i = 0; i < this._groups.length; ++i) - groups_info.push( this._groups[i].serialize() ); - - var data = { - last_node_id: this.last_node_id, - last_link_id: this.last_link_id, - nodes: nodes_info, - links: links, - groups: groups_info, - config: this.config - }; - - return data; -} - - -/** -* Configure a graph from a JSON string -* @method configure -* @param {String} str configure a graph from a JSON string -* @param {Boolean} returns if there was any error parsing -*/ -LGraph.prototype.configure = function( data, keep_old ) -{ - if(!data) - return; - - if(!keep_old) - this.clear(); - - var nodes = data.nodes; - - //decode links info (they are very verbose) - if(data.links && data.links.constructor === Array) - { - var links = []; - for(var i = 0; i < data.links.length; ++i) - { - var link = data.links[i]; - links[ link[0] ] = { id: link[0], origin_id: link[1], origin_slot: link[2], target_id: link[3], target_slot: link[4], type: link[5] }; - } - data.links = links; - } - - //copy all stored fields - for (var i in data) - this[i] = data[i]; - - var error = false; - - //create nodes - this._nodes = []; - if(nodes) - { - for(var i = 0, l = nodes.length; i < l; ++i) - { - var n_info = nodes[i]; //stored info - var node = LiteGraph.createNode( n_info.type, n_info.title ); - if(!node) - { - if(LiteGraph.debug) - console.log("Node not found: " + n_info.type); - error = true; - continue; - } - - node.id = n_info.id; //id it or it will create a new id - this.add(node, true); //add before configure, otherwise configure cannot create links - } - - //configure nodes afterwards so they can reach each other - for(var i = 0, l = nodes.length; i < l; ++i) - { - var n_info = nodes[i]; - var node = this.getNodeById( n_info.id ); - if(node) - node.configure( n_info ); - } - } - - //groups - this._groups.length = 0; - if( data.groups ) - for(var i = 0; i < data.groups.length; ++i ) - { - var group = new LiteGraph.LGraphGroup(); - group.configure( data.groups[i] ); - this.add( group ); - } - - this.updateExecutionOrder(); - this._version++; - this.setDirtyCanvas(true,true); - return error; -} - -LGraph.prototype.load = function(url) -{ - var that = this; - var req = new XMLHttpRequest(); - req.open('GET', url, true); - req.send(null); - req.onload = function (oEvent) { - if(req.status !== 200) - { - console.error("Error loading graph:",req.status,req.response); - return; - } - var data = JSON.parse( req.response ); - that.configure(data); - } - req.onerror = function(err) - { - console.error("Error loading graph:",err); - } -} - -LGraph.prototype.onNodeTrace = function(node, msg, color) -{ - //TODO -} - -// ************************************************************* -// Node CLASS ******* -// ************************************************************* - -/* - title: string - pos: [x,y] - size: [x,y] - - input|output: every connection - + { name:string, type:string, pos: [x,y]=Optional, direction: "input"|"output", links: Array }); - - flags: - + clip_area: if you render outside the node, it will be cliped - + unsafe_execution: not allowed for safe execution - + skip_repeated_outputs: when adding new outputs, it wont show if there is one already connected - + resizable: if set to false it wont be resizable with the mouse - - supported callbacks: - + onAdded: when added to graph - + onRemoved: when removed from graph - + onStart: when the graph starts playing - + onStop: when the graph stops playing - + onDrawForeground: render the inside widgets inside the node - + onDrawBackground: render the background area inside the node (only in edit mode) - + onMouseDown - + onMouseMove - + onMouseUp - + onMouseEnter - + onMouseLeave - + onExecute: execute the node - + onPropertyChanged: when a property is changed in the panel (return true to skip default behaviour) - + onGetInputs: returns an array of possible inputs - + onGetOutputs: returns an array of possible outputs - + onDblClick - + onSerialize - + onSelected - + onDeselected - + onDropItem : DOM item dropped over the node - + onDropFile : file dropped over the node - + onConnectInput : if returns false the incoming connection will be canceled - + onConnectionsChange : a connection changed (new one or removed) (LiteGraph.INPUT or LiteGraph.OUTPUT, slot, true if connected, link_info, input_info ) -*/ - -/** -* Base Class for all the node type classes -* @class LGraphNode -* @param {String} name a name for the node -*/ - -function LGraphNode(title) -{ - this._ctor(title); -} - -global.LGraphNode = LiteGraph.LGraphNode = LGraphNode; - -LGraphNode.prototype._ctor = function( title ) -{ - this.title = title || "Unnamed"; - this.size = [LiteGraph.NODE_WIDTH,60]; - this.graph = null; - - this._pos = new Float32Array(10,10); - - Object.defineProperty( this, "pos", { - set: function(v) - { - if(!v || v.length < 2) - return; - this._pos[0] = v[0]; - this._pos[1] = v[1]; - }, - get: function() - { - return this._pos; - }, - enumerable: true - }); - - this.id = -1; //not know till not added - this.type = null; - - //inputs available: array of inputs - this.inputs = []; - this.outputs = []; - this.connections = []; - - //local data - this.properties = {}; //for the values - this.properties_info = []; //for the info - - this.data = null; //persistent local data - this.flags = {}; -} - -/** -* configure a node from an object containing the serialized info -* @method configure -*/ -LGraphNode.prototype.configure = function(info) -{ - if(this.graph) - this.graph._version++; - - for (var j in info) - { - if(j == "console") - continue; - - if(j == "properties") - { - //i dont want to clone properties, I want to reuse the old container - for(var k in info.properties) - { - this.properties[k] = info.properties[k]; - if(this.onPropertyChanged) - this.onPropertyChanged(k,info.properties[k]); - } - continue; - } - - if(info[j] == null) - continue; - - else if (typeof(info[j]) == 'object') //object - { - if(this[j] && this[j].configure) - this[j].configure( info[j] ); - else - this[j] = LiteGraph.cloneObject(info[j], this[j]); - } - else //value - this[j] = info[j]; - } - - if(!info.title) - this.title = this.constructor.title; - - if(this.onConnectionsChange) - { - if(this.inputs) - for(var i = 0; i < this.inputs.length; ++i) - { - var input = this.inputs[i]; - var link_info = this.graph ? this.graph.links[ input.link ] : null; - this.onConnectionsChange( LiteGraph.INPUT, i, true, link_info, input ); //link_info has been created now, so its updated - } - - if(this.outputs) - for(var i = 0; i < this.outputs.length; ++i) - { - var output = this.outputs[i]; - if(!output.links) - continue; - for(var j = 0; j < output.links.length; ++j) - { - var link_info = this.graph ? this.graph.links[ output.links[j] ] : null; - this.onConnectionsChange( LiteGraph.OUTPUT, i, true, link_info, output ); //link_info has been created now, so its updated - } - } - } - - //FOR LEGACY, PLEASE REMOVE ON NEXT VERSION - for(var i in this.inputs) - { - var input = this.inputs[i]; - if(!input.link || !input.link.length ) - continue; - var link = input.link; - if(typeof(link) != "object") - continue; - input.link = link[0]; - if(this.graph) - this.graph.links[ link[0] ] = { - id: link[0], - origin_id: link[1], - origin_slot: link[2], - target_id: link[3], - target_slot: link[4] - }; - } - for(var i in this.outputs) - { - var output = this.outputs[i]; - if(!output.links || output.links.length == 0) - continue; - for(var j in output.links) - { - var link = output.links[j]; - if(typeof(link) != "object") - continue; - output.links[j] = link[0]; - } - } - - if( this.onConfigure ) - this.onConfigure( info ); -} - -/** -* serialize the content -* @method serialize -*/ - -LGraphNode.prototype.serialize = function() -{ - //create serialization object - var o = { - id: this.id, - type: this.type, - pos: this.pos, - size: this.size, - data: this.data, - flags: LiteGraph.cloneObject(this.flags), - mode: this.mode - }; - - if( this.inputs ) - o.inputs = this.inputs; - - if( this.outputs ) - { - //clear outputs last data (because data in connections is never serialized but stored inside the outputs info) - for(var i = 0; i < this.outputs.length; i++) - delete this.outputs[i]._data; - o.outputs = this.outputs; - } - - if( this.title && this.title != this.constructor.title ) - o.title = this.title; - - if( this.properties ) - o.properties = LiteGraph.cloneObject( this.properties ); - - if( !o.type ) - o.type = this.constructor.type; - - if( this.color ) - o.color = this.color; - if( this.bgcolor ) - o.bgcolor = this.bgcolor; - if( this.boxcolor ) - o.boxcolor = this.boxcolor; - if( this.shape ) - o.shape = this.shape; - - if(this.onSerialize) - this.onSerialize(o); - - return o; -} - - -/* Creates a clone of this node */ -LGraphNode.prototype.clone = function() -{ - var node = LiteGraph.createNode(this.type); - - //we clone it because serialize returns shared containers - var data = LiteGraph.cloneObject( this.serialize() ); - - //remove links - if(data.inputs) - for(var i = 0; i < data.inputs.length; ++i) - data.inputs[i].link = null; - - if(data.outputs) - for(var i = 0; i < data.outputs.length; ++i) - { - if(data.outputs[i].links) - data.outputs[i].links.length = 0; - } - - delete data["id"]; - //remove links - node.configure(data); - - return node; -} - - -/** -* serialize and stringify -* @method toString -*/ - -LGraphNode.prototype.toString = function() -{ - return JSON.stringify( this.serialize() ); -} -//LGraphNode.prototype.unserialize = function(info) {} //this cannot be done from within, must be done in LiteGraph - - -/** -* get the title string -* @method getTitle -*/ - -LGraphNode.prototype.getTitle = function() -{ - return this.title || this.constructor.title; -} - - - -// Execution ************************* -/** -* sets the output data -* @method setOutputData -* @param {number} slot -* @param {*} data -*/ -LGraphNode.prototype.setOutputData = function(slot, data) -{ - if(!this.outputs) - return; - - //this maybe slow and a niche case - //if(slot && slot.constructor === String) - // slot = this.findOutputSlot(slot); - - if(slot == -1 || slot >= this.outputs.length) - return; - - var output_info = this.outputs[slot]; - if(!output_info) - return; - - //store data in the output itself in case we want to debug - output_info._data = data; - - //if there are connections, pass the data to the connections - if( this.outputs[slot].links ) - { - for(var i = 0; i < this.outputs[slot].links.length; i++) - { - var link_id = this.outputs[slot].links[i]; - this.graph.links[ link_id ].data = data; - } - } -} - -/** -* Retrieves the input data (data traveling through the connection) from one slot -* @method getInputData -* @param {number} slot -* @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link -* @return {*} data or if it is not connected returns undefined -*/ -LGraphNode.prototype.getInputData = function( slot, force_update ) -{ - if(!this.inputs) - return; //undefined; - - if(slot >= this.inputs.length || this.inputs[slot].link == null) - return; - - var link_id = this.inputs[slot].link; - var link = this.graph.links[ link_id ]; - if(!link) //bug: weird case but it happens sometimes - return null; - - if(!force_update) - return link.data; - - //special case: used to extract data from the incomming connection before the graph has been executed - var node = this.graph.getNodeById( link.origin_id ); - if(!node) - return link.data; - - if(node.updateOutputData) - node.updateOutputData( link.origin_slot ); - else if(node.onExecute) - node.onExecute(); - - return link.data; -} - -/** -* Retrieves the input data from one slot using its name instead of slot number -* @method getInputDataByName -* @param {String} slot_name -* @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link -* @return {*} data or if it is not connected returns null -*/ -LGraphNode.prototype.getInputDataByName = function( slot_name, force_update ) -{ - var slot = this.findInputSlot( slot_name ); - if( slot == -1 ) - return null; - return this.getInputData( slot, force_update ); -} - - -/** -* tells you if there is a connection in one input slot -* @method isInputConnected -* @param {number} slot -* @return {boolean} -*/ -LGraphNode.prototype.isInputConnected = function(slot) -{ - if(!this.inputs) - return false; - return (slot < this.inputs.length && this.inputs[slot].link != null); -} - -/** -* tells you info about an input connection (which node, type, etc) -* @method getInputInfo -* @param {number} slot -* @return {Object} object or null { link: id, name: string, type: string or 0 } -*/ -LGraphNode.prototype.getInputInfo = function(slot) -{ - if(!this.inputs) - return null; - if(slot < this.inputs.length) - return this.inputs[slot]; - return null; -} - -/** -* returns the node connected in the input slot -* @method getInputNode -* @param {number} slot -* @return {LGraphNode} node or null -*/ -LGraphNode.prototype.getInputNode = function( slot ) -{ - if(!this.inputs) - return null; - if(slot >= this.inputs.length) - return null; - var input = this.inputs[slot]; - if(!input || !input.link) - return null; - var link_info = this.graph.links[ input.link ]; - if(!link_info) - return null; - return this.graph.getNodeById( link_info.origin_id ); -} - - -/** -* returns the value of an input with this name, otherwise checks if there is a property with that name -* @method getInputOrProperty -* @param {string} name -* @return {*} value -*/ -LGraphNode.prototype.getInputOrProperty = function( name ) -{ - if(!this.inputs || !this.inputs.length) - return this.properties ? this.properties[name] : null; - - for(var i = 0, l = this.inputs.length; i < l; ++i) - if(name == this.inputs[i].name) - { - var link_id = this.inputs[i].link; - var link = this.graph.links[ link_id ]; - return link ? link.data : null; - } - return this.properties[name]; -} - - - - -/** -* tells you the last output data that went in that slot -* @method getOutputData -* @param {number} slot -* @return {Object} object or null -*/ -LGraphNode.prototype.getOutputData = function(slot) -{ - if(!this.outputs) - return null; - if(slot >= this.outputs.length) - return null; - - var info = this.outputs[slot]; - return info._data; -} - - -/** -* tells you info about an output connection (which node, type, etc) -* @method getOutputInfo -* @param {number} slot -* @return {Object} object or null { name: string, type: string, links: [ ids of links in number ] } -*/ -LGraphNode.prototype.getOutputInfo = function(slot) -{ - if(!this.outputs) - return null; - if(slot < this.outputs.length) - return this.outputs[slot]; - return null; -} - - -/** -* tells you if there is a connection in one output slot -* @method isOutputConnected -* @param {number} slot -* @return {boolean} -*/ -LGraphNode.prototype.isOutputConnected = function(slot) -{ - if(!this.outputs) - return false; - return (slot < this.outputs.length && this.outputs[slot].links && this.outputs[slot].links.length); -} - -/** -* tells you if there is any connection in the output slots -* @method isAnyOutputConnected -* @return {boolean} -*/ -LGraphNode.prototype.isAnyOutputConnected = function() -{ - if(!this.outputs) - return false; - for(var i = 0; i < this.outputs.length; ++i) - if( this.outputs[i].links && this.outputs[i].links.length ) - return true; - return false; -} - - -/** -* retrieves all the nodes connected to this output slot -* @method getOutputNodes -* @param {number} slot -* @return {array} -*/ -LGraphNode.prototype.getOutputNodes = function(slot) -{ - if(!this.outputs || this.outputs.length == 0) - return null; - - if(slot >= this.outputs.length) - return null; - - var output = this.outputs[slot]; - if(!output.links || output.links.length == 0) - return null; - - var r = []; - for(var i = 0; i < output.links.length; i++) - { - var link_id = output.links[i]; - var link = this.graph.links[ link_id ]; - if(link) - { - var target_node = this.graph.getNodeById( link.target_id ); - if( target_node ) - r.push( target_node ); - } - } - return r; -} - -/** -* Triggers an event in this node, this will trigger any output with the same name -* @method trigger -* @param {String} event name ( "on_play", ... ) if action is equivalent to false then the event is send to all -* @param {*} param -*/ -LGraphNode.prototype.trigger = function( action, param ) -{ - if( !this.outputs || !this.outputs.length ) - return; - - if(this.graph) - this.graph._last_trigger_time = LiteGraph.getTime(); - - for(var i = 0; i < this.outputs.length; ++i) - { - var output = this.outputs[ i ]; - if(!output || output.type !== LiteGraph.EVENT || (action && output.name != action) ) - continue; - this.triggerSlot( i, param ); - } -} - -/** -* Triggers an slot event in this node -* @method triggerSlot -* @param {Number} slot the index of the output slot -* @param {*} param -*/ -LGraphNode.prototype.triggerSlot = function( slot, param ) -{ - if( !this.outputs ) - return; - - var output = this.outputs[ slot ]; - if( !output ) - return; - - var links = output.links; - if(!links || !links.length) - return; - - if(this.graph) - this.graph._last_trigger_time = LiteGraph.getTime(); - - //for every link attached here - for(var k = 0; k < links.length; ++k) - { - var link_info = this.graph.links[ links[k] ]; - if(!link_info) //not connected - continue; - var node = this.graph.getNodeById( link_info.target_id ); - if(!node) //node not found? - continue; - - //used to mark events in graph - link_info._last_time = LiteGraph.getTime(); - - var target_connection = node.inputs[ link_info.target_slot ]; - - if(node.onAction) - node.onAction( target_connection.name, param ); - else if(node.mode === LiteGraph.ON_TRIGGER) - { - if(node.onExecute) - node.onExecute(param); - } - } -} - -/** -* add a new property to this node -* @method addProperty -* @param {string} name -* @param {*} default_value -* @param {string} type string defining the output type ("vec3","number",...) -* @param {Object} extra_info this can be used to have special properties of the property (like values, etc) -*/ -LGraphNode.prototype.addProperty = function( name, default_value, type, extra_info ) -{ - var o = { name: name, type: type, default_value: default_value }; - if(extra_info) - for(var i in extra_info) - o[i] = extra_info[i]; - if(!this.properties_info) - this.properties_info = []; - this.properties_info.push(o); - if(!this.properties) - this.properties = {}; - this.properties[ name ] = default_value; - return o; -} - - -//connections - -/** -* add a new output slot to use in this node -* @method addOutput -* @param {string} name -* @param {string} type string defining the output type ("vec3","number",...) -* @param {Object} extra_info this can be used to have special properties of an output (label, special color, position, etc) -*/ -LGraphNode.prototype.addOutput = function(name,type,extra_info) -{ - var o = { name: name, type: type, links: null }; - if(extra_info) - for(var i in extra_info) - o[i] = extra_info[i]; - - if(!this.outputs) - this.outputs = []; - this.outputs.push(o); - if(this.onOutputAdded) - this.onOutputAdded(o); - this.size = this.computeSize(); - return o; -} - -/** -* add a new output slot to use in this node -* @method addOutputs -* @param {Array} array of triplets like [[name,type,extra_info],[...]] -*/ -LGraphNode.prototype.addOutputs = function(array) -{ - for(var i = 0; i < array.length; ++i) - { - var info = array[i]; - var o = {name:info[0],type:info[1],link:null}; - if(array[2]) - for(var j in info[2]) - o[j] = info[2][j]; - - if(!this.outputs) - this.outputs = []; - this.outputs.push(o); - if(this.onOutputAdded) - this.onOutputAdded(o); - } - - this.size = this.computeSize(); -} - -/** -* remove an existing output slot -* @method removeOutput -* @param {number} slot -*/ -LGraphNode.prototype.removeOutput = function(slot) -{ - this.disconnectOutput(slot); - this.outputs.splice(slot,1); - this.size = this.computeSize(); - if(this.onOutputRemoved) - this.onOutputRemoved(slot); -} - -/** -* add a new input slot to use in this node -* @method addInput -* @param {string} name -* @param {string} type string defining the input type ("vec3","number",...), it its a generic one use 0 -* @param {Object} extra_info this can be used to have special properties of an input (label, color, position, etc) -*/ -LGraphNode.prototype.addInput = function(name,type,extra_info) -{ - type = type || 0; - var o = {name:name,type:type,link:null}; - if(extra_info) - for(var i in extra_info) - o[i] = extra_info[i]; - - if(!this.inputs) - this.inputs = []; - this.inputs.push(o); - this.size = this.computeSize(); - if(this.onInputAdded) - this.onInputAdded(o); - return o; -} - -/** -* add several new input slots in this node -* @method addInputs -* @param {Array} array of triplets like [[name,type,extra_info],[...]] -*/ -LGraphNode.prototype.addInputs = function(array) -{ - for(var i = 0; i < array.length; ++i) - { - var info = array[i]; - var o = {name:info[0], type:info[1], link:null}; - if(array[2]) - for(var j in info[2]) - o[j] = info[2][j]; - - if(!this.inputs) - this.inputs = []; - this.inputs.push(o); - if(this.onInputAdded) - this.onInputAdded(o); - } - - this.size = this.computeSize(); -} - -/** -* remove an existing input slot -* @method removeInput -* @param {number} slot -*/ -LGraphNode.prototype.removeInput = function(slot) -{ - this.disconnectInput(slot); - this.inputs.splice(slot,1); - this.size = this.computeSize(); - if(this.onInputRemoved) - this.onInputRemoved(slot); -} - -/** -* add an special connection to this node (used for special kinds of graphs) -* @method addConnection -* @param {string} name -* @param {string} type string defining the input type ("vec3","number",...) -* @param {[x,y]} pos position of the connection inside the node -* @param {string} direction if is input or output -*/ -LGraphNode.prototype.addConnection = function(name,type,pos,direction) -{ - var o = { - name: name, - type: type, - pos: pos, - direction: direction, - links: null - }; - this.connections.push( o ); - return o; -} - -/** -* computes the size of a node according to its inputs and output slots -* @method computeSize -* @param {number} minHeight -* @return {number} the total size -*/ -LGraphNode.prototype.computeSize = function( minHeight, out ) -{ - var rows = Math.max( this.inputs ? this.inputs.length : 1, this.outputs ? this.outputs.length : 1); - var size = out || new Float32Array([0,0]); - rows = Math.max(rows, 1); - var font_size = LiteGraph.NODE_TEXT_SIZE; //although it should be graphcanvas.inner_text_font size - size[1] = (this.constructor.slot_start_y || 0) + rows * (font_size + 1) + ( this.widgets ? this.widgets.length : 0 ) * (LiteGraph.NODE_WIDGET_HEIGHT + 4 ) + 4; - - var font_size = font_size; - var title_width = compute_text_size( this.title ); - var input_width = 0; - var output_width = 0; - - if(this.inputs) - for(var i = 0, l = this.inputs.length; i < l; ++i) - { - var input = this.inputs[i]; - var text = input.label || input.name || ""; - var text_width = compute_text_size( text ); - if(input_width < text_width) - input_width = text_width; - } - - if(this.outputs) - for(var i = 0, l = this.outputs.length; i < l; ++i) - { - var output = this.outputs[i]; - var text = output.label || output.name || ""; - var text_width = compute_text_size( text ); - if(output_width < text_width) - output_width = text_width; - } - - size[0] = Math.max( input_width + output_width + 10, title_width ); - size[0] = Math.max( size[0], LiteGraph.NODE_WIDTH ); - - if(this.onResize) - this.onResize(size); - - function compute_text_size( text ) - { - if(!text) - return 0; - return font_size * text.length * 0.6; - } - - return size; -} - -/** -* Allows to pass -* -* @method addWidget -* @return {Float32Array[4]} the total size -*/ -LGraphNode.prototype.addWidget = function( type, name, value, callback, options ) -{ - if(!this.widgets) - this.widgets = []; - var w = { - type: type.toLowerCase(), - name: name, - value: value, - callback: callback, - options: options || {} - }; - - if(options.y) - w.y = options.y; - - if( type == "combo" && !w.options.values ) - throw("LiteGraph addWidget('combo',...) requires to pass values in options: { values:['red','blue'] }"); - this.widgets.push(w); - return w; -} - - -/** -* returns the bounding of the object, used for rendering purposes -* bounding is: [topleft_cornerx, topleft_cornery, width, height] -* @method getBounding -* @return {Float32Array[4]} the total size -*/ -LGraphNode.prototype.getBounding = function( out ) -{ - out = out || new Float32Array(4); - out[0] = this.pos[0] - 4; - out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT; - out[2] = this.size[0] + 4; - out[3] = this.size[1] + LiteGraph.NODE_TITLE_HEIGHT; - return out; -} - -/** -* checks if a point is inside the shape of a node -* @method isPointInside -* @param {number} x -* @param {number} y -* @return {boolean} -*/ -LGraphNode.prototype.isPointInside = function(x,y, margin) -{ - margin = margin || 0; - - var margin_top = this.graph && this.graph.isLive() ? 0 : 20; - if(this.flags && this.flags.collapsed) - { - //if ( distance([x,y], [this.pos[0] + this.size[0]*0.5, this.pos[1] + this.size[1]*0.5]) < LiteGraph.NODE_COLLAPSED_RADIUS) - if( isInsideRectangle( x, y, this.pos[0] - margin, this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT - margin, (this._collapsed_width||LiteGraph.NODE_COLLAPSED_WIDTH) + 2 * margin, LiteGraph.NODE_TITLE_HEIGHT + 2 * margin ) ) - return true; - } - else if ( (this.pos[0] - 4 - margin) < x && (this.pos[0] + this.size[0] + 4 + margin) > x - && (this.pos[1] - margin_top - margin) < y && (this.pos[1] + this.size[1] + margin) > y) - return true; - return false; -} - -/** -* checks if a point is inside a node slot, and returns info about which slot -* @method getSlotInPosition -* @param {number} x -* @param {number} y -* @return {Object} if found the object contains { input|output: slot object, slot: number, link_pos: [x,y] } -*/ -LGraphNode.prototype.getSlotInPosition = function( x, y ) -{ - //search for inputs - if(this.inputs) - for(var i = 0, l = this.inputs.length; i < l; ++i) - { - var input = this.inputs[i]; - var link_pos = this.getConnectionPos( true,i ); - if( isInsideRectangle(x, y, link_pos[0] - 10, link_pos[1] - 5, 20,10) ) - return { input: input, slot: i, link_pos: link_pos, locked: input.locked }; - } - - if(this.outputs) - for(var i = 0, l = this.outputs.length; i < l; ++i) - { - var output = this.outputs[i]; - var link_pos = this.getConnectionPos(false,i); - if( isInsideRectangle(x, y, link_pos[0] - 10, link_pos[1] - 5, 20,10) ) - return { output: output, slot: i, link_pos: link_pos, locked: output.locked }; - } - - return null; -} - -/** -* returns the input slot with a given name (used for dynamic slots), -1 if not found -* @method findInputSlot -* @param {string} name the name of the slot -* @return {number} the slot (-1 if not found) -*/ -LGraphNode.prototype.findInputSlot = function(name) -{ - if(!this.inputs) - return -1; - for(var i = 0, l = this.inputs.length; i < l; ++i) - if(name == this.inputs[i].name) - return i; - return -1; -} - -/** -* returns the output slot with a given name (used for dynamic slots), -1 if not found -* @method findOutputSlot -* @param {string} name the name of the slot -* @return {number} the slot (-1 if not found) -*/ -LGraphNode.prototype.findOutputSlot = function(name) -{ - if(!this.outputs) return -1; - for(var i = 0, l = this.outputs.length; i < l; ++i) - if(name == this.outputs[i].name) - return i; - return -1; -} - -/** -* connect this node output to the input of another node -* @method connect -* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) -* @param {LGraphNode} node the target node -* @param {number_or_string} target_slot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger) -* @return {boolean} if it was connected succesfully -*/ -LGraphNode.prototype.connect = function( slot, target_node, target_slot ) -{ - target_slot = target_slot || 0; - - //seek for the output slot - if( slot.constructor === String ) - { - slot = this.findOutputSlot(slot); - if(slot == -1) - { - if(LiteGraph.debug) - console.log("Connect: Error, no slot of name " + slot); - return false; - } - } - else if(!this.outputs || slot >= this.outputs.length) - { - if(LiteGraph.debug) - console.log("Connect: Error, slot number not found"); - return false; - } - - if(target_node && target_node.constructor === Number) - target_node = this.graph.getNodeById( target_node ); - if(!target_node) - throw("Node not found"); - - //avoid loopback - if(target_node == this) - return false; - - //you can specify the slot by name - if(target_slot.constructor === String) - { - target_slot = target_node.findInputSlot( target_slot ); - if(target_slot == -1) - { - if(LiteGraph.debug) - console.log("Connect: Error, no slot of name " + target_slot); - return false; - } - } - else if( target_slot === LiteGraph.EVENT ) - { - //search for first slot with event? - /* - //create input for trigger - var input = target_node.addInput("onTrigger", LiteGraph.EVENT ); - target_slot = target_node.inputs.length - 1; //last one is the one created - target_node.mode = LiteGraph.ON_TRIGGER; - */ - return false; - } - else if( !target_node.inputs || target_slot >= target_node.inputs.length ) - { - if(LiteGraph.debug) - console.log("Connect: Error, slot number not found"); - return false; - } - - //if there is something already plugged there, disconnect - if(target_node.inputs[ target_slot ].link != null ) - target_node.disconnectInput( target_slot ); - - //why here?? - this.setDirtyCanvas(false,true); - this.graph.connectionChange( this ); - - var output = this.outputs[slot]; - - //allows nodes to block connection - if(target_node.onConnectInput) - if( target_node.onConnectInput( target_slot, output.type, output ) === false) - return false; - - var input = target_node.inputs[target_slot]; - - if( LiteGraph.isValidConnection( output.type, input.type ) ) - { - var link_info = { - id: this.graph.last_link_id++, - type: input.type, - origin_id: this.id, - origin_slot: slot, - target_id: target_node.id, - target_slot: target_slot - }; - - //add to graph links list - this.graph.links[ link_info.id ] = link_info; - - //connect in output - if( output.links == null ) - output.links = []; - output.links.push( link_info.id ); - //connect in input - target_node.inputs[target_slot].link = link_info.id; - if(this.graph) - this.graph._version++; - if(this.onConnectionsChange) - this.onConnectionsChange( LiteGraph.OUTPUT, slot, true, link_info, output ); //link_info has been created now, so its updated - if(target_node.onConnectionsChange) - target_node.onConnectionsChange( LiteGraph.INPUT, target_slot, true, link_info, input ); - } - - this.setDirtyCanvas(false,true); - this.graph.connectionChange( this ); - - return true; -} - -/** -* disconnect one output to an specific node -* @method disconnectOutput -* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) -* @param {LGraphNode} target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected] -* @return {boolean} if it was disconnected succesfully -*/ -LGraphNode.prototype.disconnectOutput = function( slot, target_node ) -{ - if( slot.constructor === String ) - { - slot = this.findOutputSlot(slot); - if(slot == -1) - { - if(LiteGraph.debug) - console.log("Connect: Error, no slot of name " + slot); - return false; - } - } - else if(!this.outputs || slot >= this.outputs.length) - { - if(LiteGraph.debug) - console.log("Connect: Error, slot number not found"); - return false; - } - - //get output slot - var output = this.outputs[slot]; - if(!output.links || output.links.length == 0) - return false; - - //one of the links - if(target_node) - { - if(target_node.constructor === Number) - target_node = this.graph.getNodeById( target_node ); - if(!target_node) - throw("Target Node not found"); - - for(var i = 0, l = output.links.length; i < l; i++) - { - var link_id = output.links[i]; - var link_info = this.graph.links[ link_id ]; - - //is the link we are searching for... - if( link_info.target_id == target_node.id ) - { - output.links.splice(i,1); //remove here - var input = target_node.inputs[ link_info.target_slot ]; - input.link = null; //remove there - delete this.graph.links[ link_id ]; //remove the link from the links pool - if(this.graph) - this.graph._version++; - if(target_node.onConnectionsChange) - target_node.onConnectionsChange( LiteGraph.INPUT, link_info.target_slot, false, link_info, input ); //link_info hasnt been modified so its ok - if(this.onConnectionsChange) - this.onConnectionsChange( LiteGraph.OUTPUT, slot, false, link_info, output ); - break; - } - } - } - else //all the links - { - for(var i = 0, l = output.links.length; i < l; i++) - { - var link_id = output.links[i]; - var link_info = this.graph.links[ link_id ]; - if(!link_info) //bug: it happens sometimes - continue; - - var target_node = this.graph.getNodeById( link_info.target_id ); - var input = null; - if(this.graph) - this.graph._version++; - if(target_node) - { - input = target_node.inputs[ link_info.target_slot ]; - input.link = null; //remove other side link - if(target_node.onConnectionsChange) - target_node.onConnectionsChange( LiteGraph.INPUT, link_info.target_slot, false, link_info, input ); //link_info hasnt been modified so its ok - } - delete this.graph.links[ link_id ]; //remove the link from the links pool - if(this.onConnectionsChange) - this.onConnectionsChange( LiteGraph.OUTPUT, slot, false, link_info, output ); - } - output.links = null; - } - - - this.setDirtyCanvas(false,true); - this.graph.connectionChange( this ); - return true; -} - -/** -* disconnect one input -* @method disconnectInput -* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) -* @return {boolean} if it was disconnected succesfully -*/ -LGraphNode.prototype.disconnectInput = function( slot ) -{ - //seek for the output slot - if( slot.constructor === String ) - { - slot = this.findInputSlot(slot); - if(slot == -1) - { - if(LiteGraph.debug) - console.log("Connect: Error, no slot of name " + slot); - return false; - } - } - else if(!this.inputs || slot >= this.inputs.length) - { - if(LiteGraph.debug) - console.log("Connect: Error, slot number not found"); - return false; - } - - var input = this.inputs[slot]; - if(!input) - return false; - - var link_id = this.inputs[slot].link; - this.inputs[slot].link = null; - - //remove other side - var link_info = this.graph.links[ link_id ]; - if( link_info ) - { - var target_node = this.graph.getNodeById( link_info.origin_id ); - if(!target_node) - return false; - - var output = target_node.outputs[ link_info.origin_slot ]; - if(!output || !output.links || output.links.length == 0) - return false; - - //search in the inputs list for this link - for(var i = 0, l = output.links.length; i < l; i++) - { - if( output.links[i] == link_id ) - { - output.links.splice(i,1); - break; - } - } - - delete this.graph.links[ link_id ]; //remove from the pool - if(this.graph) - this.graph._version++; - if( this.onConnectionsChange ) - this.onConnectionsChange( LiteGraph.INPUT, slot, false, link_info, input ); - if( target_node.onConnectionsChange ) - target_node.onConnectionsChange( LiteGraph.OUTPUT, i, false, link_info, output ); - } - - this.setDirtyCanvas(false,true); - this.graph.connectionChange( this ); - return true; -} - -/** -* returns the center of a connection point in canvas coords -* @method getConnectionPos -* @param {boolean} is_input true if if a input slot, false if it is an output -* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) -* @return {[x,y]} the position -**/ -LGraphNode.prototype.getConnectionPos = function( is_input, slot_number ) -{ - if(this.flags.collapsed) - { - if(is_input) - return [this.pos[0], this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5]; - else - return [this.pos[0] + (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH), this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5]; - } - - if(is_input && slot_number == -1) - { - return [this.pos[0] + 10, this.pos[1] + 10]; - } - - if(is_input && this.inputs.length > slot_number && this.inputs[slot_number].pos) - return [this.pos[0] + this.inputs[slot_number].pos[0],this.pos[1] + this.inputs[slot_number].pos[1]]; - else if(!is_input && this.outputs.length > slot_number && this.outputs[slot_number].pos) - return [this.pos[0] + this.outputs[slot_number].pos[0],this.pos[1] + this.outputs[slot_number].pos[1]]; - - if(!is_input) //output - return [this.pos[0] + this.size[0] + 1, this.pos[1] + 10 + slot_number * LiteGraph.NODE_SLOT_HEIGHT + (this.constructor.slot_start_y || 0)]; - return [this.pos[0] , this.pos[1] + 10 + slot_number * LiteGraph.NODE_SLOT_HEIGHT + (this.constructor.slot_start_y || 0) ]; -} - -/* Force align to grid */ -LGraphNode.prototype.alignToGrid = function() -{ - this.pos[0] = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.pos[0] / LiteGraph.CANVAS_GRID_SIZE); - this.pos[1] = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.pos[1] / LiteGraph.CANVAS_GRID_SIZE); -} - - -/* Console output */ -LGraphNode.prototype.trace = function(msg) -{ - if(!this.console) - this.console = []; - this.console.push(msg); - if(this.console.length > LGraphNode.MAX_CONSOLE) - this.console.shift(); - - this.graph.onNodeTrace(this,msg); -} - -/* Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */ -LGraphNode.prototype.setDirtyCanvas = function(dirty_foreground, dirty_background) -{ - if(!this.graph) - return; - this.graph.sendActionToCanvas("setDirty",[dirty_foreground, dirty_background]); -} - -LGraphNode.prototype.loadImage = function(url) -{ - var img = new Image(); - img.src = LiteGraph.node_images_path + url; - img.ready = false; - - var that = this; - img.onload = function() { - this.ready = true; - that.setDirtyCanvas(true); - } - return img; -} - -//safe LGraphNode action execution (not sure if safe) -/* -LGraphNode.prototype.executeAction = function(action) -{ - if(action == "") return false; - - if( action.indexOf(";") != -1 || action.indexOf("}") != -1) - { - this.trace("Error: Action contains unsafe characters"); - return false; - } - - var tokens = action.split("("); - var func_name = tokens[0]; - if( typeof(this[func_name]) != "function") - { - this.trace("Error: Action not found on node: " + func_name); - return false; - } - - var code = action; - - try - { - var _foo = eval; - eval = null; - (new Function("with(this) { " + code + "}")).call(this); - eval = _foo; - } - catch (err) - { - this.trace("Error executing action {" + action + "} :" + err); - return false; - } - - return true; -} -*/ - -/* Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */ -LGraphNode.prototype.captureInput = function(v) -{ - if(!this.graph || !this.graph.list_of_graphcanvas) - return; - - var list = this.graph.list_of_graphcanvas; - - for(var i = 0; i < list.length; ++i) - { - var c = list[i]; - //releasing somebody elses capture?! - if(!v && c.node_capturing_input != this) - continue; - - //change - c.node_capturing_input = v ? this : null; - } -} - -/** -* Collapse the node to make it smaller on the canvas -* @method collapse -**/ -LGraphNode.prototype.collapse = function( force ) -{ - this.graph._version++; - if(this.constructor.collapsable === false && !force) - return; - if(!this.flags.collapsed) - this.flags.collapsed = true; - else - this.flags.collapsed = false; - this.setDirtyCanvas(true,true); -} - -/** -* Forces the node to do not move or realign on Z -* @method pin -**/ - -LGraphNode.prototype.pin = function(v) -{ - this.graph._version++; - if(v === undefined) - this.flags.pinned = !this.flags.pinned; - else - this.flags.pinned = v; -} - -LGraphNode.prototype.localToScreen = function(x,y, graphcanvas) -{ - return [(x + this.pos[0]) * graphcanvas.scale + graphcanvas.offset[0], - (y + this.pos[1]) * graphcanvas.scale + graphcanvas.offset[1]]; -} - - - - -function LGraphGroup( title ) -{ - this._ctor( title ); -} - -global.LGraphGroup = LiteGraph.LGraphGroup = LGraphGroup; - -LGraphGroup.prototype._ctor = function( title ) -{ - this.title = title || "Group"; - this._bounding = new Float32Array([10,10,140,80]); - this._pos = this._bounding.subarray(0,2); - this._size = this._bounding.subarray(2,4); - this._nodes = []; - this.color = LGraphCanvas.node_colors.pale_blue ? LGraphCanvas.node_colors.pale_blue.groupcolor : "#AAA"; - this.graph = null; - - Object.defineProperty( this, "pos", { - set: function(v) - { - if(!v || v.length < 2) - return; - this._pos[0] = v[0]; - this._pos[1] = v[1]; - }, - get: function() - { - return this._pos; - }, - enumerable: true - }); - - Object.defineProperty( this, "size", { - set: function(v) - { - if(!v || v.length < 2) - return; - this._size[0] = Math.max(140,v[0]); - this._size[1] = Math.max(80,v[1]); - }, - get: function() - { - return this._size; - }, - enumerable: true - }); -} - -LGraphGroup.prototype.configure = function(o) -{ - this.title = o.title; - this._bounding.set( o.bounding ); - this.color = o.color; -} - -LGraphGroup.prototype.serialize = function() -{ - var b = this._bounding; - return { - title: this.title, - bounding: [ b[0], b[1], b[2], b[3] ], - color: this.color - }; -} - -LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) -{ - this._pos[0] += deltax; - this._pos[1] += deltay; - if(ignore_nodes) - return; - for(var i = 0; i < this._nodes.length; ++i) - { - var node = this._nodes[i]; - node.pos[0] += deltax; - node.pos[1] += deltay; - } -} - -LGraphGroup.prototype.recomputeInsideNodes = function() -{ - this._nodes.length = 0; - var nodes = this.graph._nodes; - var node_bounding = new Float32Array(4); - - for(var i = 0; i < nodes.length; ++i) - { - var node = nodes[i]; - node.getBounding( node_bounding ); - if(!overlapBounding( this._bounding, node_bounding )) - continue; //out of the visible area - this._nodes.push( node ); - } -} - -LGraphGroup.prototype.isPointInside = LGraphNode.prototype.isPointInside; -LGraphGroup.prototype.setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas; - -//********************************************************************************* -// LGraphCanvas: LGraph renderer CLASS -//********************************************************************************* - -/** -* The Global Scope. It contains all the registered node classes. -* Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked -* -* @class LGraphCanvas -* @constructor -* @param {HTMLCanvas} canvas the canvas where you want to render (it accepts a selector in string format or the canvas element itself) -* @param {LGraph} graph [optional] -* @param {Object} options [optional] { skip_rendering, autoresize } -*/ -function LGraphCanvas( canvas, graph, options ) -{ - options = options || {}; - - //if(graph === undefined) - // throw ("No graph assigned"); - this.background_image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=' - - if(canvas && canvas.constructor === String ) - canvas = document.querySelector( canvas ); - - this.max_zoom = 10; - this.min_zoom = 0.1; - this.zoom_modify_alpha = true; //otherwise it generates ugly patterns when scaling down too much - - this.title_text_font = "bold "+LiteGraph.NODE_TEXT_SIZE+"px Arial"; - this.inner_text_font = "normal "+LiteGraph.NODE_SUBTEXT_SIZE+"px Arial"; - this.node_title_color = LiteGraph.NODE_TITLE_COLOR; - this.default_link_color = "#AAC"; - this.default_connection_color = { - input_off: "#AAB", - input_on: "#7F7", - output_off: "#AAB", - output_on: "#7F7" - }; - - this.highquality_render = true; - this.use_gradients = false; //set to true to render titlebar with gradients - this.editor_alpha = 1; //used for transition - this.pause_rendering = false; - this.render_shadows = true; - this.clear_background = true; - - this.render_only_selected = true; - this.live_mode = false; - this.show_info = true; - this.allow_dragcanvas = true; - this.allow_dragnodes = true; - this.allow_interaction = true; //allow to control widgets, buttons, collapse, etc - this.allow_searchbox = true; - this.drag_mode = false; - this.dragging_rectangle = null; - - this.filter = null; //allows to filter to only accept some type of nodes in a graph - - this.always_render_background = false; - this.render_canvas_border = true; - this.render_connections_shadows = false; //too much cpu - this.render_connections_border = true; - this.render_curved_connections = true; - this.render_connection_arrows = true; - - this.canvas_mouse = [0,0]; //mouse in canvas graph coordinates, where 0,0 is the top-left corner of the blue rectangle - - //to personalize the search box - this.onSearchBox = null; - this.onSearchBoxSelection = null; - - this.connections_width = 3; - this.round_radius = 8; - - this.current_node = null; - this.node_widget = null; //used for widgets - this.last_mouse_position = [0,0]; - - //link canvas and graph - if(graph) - graph.attachCanvas(this); - - this.setCanvas( canvas ); - this.clear(); - - if(!options.skip_render) - this.startRendering(); - - this.autoresize = options.autoresize; -} - -global.LGraphCanvas = LiteGraph.LGraphCanvas = LGraphCanvas; - -LGraphCanvas.link_type_colors = {"-1":"#F85",'number':"#AAC","node":"#DCA"}; -LGraphCanvas.gradients = {}; //cache of gradients - -/** -* clears all the data inside -* -* @method clear -*/ -LGraphCanvas.prototype.clear = function() -{ - this.frame = 0; - this.last_draw_time = 0; - this.render_time = 0; - this.fps = 0; - - this.scale = 1; - this.offset = [0,0]; - - this.dragging_rectangle = null; - - this.selected_nodes = {}; - this.selected_group = null; - - this.visible_nodes = []; - this.node_dragged = null; - this.node_over = null; - this.node_capturing_input = null; - this.connecting_node = null; - this.highlighted_links = {}; - - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - this.dirty_area = null; - - this.node_in_panel = null; - this.node_widget = null; - - this.last_mouse = [0,0]; - this.last_mouseclick = 0; - - if(this.onClear) - this.onClear(); - //this.UIinit(); -} - -/** -* assigns a graph, you can reasign graphs to the same canvas -* -* @method setGraph -* @param {LGraph} graph -*/ -LGraphCanvas.prototype.setGraph = function( graph, skip_clear ) -{ - if(this.graph == graph) - return; - - if(!skip_clear) - this.clear(); - - if(!graph && this.graph) - { - this.graph.detachCanvas(this); - return; - } - - /* - if(this.graph) - this.graph.canvas = null; //remove old graph link to the canvas - this.graph = graph; - if(this.graph) - this.graph.canvas = this; - */ - graph.attachCanvas(this); - this.setDirty(true,true); -} - -/** -* opens a graph contained inside a node in the current graph -* -* @method openSubgraph -* @param {LGraph} graph -*/ -LGraphCanvas.prototype.openSubgraph = function(graph) -{ - if(!graph) - throw("graph cannot be null"); - - if(this.graph == graph) - throw("graph cannot be the same"); - - this.clear(); - - if(this.graph) - { - if(!this._graph_stack) - this._graph_stack = []; - this._graph_stack.push(this.graph); - } - - graph.attachCanvas(this); - this.setDirty(true,true); -} - -/** -* closes a subgraph contained inside a node -* -* @method closeSubgraph -* @param {LGraph} assigns a graph -*/ -LGraphCanvas.prototype.closeSubgraph = function() -{ - if(!this._graph_stack || this._graph_stack.length == 0) - return; - var graph = this._graph_stack.pop(); - this.selected_nodes = {}; - this.highlighted_links = {}; - graph.attachCanvas(this); - this.setDirty(true,true); -} - -/** -* assigns a canvas -* -* @method setCanvas -* @param {Canvas} assigns a canvas (also accepts the ID of the element (not a selector) -*/ -LGraphCanvas.prototype.setCanvas = function( canvas, skip_events ) -{ - var that = this; - - if(canvas) - { - if( canvas.constructor === String ) - { - canvas = document.getElementById(canvas); - if(!canvas) - throw("Error creating LiteGraph canvas: Canvas not found"); - } - } - - if(canvas === this.canvas) - return; - - if(!canvas && this.canvas) - { - //maybe detach events from old_canvas - if(!skip_events) - this.unbindEvents(); - } - - this.canvas = canvas; - - if(!canvas) - return; - - //this.canvas.tabindex = "1000"; - canvas.className += " lgraphcanvas"; - canvas.data = this; - - //bg canvas: used for non changing stuff - this.bgcanvas = null; - if(!this.bgcanvas) - { - this.bgcanvas = document.createElement("canvas"); - this.bgcanvas.width = this.canvas.width; - this.bgcanvas.height = this.canvas.height; - } - - if(canvas.getContext == null) - { - if( canvas.localName != "canvas" ) - throw("Element supplied for LGraphCanvas must be a element, you passed a " + canvas.localName ); - throw("This browser doesnt support Canvas"); - } - - var ctx = this.ctx = canvas.getContext("2d"); - if(ctx == null) - { - if(!canvas.webgl_enabled) - console.warn("This canvas seems to be WebGL, enabling WebGL renderer"); - this.enableWebGL(); - } - - //input: (move and up could be unbinded) - this._mousemove_callback = this.processMouseMove.bind(this); - this._mouseup_callback = this.processMouseUp.bind(this); - - if(!skip_events) - this.bindEvents(); -} - -//used in some events to capture them -LGraphCanvas.prototype._doNothing = function doNothing(e) { e.preventDefault(); return false; }; -LGraphCanvas.prototype._doReturnTrue = function doNothing(e) { e.preventDefault(); return true; }; - -LGraphCanvas.prototype.bindEvents = function() -{ - if( this._events_binded ) - { - console.warn("LGraphCanvas: events already binded"); - return; - } - - var canvas = this.canvas; - var ref_window = this.getCanvasWindow(); - var document = ref_window.document; //hack used when moving canvas between windows - - this._mousedown_callback = this.processMouseDown.bind(this); - this._mousewheel_callback = this.processMouseWheel.bind(this); - - canvas.addEventListener("mousedown", this._mousedown_callback, true ); //down do not need to store the binded - canvas.addEventListener("mousemove", this._mousemove_callback ); - canvas.addEventListener("mousewheel", this._mousewheel_callback, false); - - canvas.addEventListener("contextmenu", this._doNothing ); - canvas.addEventListener("DOMMouseScroll", this._mousewheel_callback, false); - - //touch events - //if( 'touchstart' in document.documentElement ) - { - canvas.addEventListener("touchstart", this.touchHandler, true); - canvas.addEventListener("touchmove", this.touchHandler, true); - canvas.addEventListener("touchend", this.touchHandler, true); - canvas.addEventListener("touchcancel", this.touchHandler, true); - } - - //Keyboard ****************** - this._key_callback = this.processKey.bind(this); - - canvas.addEventListener("keydown", this._key_callback, true ); - document.addEventListener("keyup", this._key_callback, true ); //in document, otherwise it doesnt fire keyup - - //Droping Stuff over nodes ************************************ - this._ondrop_callback = this.processDrop.bind(this); - - canvas.addEventListener("dragover", this._doNothing, false ); - canvas.addEventListener("dragend", this._doNothing, false ); - canvas.addEventListener("drop", this._ondrop_callback, false ); - canvas.addEventListener("dragenter", this._doReturnTrue, false ); - - this._events_binded = true; -} - -LGraphCanvas.prototype.unbindEvents = function() -{ - if( !this._events_binded ) - { - console.warn("LGraphCanvas: no events binded"); - return; - } - - var ref_window = this.getCanvasWindow(); - var document = ref_window.document; - - this.canvas.removeEventListener( "mousedown", this._mousedown_callback ); - this.canvas.removeEventListener( "mousewheel", this._mousewheel_callback ); - this.canvas.removeEventListener( "DOMMouseScroll", this._mousewheel_callback ); - this.canvas.removeEventListener( "keydown", this._key_callback ); - document.removeEventListener( "keyup", this._key_callback ); - this.canvas.removeEventListener( "contextmenu", this._doNothing ); - this.canvas.removeEventListener( "drop", this._ondrop_callback ); - this.canvas.removeEventListener( "dragenter", this._doReturnTrue ); - - this.canvas.removeEventListener("touchstart", this.touchHandler ); - this.canvas.removeEventListener("touchmove", this.touchHandler ); - this.canvas.removeEventListener("touchend", this.touchHandler ); - this.canvas.removeEventListener("touchcancel", this.touchHandler ); - - this._mousedown_callback = null; - this._mousewheel_callback = null; - this._key_callback = null; - this._ondrop_callback = null; - - this._events_binded = false; -} - -LGraphCanvas.getFileExtension = function (url) -{ - var question = url.indexOf("?"); - if(question != -1) - url = url.substr(0,question); - var point = url.lastIndexOf("."); - if(point == -1) - return ""; - return url.substr(point+1).toLowerCase(); -} - -//this file allows to render the canvas using WebGL instead of Canvas2D -//this is useful if you plant to render 3D objects inside your nodes -LGraphCanvas.prototype.enableWebGL = function() -{ - if(typeof(GL) === undefined) - throw("litegl.js must be included to use a WebGL canvas"); - if(typeof(enableWebGLCanvas) === undefined) - throw("webglCanvas.js must be included to use this feature"); - - this.gl = this.ctx = enableWebGLCanvas(this.canvas); - this.ctx.webgl = true; - this.bgcanvas = this.canvas; - this.bgctx = this.gl; - this.canvas.webgl_enabled = true; - - /* - GL.create({ canvas: this.bgcanvas }); - this.bgctx = enableWebGLCanvas( this.bgcanvas ); - window.gl = this.gl; - */ -} - - -/* -LGraphCanvas.prototype.UIinit = function() -{ - var that = this; - $("#node-console input").change(function(e) - { - if(e.target.value == "") - return; - - var node = that.node_in_panel; - if(!node) - return; - - node.trace("] " + e.target.value, "#333"); - if(node.onConsoleCommand) - { - if(!node.onConsoleCommand(e.target.value)) - node.trace("command not found", "#A33"); - } - else if (e.target.value == "info") - { - node.trace("Special methods:"); - for(var i in node) - { - if(typeof(node[i]) == "function" && LGraphNode.prototype[i] == null && i.substr(0,2) != "on" && i[0] != "_") - node.trace(" + " + i); - } - } - else - { - try - { - eval("var _foo = function() { return ("+e.target.value+"); }"); - var result = _foo.call(node); - if(result) - node.trace(result.toString()); - delete window._foo; - } - catch(err) - { - node.trace("error: " + err, "#A33"); - } - } - - this.value = ""; - }); -} -*/ - -/** -* marks as dirty the canvas, this way it will be rendered again -* -* @class LGraphCanvas -* @method setDirty -* @param {bool} fgcanvas if the foreground canvas is dirty (the one containing the nodes) -* @param {bool} bgcanvas if the background canvas is dirty (the one containing the wires) -*/ -LGraphCanvas.prototype.setDirty = function( fgcanvas, bgcanvas ) -{ - if(fgcanvas) - this.dirty_canvas = true; - if(bgcanvas) - this.dirty_bgcanvas = true; -} - -/** -* Used to attach the canvas in a popup -* -* @method getCanvasWindow -* @return {window} returns the window where the canvas is attached (the DOM root node) -*/ -LGraphCanvas.prototype.getCanvasWindow = function() -{ - if(!this.canvas) - return window; - var doc = this.canvas.ownerDocument; - return doc.defaultView || doc.parentWindow; -} - -/** -* starts rendering the content of the canvas when needed -* -* @method startRendering -*/ -LGraphCanvas.prototype.startRendering = function() -{ - if(this.is_rendering) - return; //already rendering - - this.is_rendering = true; - renderFrame.call(this); - - function renderFrame() - { - if(!this.pause_rendering) - this.draw(); - - var window = this.getCanvasWindow(); - if(this.is_rendering) - window.requestAnimationFrame( renderFrame.bind(this) ); - } -} - -/** -* stops rendering the content of the canvas (to save resources) -* -* @method stopRendering -*/ -LGraphCanvas.prototype.stopRendering = function() -{ - this.is_rendering = false; - /* - if(this.rendering_timer_id) - { - clearInterval(this.rendering_timer_id); - this.rendering_timer_id = null; - } - */ -} - -/* LiteGraphCanvas input */ - -LGraphCanvas.prototype.processMouseDown = function(e) -{ - if(!this.graph) - return; - - this.adjustMouseEvent(e); - - var ref_window = this.getCanvasWindow(); - var document = ref_window.document; - LGraphCanvas.active_canvas = this; - var that = this; - - //move mouse move event to the window in case it drags outside of the canvas - this.canvas.removeEventListener("mousemove", this._mousemove_callback ); - ref_window.document.addEventListener("mousemove", this._mousemove_callback, true ); //catch for the entire window - ref_window.document.addEventListener("mouseup", this._mouseup_callback, true ); - - var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes ); - var skip_dragging = false; - var skip_action = false; - var now = LiteGraph.getTime(); - - this.canvas_mouse[0] = e.canvasX; - this.canvas_mouse[1] = e.canvasY; - - LiteGraph.closeAllContextMenus( ref_window ); - - if(e.which == 1) //left button mouse - { - if( e.ctrlKey ) - { - this.dragging_rectangle = new Float32Array(4); - this.dragging_rectangle[0] = e.canvasX; - this.dragging_rectangle[1] = e.canvasY; - this.dragging_rectangle[2] = 1; - this.dragging_rectangle[3] = 1; - skip_action = true; - } - - var clicking_canvas_bg = false; - - //when clicked on top of a node - //and it is not interactive - if( node && this.allow_interaction && !skip_action ) - { - if( !this.live_mode && !node.flags.pinned ) - this.bringToFront( node ); //if it wasnt selected? - - //not dragging mouse to connect two slots - if(!this.connecting_node && !node.flags.collapsed && !this.live_mode) - { - //search for outputs - if(node.outputs) - for(var i = 0, l = node.outputs.length; i < l; ++i) - { - var output = node.outputs[i]; - var link_pos = node.getConnectionPos(false,i); - if( isInsideRectangle( e.canvasX, e.canvasY, link_pos[0] - 10, link_pos[1] - 5, 20,10) ) - { - this.connecting_node = node; - this.connecting_output = output; - this.connecting_pos = node.getConnectionPos(false,i); - this.connecting_slot = i; - - skip_action = true; - break; - } - } - - //search for inputs - if(node.inputs) - for(var i = 0, l = node.inputs.length; i < l; ++i) - { - var input = node.inputs[i]; - var link_pos = node.getConnectionPos( true, i ); - if( isInsideRectangle(e.canvasX, e.canvasY, link_pos[0] - 10, link_pos[1] - 5, 20,10) ) - { - if(input.link !== null) - { - node.disconnectInput(i); - this.dirty_bgcanvas = true; - skip_action = true; - } - } - } - - //Search for corner - if( !skip_action && node.flags.resizable !== false && isInsideRectangle(e.canvasX, e.canvasY, node.pos[0] + node.size[0] - 5, node.pos[1] + node.size[1] - 5 ,5,5 )) - { - this.resizing_node = node; - this.canvas.style.cursor = "se-resize"; - skip_action = true; - } - } - - //Search for corner - if( !skip_action && isInsideRectangle(e.canvasX, e.canvasY, node.pos[0], node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT, LiteGraph.NODE_TITLE_HEIGHT, LiteGraph.NODE_TITLE_HEIGHT )) - { - node.collapse(); - skip_action = true; - } - - //it wasnt clicked on the links boxes - if(!skip_action) - { - var block_drag_node = false; - - //widgets - var widget = this.processNodeWidgets( node, this.canvas_mouse, e ); - if(widget) - { - block_drag_node = true; - this.node_widget = [node, widget]; - } - - //double clicking - if ((now - this.last_mouseclick) < 300 && this.selected_nodes[ node.id ]) - { - //double click node - if( node.onDblClick) - node.onDblClick(e); - this.processNodeDblClicked( node ); - block_drag_node = true; - } - - //if do not capture mouse - if( node.onMouseDown && node.onMouseDown( e, [e.canvasX - node.pos[0], e.canvasY - node.pos[1]] ) ) - block_drag_node = true; - else if(this.live_mode) - { - clicking_canvas_bg = true; - block_drag_node = true; - } - - if(!block_drag_node) - { - if(this.allow_dragnodes) - this.node_dragged = node; - if(!this.selected_nodes[ node.id ]) - this.processNodeSelected( node, e ); - } - - this.dirty_canvas = true; - } - } - else //clicked outside of nodes - { - this.selected_group = this.graph.getGroupOnPos( e.canvasX, e.canvasY ); - this.selected_group_resizing = false; - if( this.selected_group ) - { - if( e.ctrlKey ) - this.dragging_rectangle = null; - - var dist = distance( [e.canvasX, e.canvasY], [ this.selected_group.pos[0] + this.selected_group.size[0], this.selected_group.pos[1] + this.selected_group.size[1] ] ); - if( (dist * this.scale) < 10 ) - this.selected_group_resizing = true; - else - this.selected_group.recomputeInsideNodes(); - } - clicking_canvas_bg = true; - } - - if( !skip_action && clicking_canvas_bg && this.allow_dragcanvas ) - { - this.dragging_canvas = true; - } - } - else if (e.which == 2) //middle button - { - - } - else if (e.which == 3) //right button - { - this.processContextMenu( node, e ); - } - - //TODO - //if(this.node_selected != prev_selected) - // this.onNodeSelectionChange(this.node_selected); - - this.last_mouse[0] = e.localX; - this.last_mouse[1] = e.localY; - this.last_mouseclick = LiteGraph.getTime(); - - /* - if( (this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null) - this.draw(); - */ - - this.graph.change(); - - //this is to ensure to defocus(blur) if a text input element is on focus - if(!ref_window.document.activeElement || (ref_window.document.activeElement.nodeName.toLowerCase() != "input" && ref_window.document.activeElement.nodeName.toLowerCase() != "textarea")) - e.preventDefault(); - e.stopPropagation(); - - if(this.onMouseDown) - this.onMouseDown(e); - - return false; -} - -LGraphCanvas.prototype.processMouseMove = function(e) -{ - if(this.autoresize) - this.resize(); - - if(!this.graph) - return; - - LGraphCanvas.active_canvas = this; - this.adjustMouseEvent(e); - var mouse = [e.localX, e.localY]; - var delta = [mouse[0] - this.last_mouse[0], mouse[1] - this.last_mouse[1]]; - this.last_mouse = mouse; - this.canvas_mouse[0] = e.canvasX; - this.canvas_mouse[1] = e.canvasY; - - if( this.node_widget ) - { - this.processNodeWidgets( this.node_widget[0], this.canvas_mouse, e, this.node_widget[1] ); - this.dirty_canvas = true; - } - - if( this.dragging_rectangle ) - { - this.dragging_rectangle[2] = e.canvasX - this.dragging_rectangle[0]; - this.dragging_rectangle[3] = e.canvasY - this.dragging_rectangle[1]; - this.dirty_canvas = true; - } - else if (this.selected_group) - { - if( this.selected_group_resizing ) - this.selected_group.size = [ e.canvasX - this.selected_group.pos[0], e.canvasY - this.selected_group.pos[1] ]; - else - { - var deltax = delta[0] / this.scale; - var deltay = delta[1] / this.scale; - this.selected_group.move( deltax, deltay, e.ctrlKey ); - if( this.selected_group._nodes.length) - this.dirty_canvas = true; - } - this.dirty_bgcanvas = true; - } - else if(this.dragging_canvas) - { - this.offset[0] += delta[0] / this.scale; - this.offset[1] += delta[1] / this.scale; - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - } - else if(this.allow_interaction) - { - if(this.connecting_node) - this.dirty_canvas = true; - - //get node over - var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes ); - - //remove mouseover flag - for(var i = 0, l = this.graph._nodes.length; i < l; ++i) - { - if(this.graph._nodes[i].mouseOver && node != this.graph._nodes[i]) - { - //mouse leave - this.graph._nodes[i].mouseOver = false; - if(this.node_over && this.node_over.onMouseLeave) - this.node_over.onMouseLeave(e); - this.node_over = null; - this.dirty_canvas = true; - } - } - - //mouse over a node - if(node) - { - //this.canvas.style.cursor = "move"; - if(!node.mouseOver) - { - //mouse enter - node.mouseOver = true; - this.node_over = node; - this.dirty_canvas = true; - - if(node.onMouseEnter) node.onMouseEnter(e); - } - - //in case the node wants to do something - if(node.onMouseMove) - node.onMouseMove(e); - - //if dragging a link - if(this.connecting_node) - { - var pos = this._highlight_input || [0,0]; //to store the output of isOverNodeInput - - //on top of input - if( this.isOverNodeBox( node, e.canvasX, e.canvasY ) ) - { - //mouse on top of the corner box, dont know what to do - } - else - { - //check if I have a slot below de mouse - var slot = this.isOverNodeInput( node, e.canvasX, e.canvasY, pos ); - if(slot != -1 && node.inputs[slot] ) - { - var slot_type = node.inputs[slot].type; - if( LiteGraph.isValidConnection( this.connecting_output.type, slot_type ) ) - this._highlight_input = pos; - } - else - this._highlight_input = null; - } - } - - //Search for corner - if(this.canvas) - { - if( isInsideRectangle(e.canvasX, e.canvasY, node.pos[0] + node.size[0] - 5, node.pos[1] + node.size[1] - 5 ,5,5 )) - this.canvas.style.cursor = "se-resize"; - else - this.canvas.style.cursor = null; - } - } - else if(this.canvas) - this.canvas.style.cursor = null; - - if(this.node_capturing_input && this.node_capturing_input != node && this.node_capturing_input.onMouseMove) - { - this.node_capturing_input.onMouseMove(e); - } - - - if(this.node_dragged && !this.live_mode) - { - /* - this.node_dragged.pos[0] += delta[0] / this.scale; - this.node_dragged.pos[1] += delta[1] / this.scale; - this.node_dragged.pos[0] = Math.round(this.node_dragged.pos[0]); - this.node_dragged.pos[1] = Math.round(this.node_dragged.pos[1]); - */ - - for(var i in this.selected_nodes) - { - var n = this.selected_nodes[i]; - - n.pos[0] += delta[0] / this.scale; - n.pos[1] += delta[1] / this.scale; - //n.pos[0] = Math.round(n.pos[0]); - //n.pos[1] = Math.round(n.pos[1]); - } - - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - } - - if(this.resizing_node && !this.live_mode) - { - //convert mouse to node space - this.resizing_node.size[0] = e.canvasX - this.resizing_node.pos[0]; - this.resizing_node.size[1] = e.canvasY - this.resizing_node.pos[1]; - - //constraint size - var max_slots = Math.max( this.resizing_node.inputs ? this.resizing_node.inputs.length : 0, this.resizing_node.outputs ? this.resizing_node.outputs.length : 0); - var min_height = max_slots * LiteGraph.NODE_SLOT_HEIGHT + ( this.resizing_node.widgets ? this.resizing_node.widgets.length : 0 ) * (LiteGraph.NODE_WIDGET_HEIGHT + 4 ) + 4; - if(this.resizing_node.size[1] < min_height ) - this.resizing_node.size[1] = min_height; - if(this.resizing_node.size[0] < LiteGraph.NODE_MIN_WIDTH) - this.resizing_node.size[0] = LiteGraph.NODE_MIN_WIDTH; - - this.canvas.style.cursor = "se-resize"; - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - } - } - - /* - if((this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null) - this.draw(); - */ - - e.preventDefault(); - //e.stopPropagation(); - return false; - //this is not really optimal - //this.graph.change(); -} - -LGraphCanvas.prototype.processMouseUp = function(e) -{ - if(!this.graph) - return; - - var window = this.getCanvasWindow(); - var document = window.document; - LGraphCanvas.active_canvas = this; - - //restore the mousemove event back to the canvas - document.removeEventListener("mousemove", this._mousemove_callback, true ); - this.canvas.addEventListener("mousemove", this._mousemove_callback, true); - document.removeEventListener("mouseup", this._mouseup_callback, true ); - - this.adjustMouseEvent(e); - var now = LiteGraph.getTime(); - e.click_time = (now - this.last_mouseclick); - - if (e.which == 1) //left button - { - this.node_widget = null; - this.selected_group = null; - this.selected_group_resizing = false; - - if( this.dragging_rectangle ) - { - if(this.graph) - { - var nodes = this.graph._nodes; - var node_bounding = new Float32Array(4); - this.deselectAllNodes(); - if( this.dragging_rectangle[2] < 0 ) //flip if negative width - this.dragging_rectangle[0] += this.dragging_rectangle[2]; - if( this.dragging_rectangle[3] < 0 ) //flip if negative height - this.dragging_rectangle[1] += this.dragging_rectangle[3]; - this.dragging_rectangle[2] = Math.abs( this.dragging_rectangle[2] * this.scale ); //abs to convert negative width - this.dragging_rectangle[3] = Math.abs( this.dragging_rectangle[3] * this.scale ); //abs to convert negative height - - for(var i = 0; i < nodes.length; ++i) - { - var node = nodes[i]; - node.getBounding( node_bounding ); - if(!overlapBounding( this.dragging_rectangle, node_bounding )) - continue; //out of the visible area - this.selectNode( node, true ); - } - } - this.dragging_rectangle = null; - } - else if(this.connecting_node) //dragging a connection - { - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - - var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes ); - - //node below mouse - if(node) - { - if( this.connecting_output.type == LiteGraph.EVENT && this.isOverNodeBox( node, e.canvasX, e.canvasY ) ) - { - this.connecting_node.connect( this.connecting_slot, node, LiteGraph.EVENT ); - } - else - { - //slot below mouse? connect - var slot = this.isOverNodeInput(node, e.canvasX, e.canvasY); - if(slot != -1) - { - this.connecting_node.connect(this.connecting_slot, node, slot); - } - else - { //not on top of an input - var input = node.getInputInfo(0); - //auto connect - if(this.connecting_output.type == LiteGraph.EVENT) - this.connecting_node.connect( this.connecting_slot, node, LiteGraph.EVENT ); - else - if(input && !input.link && LiteGraph.isValidConnection( input.type && this.connecting_output.type ) ) - this.connecting_node.connect( this.connecting_slot, node, 0 ); - } - } - } - - this.connecting_output = null; - this.connecting_pos = null; - this.connecting_node = null; - this.connecting_slot = -1; - - }//not dragging connection - else if(this.resizing_node) - { - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - this.resizing_node = null; - } - else if(this.node_dragged) //node being dragged? - { - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - this.node_dragged.pos[0] = Math.round(this.node_dragged.pos[0]); - this.node_dragged.pos[1] = Math.round(this.node_dragged.pos[1]); - if(this.graph.config.align_to_grid) - this.node_dragged.alignToGrid(); - this.node_dragged = null; - } - else //no node being dragged - { - //get node over - var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes ); - if ( !node && e.click_time < 300 ) - this.deselectAllNodes(); - - this.dirty_canvas = true; - this.dragging_canvas = false; - - if( this.node_over && this.node_over.onMouseUp ) - this.node_over.onMouseUp(e, [e.canvasX - this.node_over.pos[0], e.canvasY - this.node_over.pos[1]] ); - if( this.node_capturing_input && this.node_capturing_input.onMouseUp ) - this.node_capturing_input.onMouseUp(e, [e.canvasX - this.node_capturing_input.pos[0], e.canvasY - this.node_capturing_input.pos[1]] ); - } - } - else if (e.which == 2) //middle button - { - //trace("middle"); - this.dirty_canvas = true; - this.dragging_canvas = false; - } - else if (e.which == 3) //right button - { - //trace("right"); - this.dirty_canvas = true; - this.dragging_canvas = false; - } - - /* - if((this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null) - this.draw(); - */ - - this.graph.change(); - - e.stopPropagation(); - e.preventDefault(); - return false; -} - - -LGraphCanvas.prototype.processMouseWheel = function(e) -{ - if(!this.graph || !this.allow_dragcanvas) - return; - - var delta = (e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60); - - this.adjustMouseEvent(e); - - var zoom = this.scale; - - if (delta > 0) - zoom *= 1.1; - else if (delta < 0) - zoom *= 1/(1.1); - - this.setZoom( zoom, [ e.localX, e.localY ] ); - - /* - if(this.rendering_timer_id == null) - this.draw(); - */ - - this.graph.change(); - - e.preventDefault(); - return false; // prevent default -} - -LGraphCanvas.prototype.isOverNodeBox = function( node, canvasx, canvasy ) -{ - var title_height = LiteGraph.NODE_TITLE_HEIGHT; - if( isInsideRectangle( canvasx, canvasy, node.pos[0] + 2, node.pos[1] + 2 - title_height, title_height - 4,title_height - 4) ) - return true; - return false; -} - -LGraphCanvas.prototype.isOverNodeInput = function(node, canvasx, canvasy, slot_pos ) -{ - if(node.inputs) - for(var i = 0, l = node.inputs.length; i < l; ++i) - { - var input = node.inputs[i]; - var link_pos = node.getConnectionPos(true,i); - if( isInsideRectangle(canvasx, canvasy, link_pos[0] - 10, link_pos[1] - 5, 20,10) ) - { - if(slot_pos) - { - slot_pos[0] = link_pos[0]; - slot_pos[1] = link_pos[1]; - } - return i; - } - } - return -1; -} - -LGraphCanvas.prototype.processKey = function(e) -{ - if(!this.graph) - return; - - var block_default = false; - //console.log(e); //debug - - if(e.target.localName == "input") - return; - - if(e.type == "keydown") - { - if(e.keyCode == 32) - { - this.dragging_canvas = true; - block_default = true; - } - - //select all Control A - if(e.keyCode == 65 && e.ctrlKey) - { - this.selectNodes(); - block_default = true; - } - - if(e.code == "KeyC" && (e.metaKey || e.ctrlKey) && !e.shiftKey ) //copy - { - if(this.selected_nodes) - { - this.copyToClipboard(); - block_default = true; - } - } - - if(e.code == "KeyV" && (e.metaKey || e.ctrlKey) && !e.shiftKey ) //paste - { - this.pasteFromClipboard(); - } - - //delete or backspace - if(e.keyCode == 46 || e.keyCode == 8) - { - this.deleteSelectedNodes(); - block_default = true; - } - - //collapse - //... - - //TODO - if(this.selected_nodes) - for (var i in this.selected_nodes) - if(this.selected_nodes[i].onKeyDown) - this.selected_nodes[i].onKeyDown(e); - } - else if( e.type == "keyup" ) - { - if(e.keyCode == 32) - this.dragging_canvas = false; - - if(this.selected_nodes) - for (var i in this.selected_nodes) - if(this.selected_nodes[i].onKeyUp) - this.selected_nodes[i].onKeyUp(e); - } - - this.graph.change(); - - if(block_default) - { - e.preventDefault(); - return false; - } -} - -LGraphCanvas.prototype.copyToClipboard = function() -{ - var clipboard_info = { - nodes: [], - links: [] - }; - var index = 0; - var selected_nodes_array = []; - for(var i in this.selected_nodes) - { - var node = this.selected_nodes[i]; - node._relative_id = index; - selected_nodes_array.push( node ); - index += 1; - } - - for(var i = 0; i < selected_nodes_array.length; ++i) - { - var node = selected_nodes_array[i]; - clipboard_info.nodes.push( node.clone().serialize() ); - if(node.inputs && node.inputs.length) - for(var j = 0; j < node.inputs.length; ++j) - { - var input = node.inputs[j]; - if(!input || input.link == null) - continue; - var link_info = this.graph.links[ input.link ]; - if(!link_info) - continue; - var target_node = this.graph.getNodeById( link_info.origin_id ); - if(!target_node || !this.selected_nodes[ target_node.id ] ) //improve this by allowing connections to non-selected nodes - continue; //not selected - clipboard_info.links.push([ target_node._relative_id, j, node._relative_id, link_info.target_slot ]); - } - } - localStorage.setItem( "litegrapheditor_clipboard", JSON.stringify( clipboard_info ) ); -} - -LGraphCanvas.prototype.pasteFromClipboard = function() -{ - var data = localStorage.getItem( "litegrapheditor_clipboard" ); - if(!data) - return; - - //create nodes - var clipboard_info = JSON.parse(data); - var nodes = []; - for(var i = 0; i < clipboard_info.nodes.length; ++i) - { - var node_data = clipboard_info.nodes[i]; - var node = LiteGraph.createNode( node_data.type ); - if(node) - { - node.configure(node_data); - node.pos[0] += 5; - node.pos[1] += 5; - this.graph.add( node ); - nodes.push( node ); - } - } - - //create links - for(var i = 0; i < clipboard_info.links.length; ++i) - { - var link_info = clipboard_info.links[i]; - var origin_node = nodes[ link_info[0] ]; - var target_node = nodes[ link_info[2] ]; - origin_node.connect( link_info[1], target_node, link_info[3] ); - } - - this.selectNodes( nodes ); -} - -LGraphCanvas.prototype.processDrop = function(e) -{ - e.preventDefault(); - this.adjustMouseEvent(e); - - - var pos = [e.canvasX,e.canvasY]; - var node = this.graph.getNodeOnPos(pos[0],pos[1]); - - if(!node) - { - var r = null; - if(this.onDropItem) - r = this.onDropItem( event ); - if(!r) - this.checkDropItem(e); - return; - } - - if( node.onDropFile || node.onDropData ) - { - var files = e.dataTransfer.files; - if(files && files.length) - { - for(var i=0; i < files.length; i++) - { - var file = e.dataTransfer.files[0]; - var filename = file.name; - var ext = LGraphCanvas.getFileExtension( filename ); - //console.log(file); - - if(node.onDropFile) - node.onDropFile(file); - - if(node.onDropData) - { - //prepare reader - var reader = new FileReader(); - reader.onload = function (event) { - //console.log(event.target); - var data = event.target.result; - node.onDropData( data, filename, file ); - }; - - //read data - var type = file.type.split("/")[0]; - if(type == "text" || type == "") - reader.readAsText(file); - else if (type == "image") - reader.readAsDataURL(file); - else - reader.readAsArrayBuffer(file); - } - } - } - } - - if(node.onDropItem) - { - if( node.onDropItem( event ) ) - return true; - } - - if(this.onDropItem) - return this.onDropItem( event ); - - return false; -} - -//called if the graph doesnt have a default drop item behaviour -LGraphCanvas.prototype.checkDropItem = function(e) -{ - if(e.dataTransfer.files.length) - { - var file = e.dataTransfer.files[0]; - var ext = LGraphCanvas.getFileExtension( file.name ).toLowerCase(); - var nodetype = LiteGraph.node_types_by_file_extension[ext]; - if(nodetype) - { - var node = LiteGraph.createNode( nodetype.type ); - node.pos = [e.canvasX, e.canvasY]; - this.graph.add( node ); - if( node.onDropFile ) - node.onDropFile( file ); - } - } -} - - -LGraphCanvas.prototype.processNodeDblClicked = function(n) -{ - if(this.onShowNodePanel) - this.onShowNodePanel(n); - - if(this.onNodeDblClicked) - this.onNodeDblClicked(n); - - this.setDirty(true); -} - -LGraphCanvas.prototype.processNodeSelected = function(node,e) -{ - this.selectNode( node, e && e.shiftKey ); - if(this.onNodeSelected) - this.onNodeSelected(node); -} - -LGraphCanvas.prototype.processNodeDeselected = function(node) -{ - this.deselectNode(node); - if(this.onNodeDeselected) - this.onNodeDeselected(node); -} - -LGraphCanvas.prototype.selectNode = function( node, add_to_current_selection ) -{ - if(node == null) - this.deselectAllNodes(); - else - this.selectNodes([node], add_to_current_selection ); -} - -LGraphCanvas.prototype.selectNodes = function( nodes, add_to_current_selection ) -{ - if(!add_to_current_selection) - this.deselectAllNodes(); - - nodes = nodes || this.graph._nodes; - for(var i = 0; i < nodes.length; ++i) - { - var node = nodes[i]; - if(node.selected) - continue; - - if( !node.selected && node.onSelected ) - node.onSelected(); - node.selected = true; - this.selected_nodes[ node.id ] = node; - - if(node.inputs) - for(var i = 0; i < node.inputs.length; ++i) - this.highlighted_links[ node.inputs[i].link ] = true; - if(node.outputs) - for(var i = 0; i < node.outputs.length; ++i) - { - var out = node.outputs[i]; - if( out.links ) - for(var j = 0; j < out.links.length; ++j) - this.highlighted_links[ out.links[j] ] = true; - } - - } - - this.setDirty(true); -} - -LGraphCanvas.prototype.deselectNode = function( node ) -{ - if(!node.selected) - return; - if(node.onDeselected) - node.onDeselected(); - node.selected = false; - - //remove highlighted - if(node.inputs) - for(var i = 0; i < node.inputs.length; ++i) - delete this.highlighted_links[ node.inputs[i].link ]; - if(node.outputs) - for(var i = 0; i < node.outputs.length; ++i) - { - var out = node.outputs[i]; - if( out.links ) - for(var j = 0; j < out.links.length; ++j) - delete this.highlighted_links[ out.links[j] ]; - } -} - -LGraphCanvas.prototype.deselectAllNodes = function() -{ - if(!this.graph) - return; - var nodes = this.graph._nodes; - for(var i = 0, l = nodes.length; i < l; ++i) - { - var node = nodes[i]; - if(!node.selected) - continue; - if(node.onDeselected) - node.onDeselected(); - node.selected = false; - } - this.selected_nodes = {}; - this.highlighted_links = {}; - this.setDirty(true); -} - -LGraphCanvas.prototype.deleteSelectedNodes = function() -{ - for(var i in this.selected_nodes) - { - var m = this.selected_nodes[i]; - //if(m == this.node_in_panel) this.showNodePanel(null); - this.graph.remove(m); - } - this.selected_nodes = {}; - this.highlighted_links = {}; - this.setDirty(true); -} - -LGraphCanvas.prototype.centerOnNode = function(node) -{ - this.offset[0] = -node.pos[0] - node.size[0] * 0.5 + (this.canvas.width * 0.5 / this.scale); - this.offset[1] = -node.pos[1] - node.size[1] * 0.5 + (this.canvas.height * 0.5 / this.scale); - this.setDirty(true,true); -} - -LGraphCanvas.prototype.adjustMouseEvent = function(e) -{ - if(this.canvas) - { - var b = this.canvas.getBoundingClientRect(); - e.localX = e.pageX - b.left; - e.localY = e.pageY - b.top; - } - else - { - e.localX = e.pageX; - e.localY = e.pageY; - } - - e.deltaX = e.localX - this.last_mouse_position[0]; - e.deltaY = e.localY - this.last_mouse_position[1]; - - this.last_mouse_position[0] = e.localX; - this.last_mouse_position[1] = e.localY; - - e.canvasX = e.localX / this.scale - this.offset[0]; - e.canvasY = e.localY / this.scale - this.offset[1]; -} - -LGraphCanvas.prototype.setZoom = function(value, zooming_center) -{ - if(!zooming_center && this.canvas) - zooming_center = [this.canvas.width * 0.5,this.canvas.height * 0.5]; - - var center = this.convertOffsetToCanvas( zooming_center ); - - this.scale = value; - - if(this.scale > this.max_zoom) - this.scale = this.max_zoom; - else if(this.scale < this.min_zoom) - this.scale = this.min_zoom; - - var new_center = this.convertOffsetToCanvas( zooming_center ); - var delta_offset = [new_center[0] - center[0], new_center[1] - center[1]]; - - this.offset[0] += delta_offset[0]; - this.offset[1] += delta_offset[1]; - - this.dirty_canvas = true; - this.dirty_bgcanvas = true; -} - -LGraphCanvas.prototype.convertOffsetToCanvas = function( pos, out ) -{ - out = out || []; - out[0] = pos[0] / this.scale - this.offset[0]; - out[1] = pos[1] / this.scale - this.offset[1]; - return out; -} - -LGraphCanvas.prototype.convertCanvasToOffset = function( pos, out ) -{ - out = out || []; - out[0] = (pos[0] + this.offset[0]) * this.scale; - out[1] = (pos[1] + this.offset[1]) * this.scale; - return out; -} - -LGraphCanvas.prototype.convertEventToCanvas = function(e) -{ - var rect = this.canvas.getBoundingClientRect(); - return this.convertOffsetToCanvas([e.pageX - rect.left,e.pageY - rect.top]); -} - -LGraphCanvas.prototype.bringToFront = function(n) -{ - var i = this.graph._nodes.indexOf(n); - if(i == -1) return; - - this.graph._nodes.splice(i,1); - this.graph._nodes.push(n); -} - -LGraphCanvas.prototype.sendToBack = function(n) -{ - var i = this.graph._nodes.indexOf(n); - if(i == -1) return; - - this.graph._nodes.splice(i,1); - this.graph._nodes.unshift(n); -} - -/* Interaction */ - - - -/* LGraphCanvas render */ -var temp = new Float32Array(4); - -LGraphCanvas.prototype.computeVisibleNodes = function( nodes, out ) -{ - var visible_nodes = out || []; - visible_nodes.length = 0; - nodes = nodes || this.graph._nodes; - for(var i = 0, l = nodes.length; i < l; ++i) - { - var n = nodes[i]; - - //skip rendering nodes in live mode - if(this.live_mode && !n.onDrawBackground && !n.onDrawForeground) - continue; - - if(!overlapBounding( this.visible_area, n.getBounding( temp ) )) - continue; //out of the visible area - - visible_nodes.push(n); - } - return visible_nodes; -} - -LGraphCanvas.prototype.draw = function(force_canvas, force_bgcanvas) -{ - if(!this.canvas) - return; - - //fps counting - var now = LiteGraph.getTime(); - this.render_time = (now - this.last_draw_time)*0.001; - this.last_draw_time = now; - - if(this.graph) - { - var start = [-this.offset[0], -this.offset[1] ]; - var end = [start[0] + this.canvas.width / this.scale, start[1] + this.canvas.height / this.scale]; - this.visible_area = new Float32Array([ start[0], start[1], end[0] - start[0], end[1] - start[1] ]); - } - - if(this.dirty_bgcanvas || force_bgcanvas || this.always_render_background || (this.graph && this.graph._last_trigger_time && (now - this.graph._last_trigger_time) < 1000) ) - this.drawBackCanvas(); - - if(this.dirty_canvas || force_canvas) - this.drawFrontCanvas(); - - this.fps = this.render_time ? (1.0 / this.render_time) : 0; - this.frame += 1; -} - -LGraphCanvas.prototype.drawFrontCanvas = function() -{ - this.dirty_canvas = false; - - if(!this.ctx) - this.ctx = this.bgcanvas.getContext("2d"); - var ctx = this.ctx; - if(!ctx) //maybe is using webgl... - return; - - if(ctx.start2D) - ctx.start2D(); - - var canvas = this.canvas; - - //reset in case of error - ctx.restore(); - ctx.setTransform(1, 0, 0, 1, 0, 0); - - //clip dirty area if there is one, otherwise work in full canvas - if(this.dirty_area) - { - ctx.save(); - ctx.beginPath(); - ctx.rect(this.dirty_area[0],this.dirty_area[1],this.dirty_area[2],this.dirty_area[3]); - ctx.clip(); - } - - //clear - //canvas.width = canvas.width; - if(this.clear_background) - ctx.clearRect(0,0,canvas.width, canvas.height); - - //draw bg canvas - if(this.bgcanvas == this.canvas) - this.drawBackCanvas(); - else - ctx.drawImage(this.bgcanvas,0,0); - - //rendering - if(this.onRender) - this.onRender(canvas, ctx); - - //info widget - if(this.show_info) - this.renderInfo(ctx); - - if(this.graph) - { - //apply transformations - ctx.save(); - ctx.scale(this.scale,this.scale); - ctx.translate( this.offset[0],this.offset[1] ); - - //draw nodes - var drawn_nodes = 0; - var visible_nodes = this.computeVisibleNodes( null, this.visible_nodes ); - - for (var i = 0; i < visible_nodes.length; ++i) - { - var node = visible_nodes[i]; - - //transform coords system - ctx.save(); - ctx.translate( node.pos[0], node.pos[1] ); - - //Draw - this.drawNode( node, ctx ); - drawn_nodes += 1; - - //Restore - ctx.restore(); - } - - //connections ontop? - if(this.graph.config.links_ontop) - if(!this.live_mode) - this.drawConnections(ctx); - - //current connection - if(this.connecting_pos != null) - { - ctx.lineWidth = this.connections_width; - var link_color = null; - switch( this.connecting_output.type ) - { - case LiteGraph.EVENT: link_color = "#F85"; break; - default: - link_color = "#AFA"; - } - //the connection being dragged by the mouse - this.renderLink( ctx, this.connecting_pos, [this.canvas_mouse[0],this.canvas_mouse[1]], null, false, null, link_color ); - - ctx.beginPath(); - if( this.connecting_output.type === LiteGraph.EVENT ) - ctx.rect( (this.connecting_pos[0] - 6) + 0.5, (this.connecting_pos[1] - 5) + 0.5,14,10); - else - ctx.arc( this.connecting_pos[0], this.connecting_pos[1],4,0,Math.PI*2); - ctx.fill(); - - ctx.fillStyle = "#ffcc00"; - if(this._highlight_input) - { - ctx.beginPath(); - ctx.arc( this._highlight_input[0], this._highlight_input[1],6,0,Math.PI*2); - ctx.fill(); - } - } - - if( this.dragging_rectangle ) - { - ctx.strokeStyle = "#FFF"; - ctx.strokeRect( this.dragging_rectangle[0], this.dragging_rectangle[1], this.dragging_rectangle[2], this.dragging_rectangle[3] ); - } - - - ctx.restore(); - } - - if(this.dirty_area) - { - ctx.restore(); - //this.dirty_area = null; - } - - if(ctx.finish2D) //this is a function I use in webgl renderer - ctx.finish2D(); -} - -LGraphCanvas.prototype.renderInfo = function( ctx, x, y ) -{ - x = x || 0; - y = y || 0; - - ctx.save(); - ctx.translate( x, y ); - - ctx.font = "10px Arial"; - ctx.fillStyle = "#888"; - if(this.graph) - { - ctx.fillText( "T: " + this.graph.globaltime.toFixed(2)+"s",5,13*1 ); - ctx.fillText( "I: " + this.graph.iteration,5,13*2 ); - ctx.fillText( "V: " + this.graph._version,5,13*3 ); - ctx.fillText( "FPS:" + this.fps.toFixed(2),5,13*4 ); - } - else - ctx.fillText( "No graph selected",5,13*1 ); - ctx.restore(); -} - -LGraphCanvas.prototype.drawBackCanvas = function() -{ - var canvas = this.bgcanvas; - if(canvas.width != this.canvas.width || - canvas.height != this.canvas.height) - { - canvas.width = this.canvas.width; - canvas.height = this.canvas.height; - } - - if(!this.bgctx) - this.bgctx = this.bgcanvas.getContext("2d"); - var ctx = this.bgctx; - if(ctx.start) - ctx.start(); - - //clear - if(this.clear_background) - ctx.clearRect(0,0,canvas.width, canvas.height); - - if(this._graph_stack && this._graph_stack.length) - { - ctx.strokeStyle = this._graph_stack[ this._graph_stack.length - 1].bgcolor; - ctx.lineWidth = 10; - ctx.strokeRect(1,1,canvas.width-2,canvas.height-2); - ctx.lineWidth = 1; - } - - var bg_already_painted = false; - if(this.onRenderBackground) - bg_already_painted = this.onRenderBackground(); - - //reset in case of error - ctx.restore(); - ctx.setTransform(1, 0, 0, 1, 0, 0); - - if(this.graph) - { - //apply transformations - ctx.save(); - ctx.scale(this.scale,this.scale); - ctx.translate(this.offset[0],this.offset[1]); - - //render BG - if(this.background_image && this.scale > 0.5 && !bg_already_painted) - { - if (this.zoom_modify_alpha) - ctx.globalAlpha = (1.0 - 0.5 / this.scale) * this.editor_alpha; - else - ctx.globalAlpha = this.editor_alpha; - ctx.imageSmoothingEnabled = ctx.mozImageSmoothingEnabled = ctx.imageSmoothingEnabled = false; - if(!this._bg_img || this._bg_img.name != this.background_image) - { - this._bg_img = new Image(); - this._bg_img.name = this.background_image; - this._bg_img.src = this.background_image; - var that = this; - this._bg_img.onload = function() { - that.draw(true,true); - } - } - - var pattern = null; - if(this._pattern == null && this._bg_img.width > 0) - { - pattern = ctx.createPattern( this._bg_img, 'repeat' ); - this._pattern_img = this._bg_img; - this._pattern = pattern; - } - else - pattern = this._pattern; - if(pattern) - { - ctx.fillStyle = pattern; - ctx.fillRect(this.visible_area[0],this.visible_area[1],this.visible_area[2],this.visible_area[3]); - ctx.fillStyle = "transparent"; - } - - ctx.globalAlpha = 1.0; - ctx.imageSmoothingEnabled = ctx.mozImageSmoothingEnabled = ctx.imageSmoothingEnabled = true; - } - - //groups - if(this.graph._groups.length && !this.live_mode) - this.drawGroups(canvas, ctx); - - if(this.onBackgroundRender) - this.onBackgroundRender(canvas, ctx); - - //DEBUG: show clipping area - //ctx.fillStyle = "red"; - //ctx.fillRect( this.visible_area[0] + 10, this.visible_area[1] + 10, this.visible_area[2] - 20, this.visible_area[3] - 20); - - //bg - if (this.render_canvas_border) { - ctx.strokeStyle = "#235"; - ctx.strokeRect(0,0,canvas.width,canvas.height); - } - - if(this.render_connections_shadows) - { - ctx.shadowColor = "#000"; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 0; - ctx.shadowBlur = 6; - } - else - ctx.shadowColor = "rgba(0,0,0,0)"; - - //draw connections - if(!this.live_mode) - this.drawConnections(ctx); - - ctx.shadowColor = "rgba(0,0,0,0)"; - - //restore state - ctx.restore(); - } - - if(ctx.finish) - ctx.finish(); - - this.dirty_bgcanvas = false; - this.dirty_canvas = true; //to force to repaint the front canvas with the bgcanvas -} - -var temp_vec2 = new Float32Array(2); - -/* Renders the LGraphNode on the canvas */ -LGraphCanvas.prototype.drawNode = function(node, ctx ) -{ - var glow = false; - this.current_node = node; - - var color = node.color || node.constructor.color || LiteGraph.NODE_DEFAULT_COLOR; - var bgcolor = node.bgcolor || node.constructor.bgcolor || LiteGraph.NODE_DEFAULT_BGCOLOR; - - //shadow and glow - if (node.mouseOver) - glow = true; - - if(node.selected) - { - /* - ctx.shadowColor = "#EEEEFF";//glow ? "#AAF" : "#000"; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 0; - ctx.shadowBlur = 1; - */ - } - else if(this.render_shadows) - { - ctx.shadowColor = "rgba(0,0,0,0.5)"; - ctx.shadowOffsetX = 2; - ctx.shadowOffsetY = 2; - ctx.shadowBlur = 3; - } - else - ctx.shadowColor = "transparent"; - - //only render if it forces it to do it - if(this.live_mode) - { - if(!node.flags.collapsed) - { - ctx.shadowColor = "transparent"; - if(node.onDrawForeground) - node.onDrawForeground(ctx, this); - } - - return; - } - - //custom draw collapsed method - if(node.flags.collapsed && node.onDrawCollaped && node.onDrawCollapsed(ctx, this) == true) - return; - - var editor_alpha = this.editor_alpha; - ctx.globalAlpha = editor_alpha; - - //clip if required (mask) - var shape = node._shape || LiteGraph.BOX_SHAPE; - var size = temp_vec2; - temp_vec2.set( node.size ); - if( node.flags.collapsed ) - { - ctx.font = this.inner_text_font; - var title = node.getTitle ? node.getTitle() : node.title; - node._collapsed_width = Math.min( node.size[0], ctx.measureText(title).width + 40 );//LiteGraph.NODE_COLLAPSED_WIDTH; - size[0] = node._collapsed_width; - size[1] = 0; - } - - if( node.flags.clip_area ) //Start clipping - { - ctx.save(); - ctx.beginPath(); - if(shape == LiteGraph.BOX_SHAPE) - ctx.rect(0,0,size[0], size[1]); - else if (shape == LiteGraph.ROUND_SHAPE) - ctx.roundRect(0,0,size[0], size[1],10); - else if (shape == LiteGraph.CIRCLE_SHAPE) - ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5, 0, Math.PI*2); - ctx.clip(); - } - - //draw shape - this.drawNodeShape( node, ctx, size, color, bgcolor, node.selected, node.mouseOver ); - ctx.shadowColor = "transparent"; - - //connection slots - ctx.textAlign = "left"; - ctx.font = this.inner_text_font; - - var render_text = this.scale > 0.6; - - var out_slot = this.connecting_output; - ctx.lineWidth = 1; - - var max_y = 0; - - //render inputs and outputs - if(!node.flags.collapsed) - { - //input connection slots - if(node.inputs) - for(var i = 0; i < node.inputs.length; i++) - { - var slot = node.inputs[i]; - - ctx.globalAlpha = editor_alpha; - //change opacity of incompatible slots when dragging a connection - if ( this.connecting_node && LiteGraph.isValidConnection( slot.type && out_slot.type ) ) - ctx.globalAlpha = 0.4 * editor_alpha; - - ctx.fillStyle = slot.link != null ? this.default_connection_color.input_on : this.default_connection_color.input_off; - - var pos = node.getConnectionPos( true, i ); - pos[0] -= node.pos[0]; - pos[1] -= node.pos[1]; - if( max_y < pos[1] + LiteGraph.NODE_SLOT_HEIGHT*0.5 ) - max_y = pos[1] + LiteGraph.NODE_SLOT_HEIGHT*0.5; - - ctx.beginPath(); - - if (slot.type === LiteGraph.EVENT) - ctx.rect((pos[0] - 6) + 0.5, (pos[1] - 5) + 0.5,14,10); - else - ctx.arc(pos[0],pos[1],4,0,Math.PI*2); - - ctx.fill(); - - //render name - if(render_text) - { - var text = slot.label != null ? slot.label : slot.name; - if(text) - { - ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR; - ctx.fillText(text,pos[0] + 10,pos[1] + 5); - } - } - } - - //output connection slots - if(this.connecting_node) - ctx.globalAlpha = 0.4 * editor_alpha; - - ctx.textAlign = "right"; - ctx.strokeStyle = "black"; - if(node.outputs) - for(var i = 0; i < node.outputs.length; i++) - { - var slot = node.outputs[i]; - - var pos = node.getConnectionPos(false,i); - pos[0] -= node.pos[0]; - pos[1] -= node.pos[1]; - if( max_y < pos[1] + LiteGraph.NODE_SLOT_HEIGHT*0.5) - max_y = pos[1] + LiteGraph.NODE_SLOT_HEIGHT*0.5; - - ctx.fillStyle = slot.links && slot.links.length ? this.default_connection_color.output_on : this.default_connection_color.output_off; - ctx.beginPath(); - //ctx.rect( node.size[0] - 14,i*14,10,10); - - if (slot.type === LiteGraph.EVENT) - ctx.rect((pos[0] - 6) + 0.5,(pos[1] - 5) + 0.5,14,10); - else - ctx.arc( pos[0],pos[1],4,0, Math.PI*2 ); - - //trigger - //if(slot.node_id != null && slot.slot == -1) - // ctx.fillStyle = "#F85"; - - //if(slot.links != null && slot.links.length) - ctx.fill(); - ctx.stroke(); - - //render output name - if(render_text) - { - var text = slot.label != null ? slot.label : slot.name; - if(text) - { - ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR; - ctx.fillText(text, pos[0] - 10,pos[1] + 5); - } - } - } - - ctx.textAlign = "left"; - ctx.globalAlpha = 1; - - if(node.widgets) - this.drawNodeWidgets( node, max_y, ctx, (this.node_widget && this.node_widget[0] == node) ? this.node_widget[1] : null ); - - //draw foreground - if(node.onDrawForeground) - { - //immediate gui stuff - if( node.gui_rects ) - node.gui_rects.length = 0; - node.onDrawForeground( ctx, this ); - } - } - else //if collapsed - { - if(node.inputs) - { - for(var i = 0; i < node.inputs.length; i++) - { - var slot = node.inputs[i]; - if( slot.link == null ) - continue; - ctx.fillStyle = this.default_connection_color.input_on; - ctx.beginPath(); - if ( slot.type === LiteGraph.EVENT ) - ctx.rect(0.5, 4 - LiteGraph.NODE_TITLE_HEIGHT + 0.5,14,LiteGraph.NODE_TITLE_HEIGHT - 8); - else - ctx.arc( 0, LiteGraph.NODE_TITLE_HEIGHT * -0.5, 4, 0, Math.PI*2 ); - ctx.fill(); - break; - } - } - - if(node.outputs) - { - for(var i = 0; i < node.outputs.length; i++) - { - var slot = node.outputs[i]; - if(!slot.links || !slot.links.length) - continue; - ctx.fillStyle = this.default_connection_color.output_on; - ctx.strokeStyle = "black"; - ctx.beginPath(); - if (slot.type === LiteGraph.EVENT) - ctx.rect( node._collapsed_width - 4 + 0.5, 4 - LiteGraph.NODE_TITLE_HEIGHT + 0.5,14,LiteGraph.NODE_TITLE_HEIGHT - 8); - else - ctx.arc( node._collapsed_width, LiteGraph.NODE_TITLE_HEIGHT * -0.5, 4, 0, Math.PI*2 ); - ctx.fill(); - ctx.stroke(); - } - } - - } - - if(node.flags.clip_area) - ctx.restore(); - - ctx.globalAlpha = 1.0; -} - -/* Renders the node shape */ -LGraphCanvas.prototype.drawNodeShape = function( node, ctx, size, fgcolor, bgcolor, selected, mouse_over ) -{ - //bg rect - ctx.strokeStyle = fgcolor; - ctx.fillStyle = bgcolor; - - var title_height = LiteGraph.NODE_TITLE_HEIGHT; - - //render node area depending on shape - var shape = node._shape || node.constructor.shape || LiteGraph.BOX_SHAPE; - var title_mode = node.constructor.title_mode; - - var render_title = true; - if( title_mode == LiteGraph.TRANSPARENT_TITLE ) - render_title = false; - else if( title_mode == LiteGraph.AUTOHIDE_TITLE && mouse_over) - render_title = true; - - var areax = 0; - var areay = render_title ? -title_height : 0; - var areaw = size[0]+1; - var areah = render_title ? size[1] + title_height : size[1]; - - if(!node.flags.collapsed) - { - if(shape == LiteGraph.BOX_SHAPE || this.scale < 0.5) - { - ctx.beginPath(); - ctx.rect( areax, areay, areaw, areah ); - ctx.fill(); - } - else if (shape == LiteGraph.ROUND_SHAPE || shape == LiteGraph.CARD_SHAPE) - { - ctx.beginPath(); - ctx.roundRect( areax, areay, areaw, areah, this.round_radius, shape == LiteGraph.CARD_SHAPE ? 0 : this.round_radius); - ctx.fill(); - } - else if (shape == LiteGraph.CIRCLE_SHAPE) - { - ctx.beginPath(); - ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5, 0, Math.PI*2); - ctx.fill(); - } - } - ctx.shadowColor = "transparent"; - - //image - if (node.bgImage && node.bgImage.width) - ctx.drawImage( node.bgImage, (size[0] - node.bgImage.width) * 0.5 , (size[1] - node.bgImage.height) * 0.5); - - if(node.bgImageUrl && !node.bgImage) - node.bgImage = node.loadImage(node.bgImageUrl); - - if( node.onDrawBackground ) - { - //immediate gui stuff - if( node.gui_rects ) - node.gui_rects.length = 0; - node.onDrawBackground( ctx, this ); - } - - //title bg (remember, it is rendered ABOVE the node) - if(render_title || title_mode == LiteGraph.TRANSPARENT_TITLE ) - { - //title bar - if(title_mode != LiteGraph.TRANSPARENT_TITLE) //!node.flags.collapsed) - { - //* gradient test - if(this.use_gradients) - { - var grad = LGraphCanvas.gradients[ fgcolor ]; - if(!grad) - { - grad = LGraphCanvas.gradients[ fgcolor ] = ctx.createLinearGradient(0,0,400,0); - grad.addColorStop(0, fgcolor); - grad.addColorStop(1, "#000"); - } - ctx.fillStyle = grad; - } - else - ctx.fillStyle = fgcolor; - - var old_alpha = ctx.globalAlpha; - //ctx.globalAlpha = 0.5 * old_alpha; - ctx.beginPath(); - if(shape == LiteGraph.BOX_SHAPE || this.scale < 0.5) - { - ctx.rect(0, -title_height, size[0]+1, title_height); - ctx.fill() - //ctx.stroke(); - } - else if (shape == LiteGraph.ROUND_SHAPE || shape == LiteGraph.CARD_SHAPE) - { - ctx.roundRect(0,-title_height,size[0], title_height, this.round_radius, node.flags.collapsed ? this.round_radius : 0); - ctx.fill(); - } - - /* - else if (shape == LiteGraph.CIRCLE_SHAPE) - { - ctx.beginPath(); - ctx.arc(title_height *0.5, title_height * -0.5, (title_height - 6) *0.5,0,Math.PI*2); - ctx.fill(); - } - */ - } - - //title box - ctx.fillStyle = node.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR; - ctx.beginPath(); - if (shape == LiteGraph.ROUND_SHAPE || shape == LiteGraph.CIRCLE_SHAPE || shape == LiteGraph.CARD_SHAPE) - ctx.arc(title_height *0.5, title_height * -0.5, (title_height - 8) *0.5,0,Math.PI*2); - else - ctx.rect(4,-title_height + 4,title_height - 8,title_height - 8); - ctx.fill(); - ctx.globalAlpha = old_alpha; - - //title text - if( this.scale > 0.5 ) - { - ctx.font = this.title_text_font; - var title = node.getTitle(); - if(title) - { - if(selected) - ctx.fillStyle = "white"; - else - ctx.fillStyle = node.constructor.title_text_color || this.node_title_color; - if( node.flags.collapsed ) - { - ctx.textAlign = "center"; - var measure = ctx.measureText(title); - ctx.fillText( title, title_height + measure.width * 0.5, -title_height * 0.2 ); - ctx.textAlign = "left"; - } - else - { - ctx.textAlign = "left"; - ctx.fillText( title, title_height, -title_height * 0.2 ); - } - } - } - } - - //render selection marker - if(selected) - { - if( title_mode == LiteGraph.TRANSPARENT_TITLE ) - { - areay -= title_height; - areah += title_height; - } - ctx.lineWidth = 1; - ctx.beginPath(); - if(shape == LiteGraph.BOX_SHAPE) - ctx.rect(-6 + areax,-6 + areay, 12 + areaw, 12 + areah ); - else if (shape == LiteGraph.ROUND_SHAPE || (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed) ) - ctx.roundRect(-6 + areax,-6 + areay, 12 + areaw, 12 + areah , this.round_radius * 2); - else if (shape == LiteGraph.CARD_SHAPE) - ctx.roundRect(-6 + areax,-6 + areay, 12 + areaw, 12 + areah , this.round_radius * 2, 2); - else if (shape == LiteGraph.CIRCLE_SHAPE) - ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI*2); - ctx.strokeStyle = "#DDD"; - ctx.stroke(); - ctx.strokeStyle = fgcolor; - } -} - -//OPTIMIZE THIS: precatch connections position instead of recomputing them every time -LGraphCanvas.prototype.drawConnections = function(ctx) -{ - var now = LiteGraph.getTime(); - - //draw connections - ctx.lineWidth = this.connections_width; - - ctx.fillStyle = "#AAA"; - ctx.strokeStyle = "#AAA"; - ctx.globalAlpha = this.editor_alpha; - //for every node - for (var n = 0, l = this.graph._nodes.length; n < l; ++n) - { - var node = this.graph._nodes[n]; - //for every input (we render just inputs because it is easier as every slot can only have one input) - if(node.inputs && node.inputs.length) - for(var i = 0; i < node.inputs.length; ++i) - { - var input = node.inputs[i]; - if(!input || input.link == null) - continue; - var link_id = input.link; - var link = this.graph.links[ link_id ]; - if(!link) - continue; - - var start_node = this.graph.getNodeById( link.origin_id ); - if(start_node == null) continue; - var start_node_slot = link.origin_slot; - var start_node_slotpos = null; - - if(start_node_slot == -1) - start_node_slotpos = [start_node.pos[0] + 10, start_node.pos[1] + 10]; - else - start_node_slotpos = start_node.getConnectionPos(false, start_node_slot); - - this.renderLink( ctx, start_node_slotpos, node.getConnectionPos(true,i), link ); - - //event triggered rendered on top - if(link && link._last_time && (now - link._last_time) < 1000 ) - { - var f = 2.0 - (now - link._last_time) * 0.002; - var color = "rgba(255,255,255, " + f.toFixed(2) + ")"; - this.renderLink( ctx, start_node_slotpos, node.getConnectionPos(true,i), link, true, f, color ); - } - } - } - ctx.globalAlpha = 1; -} - -LGraphCanvas.prototype.renderLink = function( ctx, a, b, link, skip_border, flow, color ) -{ - if(!this.highquality_render) - { - ctx.beginPath(); - ctx.moveTo(a[0],a[1]); - ctx.lineTo(b[0],b[1]); - ctx.stroke(); - return; - } - - var dist = distance(a,b); - - if(this.render_connections_border && this.scale > 0.6) - ctx.lineWidth = this.connections_width + 4; - - //choose color - if( !color && link ) - color = LGraphCanvas.link_type_colors[ link.type ]; - if(!color) - color = this.default_link_color; - - if( link != null && this.highlighted_links[ link.id ] ) - color = "#FFF"; - - //begin line shape - ctx.beginPath(); - - if(this.render_curved_connections) //splines - { - ctx.moveTo(a[0],a[1]); - ctx.bezierCurveTo(a[0] + dist*0.25, a[1], - b[0] - dist*0.25 , b[1], - b[0] ,b[1] ); - } - else //lines - { - ctx.moveTo(a[0]+10,a[1]); - ctx.lineTo(((a[0]+10) + (b[0]-10))*0.5,a[1]); - ctx.lineTo(((a[0]+10) + (b[0]-10))*0.5,b[1]); - ctx.lineTo(b[0]-10,b[1]); - } - - //rendering the outline of the connection can be a little bit slow - if(this.render_connections_border && this.scale > 0.6 && !skip_border) - { - ctx.strokeStyle = "rgba(0,0,0,0.5)"; - ctx.stroke(); - } - - ctx.lineWidth = this.connections_width; - ctx.fillStyle = ctx.strokeStyle = color; - ctx.stroke(); - //end line shape - - //render arrow in the middle - if( this.render_connection_arrows && this.scale >= 0.6 ) - { - //render arrow - if(this.render_connection_arrows && this.scale > 0.6) - { - //compute two points in the connection - var pos = this.computeConnectionPoint(a,b,0.5); - var pos2 = this.computeConnectionPoint(a,b,0.51); - - //compute the angle between them so the arrow points in the right direction - var angle = 0; - if(this.render_curved_connections) - angle = -Math.atan2( pos2[0] - pos[0], pos2[1] - pos[1]); - else - angle = b[1] > a[1] ? 0 : Math.PI; - - //render arrow - ctx.save(); - ctx.translate(pos[0],pos[1]); - ctx.rotate(angle); - ctx.beginPath(); - ctx.moveTo(-5,-5); - ctx.lineTo(0,+5); - ctx.lineTo(+5,-5); - ctx.fill(); - ctx.restore(); - } - } - - //render flowing points - if(flow) - { - for(var i = 0; i < 5; ++i) - { - var f = (LiteGraph.getTime() * 0.001 + (i * 0.2)) % 1; - var pos = this.computeConnectionPoint(a,b,f); - ctx.beginPath(); - ctx.arc(pos[0],pos[1],5,0,2*Math.PI); - ctx.fill(); - } - } -} - -LGraphCanvas.prototype.computeConnectionPoint = function(a,b,t) -{ - var dist = distance(a,b); - var p0 = a; - var p1 = [ a[0] + dist*0.25, a[1] ]; - var p2 = [ b[0] - dist*0.25, b[1] ]; - var p3 = b; - - var c1 = (1-t)*(1-t)*(1-t); - var c2 = 3*((1-t)*(1-t))*t; - var c3 = 3*(1-t)*(t*t); - var c4 = t*t*t; - - var x = c1*p0[0] + c2*p1[0] + c3*p2[0] + c4*p3[0]; - var y = c1*p0[1] + c2*p1[1] + c3*p2[1] + c4*p3[1]; - return [x,y]; -} - -LGraphCanvas.prototype.drawNodeWidgets = function( node, posY, ctx, active_widget ) -{ - if(!node.widgets || !node.widgets.length) - return 0; - var width = node.size[0]; - var widgets = node.widgets; - posY += 2; - var H = LiteGraph.NODE_WIDGET_HEIGHT; - var show_text = this.scale > 0.5; - - for(var i = 0; i < widgets.length; ++i) - { - var w = widgets[i]; - var y = posY; - if(w.y) - y = w.y; - w.last_y = y; - ctx.strokeStyle = "#AAA"; - ctx.fillStyle = "#222"; - ctx.textAlign = "left"; - - switch( w.type ) - { - case "button": - if(w.clicked) - { - ctx.fillStyle = "#AAA"; - w.clicked = false; - this.dirty_canvas = true; - } - ctx.fillRect(10,y,width-20,H); - ctx.strokeRect(10,y,width-20,H); - if(show_text) - { - ctx.textAlign = "center"; - ctx.fillStyle = "#AAA"; - ctx.fillText( w.name, width*0.5, y + H*0.7 ); - } - break; - case "slider": - ctx.fillStyle = "#111"; - ctx.fillRect(10,y,width-20,H); - var range = w.options.max - w.options.min; - var nvalue = (w.value - w.options.min) / range; - ctx.fillStyle = active_widget == w ? "#89A" : "#678"; - ctx.fillRect(10,y,nvalue*(width-20),H); - ctx.strokeRect(10,y,width-20,H); - if(show_text) - { - ctx.textAlign = "center"; - ctx.fillStyle = "#DDD"; - ctx.fillText( w.name + " " + Number(w.value).toFixed(3), width*0.5, y + H*0.7 ); - } - break; - case "number": - case "combo": - ctx.textAlign = "left"; - ctx.strokeStyle = "#AAA"; - ctx.fillStyle = "#111"; - ctx.beginPath(); - ctx.roundRect( 10, posY, width - 20, H,H*0.5 ); - ctx.fill(); - ctx.stroke(); - ctx.fillStyle = "#AAA"; - ctx.beginPath(); - ctx.moveTo( 26, posY + 5 ); - ctx.lineTo( 16, posY + H*0.5 ); - ctx.lineTo( 26, posY + H - 5 ); - ctx.moveTo( width - 26, posY + 5 ); - ctx.lineTo( width - 16, posY + H*0.5 ); - ctx.lineTo( width - 26, posY + H - 5 ); - ctx.fill(); - if(show_text) - { - ctx.fillStyle = "#999"; - ctx.fillText( w.name, 30, y + H*0.7 ); - ctx.fillStyle = "#DDD"; - ctx.textAlign = "right"; - if(w.type == "number") - ctx.fillText( Number(w.value).toFixed( w.options.precision !== undefined ? w.options.precision : 3), width - 40, y + H*0.7 ); - else - ctx.fillText( w.value, width - 40, y + H*0.7 ); - } - break; - default: - break; - } - posY += H + 4; - } - ctx.textAlign = "left"; -} - -LGraphCanvas.prototype.processNodeWidgets = function( node, pos, event, active_widget ) -{ - if(!node.widgets || !node.widgets.length) - return null; - - var x = pos[0] - node.pos[0]; - var y = pos[1] - node.pos[1]; - var width = node.size[0]; - var that = this; - - for(var i = 0; i < node.widgets.length; ++i) - { - var w = node.widgets[i]; - if( w == active_widget || (x > 6 && x < (width - 12) && y > w.last_y && y < (w.last_y + LiteGraph.NODE_WIDGET_HEIGHT)) ) - { - //inside widget - switch( w.type ) - { - case "button": - if(w.callback) - setTimeout( function(){ w.callback( w, that, node, pos ); }, 20 ); - w.clicked = true; - this.dirty_canvas = true; - break; - case "slider": - var range = w.options.max - w.options.min; - var nvalue = Math.clamp( (x - 10) / (width - 20), 0, 1); - w.value = w.options.min + (w.options.max - w.options.min) * nvalue; - if(w.callback) - setTimeout( function(){ w.callback( w.value, that, node, pos ); }, 20 ); - this.dirty_canvas = true; - break; - case "number": - case "combo": - if(event.type == "mousemove" && w.type == "number") - { - w.value += (event.deltaX * 0.1) * (w.options.step || 1); - if(w.options.min != null && w.value < w.options.min) - w.value = w.options.min; - if(w.options.max != null && w.value > w.options.max) - w.value = w.options.max; - } - else if( event.type == "mousedown" ) - { - var delta = ( x < 40 ? -1 : ( x > width - 40 ? 1 : 0) ); - if (w.type == "number") - { - w.value += delta * 0.1 * (w.options.step || 1); - if(w.options.min != null && w.value < w.options.min) - w.value = w.options.min; - if(w.options.max != null && w.value > w.options.max) - w.value = w.options.max; - } - else if(delta) - { - var index = w.options.values.indexOf( w.value ) + delta; - if( index >= w.options.values.length ) - index = 0; - if( index < 0 ) - index = w.options.values.length - 1; - w.value = w.options.values[ index ]; - } - } - if(w.callback) - setTimeout( function(){ w.callback( w.value, that, node, pos ); }, 20 ); - this.dirty_canvas = true; - break; - } - - return w; - } - } - return null; -} - - -LGraphCanvas.prototype.drawGroups = function(canvas, ctx) -{ - if(!this.graph) - return; - - var groups = this.graph._groups; - - ctx.save(); - ctx.globalAlpha = 0.5; - ctx.font = "24px Arial"; - - for(var i = 0; i < groups.length; ++i) - { - var group = groups[i]; - - if(!overlapBounding( this.visible_area, group._bounding )) - continue; //out of the visible area - - ctx.fillStyle = group.color || "#335"; - ctx.strokeStyle = group.color || "#335"; - var pos = group._pos; - var size = group._size; - ctx.globalAlpha = 0.25; - ctx.beginPath(); - ctx.rect( pos[0] + 0.5, pos[1] + 0.5, size[0], size[1] ); - ctx.fill(); - ctx.globalAlpha = 1; - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo( pos[0] + size[0], pos[1] + size[1] ); - ctx.lineTo( pos[0] + size[0] - 10, pos[1] + size[1] ); - ctx.lineTo( pos[0] + size[0], pos[1] + size[1] - 10 ); - ctx.fill(); - - ctx.fillText( group.title, pos[0] + 4, pos[1] + 24 ); - } - - ctx.restore(); -} - - -LGraphCanvas.prototype.resize = function(width, height) -{ - if(!width && !height) - { - var parent = this.canvas.parentNode; - width = parent.offsetWidth; - height = parent.offsetHeight; - } - - if(this.canvas.width == width && this.canvas.height == height) - return; - - this.canvas.width = width; - this.canvas.height = height; - this.bgcanvas.width = this.canvas.width; - this.bgcanvas.height = this.canvas.height; - this.setDirty(true,true); -} - - -LGraphCanvas.prototype.switchLiveMode = function(transition) -{ - if(!transition) - { - this.live_mode = !this.live_mode; - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - return; - } - - var self = this; - var delta = this.live_mode ? 1.1 : 0.9; - if(this.live_mode) - { - this.live_mode = false; - this.editor_alpha = 0.1; - } - - var t = setInterval(function() { - self.editor_alpha *= delta; - self.dirty_canvas = true; - self.dirty_bgcanvas = true; - - if(delta < 1 && self.editor_alpha < 0.01) - { - clearInterval(t); - if(delta < 1) - self.live_mode = true; - } - if(delta > 1 && self.editor_alpha > 0.99) - { - clearInterval(t); - self.editor_alpha = 1; - } - },1); -} - -LGraphCanvas.prototype.onNodeSelectionChange = function(node) -{ - return; //disabled - //if(this.node_in_panel) this.showNodePanel(node); -} - -LGraphCanvas.prototype.touchHandler = function(event) -{ - //alert("foo"); - var touches = event.changedTouches, - first = touches[0], - type = ""; - - switch(event.type) - { - case "touchstart": type = "mousedown"; break; - case "touchmove": type = "mousemove"; break; - case "touchend": type = "mouseup"; break; - default: return; - } - - //initMouseEvent(type, canBubble, cancelable, view, clickCount, - // screenX, screenY, clientX, clientY, ctrlKey, - // altKey, shiftKey, metaKey, button, relatedTarget); - - var window = this.getCanvasWindow(); - var document = window.document; - - var simulatedEvent = document.createEvent("MouseEvent"); - simulatedEvent.initMouseEvent(type, true, true, window, 1, - first.screenX, first.screenY, - first.clientX, first.clientY, false, - false, false, false, 0/*left*/, null); - first.target.dispatchEvent(simulatedEvent); - event.preventDefault(); -} - -/* CONTEXT MENU ********************/ - -LGraphCanvas.onGroupAdd = function(info,entry,mouse_event) -{ - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var group = new LiteGraph.LGraphGroup(); - group.pos = canvas.convertEventToCanvas( mouse_event ); - canvas.graph.add( group ); -} - -LGraphCanvas.onMenuAdd = function( node, options, e, prev_menu ) -{ - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var values = LiteGraph.getNodeTypesCategories(); - var entries = []; - for(var i in values) - if(values[i]) - entries.push({ value: values[i], content: values[i], has_submenu: true }); - - var menu = new LiteGraph.ContextMenu( entries, { event: e, callback: inner_clicked, parentMenu: prev_menu }, ref_window); - - function inner_clicked( v, option, e ) - { - var category = v.value; - var node_types = LiteGraph.getNodeTypesInCategory( category, canvas.filter ); - var values = []; - for(var i in node_types) - values.push( { content: node_types[i].title, value: node_types[i].type }); - - new LiteGraph.ContextMenu( values, {event: e, callback: inner_create, parentMenu: menu }, ref_window); - return false; - } - - function inner_create( v, e ) - { - var first_event = prev_menu.getFirstEvent(); - var node = LiteGraph.createNode( v.value ); - if(node) - { - node.pos = canvas.convertEventToCanvas( first_event ); - canvas.graph.add( node ); - } - } - - return false; -} - -LGraphCanvas.onMenuCollapseAll = function() -{ - -} - - -LGraphCanvas.onMenuNodeEdit = function() -{ - -} - -LGraphCanvas.showMenuNodeOptionalInputs = function( v, options, e, prev_menu, node ) -{ - if(!node) - return; - - var that = this; - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var options = node.optional_inputs; - if(node.onGetInputs) - options = node.onGetInputs(); - - var entries = []; - if(options) - for (var i in options) - { - var entry = options[i]; - if(!entry) - { - entries.push(null); - continue; - } - var label = entry[0]; - if(entry[2] && entry[2].label) - label = entry[2].label; - var data = {content: label, value: entry}; - if(entry[1] == LiteGraph.ACTION) - data.className = "event"; - entries.push(data); - } - - if(this.onMenuNodeInputs) - entries = this.onMenuNodeInputs( entries ); - - if(!entries.length) - return; - - var menu = new LiteGraph.ContextMenu(entries, { event: e, callback: inner_clicked, parentMenu: prev_menu, node: node }, ref_window); - - function inner_clicked(v, e, prev) - { - if(!node) - return; - - if(v.callback) - v.callback.call( that, node, v, e, prev ); - - if(v.value) - { - node.addInput(v.value[0],v.value[1], v.value[2]); - node.setDirtyCanvas(true,true); - } - } - - return false; -} - -LGraphCanvas.showMenuNodeOptionalOutputs = function( v, options, e, prev_menu, node ) -{ - if(!node) - return; - - var that = this; - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var options = node.optional_outputs; - if(node.onGetOutputs) - options = node.onGetOutputs(); - - var entries = []; - if(options) - for (var i in options) - { - var entry = options[i]; - if(!entry) //separator? - { - entries.push(null); - continue; - } - - if(node.flags && node.flags.skip_repeated_outputs && node.findOutputSlot(entry[0]) != -1) - continue; //skip the ones already on - var label = entry[0]; - if(entry[2] && entry[2].label) - label = entry[2].label; - var data = {content: label, value: entry}; - if(entry[1] == LiteGraph.EVENT) - data.className = "event"; - entries.push(data); - } - - if(this.onMenuNodeOutputs) - entries = this.onMenuNodeOutputs( entries ); - - if(!entries.length) - return; - - var menu = new LiteGraph.ContextMenu(entries, {event: e, callback: inner_clicked, parentMenu: prev_menu, node: node }, ref_window); - - function inner_clicked( v, e, prev ) - { - if(!node) - return; - - if(v.callback) - v.callback.call( that, node, v, e, prev ); - - if(!v.value) - return; - - var value = v.value[1]; - - if(value && (value.constructor === Object || value.constructor === Array)) //submenu why? - { - var entries = []; - for(var i in value) - entries.push({ content: i, value: value[i]}); - new LiteGraph.ContextMenu( entries, { event: e, callback: inner_clicked, parentMenu: prev_menu, node: node }); - return false; - } - else - { - node.addOutput( v.value[0], v.value[1], v.value[2]); - node.setDirtyCanvas(true,true); - } - - } - - return false; -} - -LGraphCanvas.onShowMenuNodeProperties = function( value, options, e, prev_menu, node ) -{ - if(!node || !node.properties) - return; - - var that = this; - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var entries = []; - for (var i in node.properties) - { - var value = node.properties[i] !== undefined ? node.properties[i] : " "; - //value could contain invalid html characters, clean that - value = LGraphCanvas.decodeHTML(value); - entries.push({content: "" + i + "" + "" + value + "", value: i}); - } - if(!entries.length) - return; - - var menu = new LiteGraph.ContextMenu(entries, {event: e, callback: inner_clicked, parentMenu: prev_menu, allow_html: true, node: node },ref_window); - - function inner_clicked( v, options, e, prev ) - { - if(!node) - return; - var rect = this.getBoundingClientRect(); - canvas.showEditPropertyValue( node, v.value, { position: [rect.left, rect.top] }); - } - - return false; -} - -LGraphCanvas.decodeHTML = function( str ) -{ - var e = document.createElement("div"); - e.innerText = str; - return e.innerHTML; -} - -LGraphCanvas.onResizeNode = function( value, options, e, menu, node ) -{ - if(!node) - return; - node.size = node.computeSize(); - node.setDirtyCanvas(true,true); -} - - -LGraphCanvas.onShowTitleEditor = function( value, options, e, menu, node ) -{ - var input_html = ""; - - var dialog = document.createElement("div"); - dialog.className = "graphdialog"; - dialog.innerHTML = "Title"; - var input = dialog.querySelector("input"); - if(input) - { - input.value = node.title; - input.addEventListener("keydown", function(e){ - if(e.keyCode != 13) - return; - inner(); - e.preventDefault(); - e.stopPropagation(); - }); - } - - var graphcanvas = LGraphCanvas.active_canvas; - var canvas = graphcanvas.canvas; - - var rect = canvas.getBoundingClientRect(); - var offsetx = -20; - var offsety = -20; - if(rect) - { - offsetx -= rect.left; - offsety -= rect.top; - } - - if( event ) - { - dialog.style.left = (event.pageX + offsetx) + "px"; - dialog.style.top = (event.pageY + offsety)+ "px"; - } - else - { - dialog.style.left = (canvas.width * 0.5 + offsetx) + "px"; - dialog.style.top = (canvas.height * 0.5 + offsety) + "px"; - } - - var button = dialog.querySelector("button"); - button.addEventListener("click", inner ); - canvas.parentNode.appendChild( dialog ); - - function inner() - { - setValue( input.value ); - } - - function setValue(value) - { - node.title = value; - dialog.parentNode.removeChild( dialog ); - node.setDirtyCanvas(true,true); - } -} - -LGraphCanvas.prototype.showSearchBox = function(event) -{ - var that = this; - var input_html = ""; - - var dialog = document.createElement("div"); - dialog.className = "graphdialog"; - dialog.innerHTML = "Search
"; - dialog.close = function() - { - that.search_box = null; - dialog.parentNode.removeChild( dialog ); - } - - dialog.addEventListener("mouseleave",function(e){ - dialog.close(); - }); - - if(that.search_box) - that.search_box.close(); - that.search_box = dialog; - - var helper = dialog.querySelector(".helper"); - - var first = null; - var timeout = null; - var selected = null; - - var input = dialog.querySelector("input"); - if(input) - { - input.addEventListener("keydown", function(e){ - - if(e.keyCode == 38) //UP - changeSelection(false); - else if(e.keyCode == 40) //DOWN - changeSelection(true); - else if(e.keyCode == 27) //ESC - dialog.close(); - else if(e.keyCode == 13) - { - if(selected) - select( selected.innerHTML ) - else if(first) - select(first); - else - dialog.close(); - } - else - { - if(timeout) - clearInterval(timeout); - timeout = setTimeout( refreshHelper, 10 ); - return; - } - e.preventDefault(); - e.stopPropagation(); - }); - } - - var graphcanvas = LGraphCanvas.active_canvas; - var canvas = graphcanvas.canvas; - - var rect = canvas.getBoundingClientRect(); - var offsetx = -20; - var offsety = -20; - if(rect) - { - offsetx -= rect.left; - offsety -= rect.top; - } - - if( event ) - { - dialog.style.left = (event.pageX + offsetx) + "px"; - dialog.style.top = (event.pageY + offsety)+ "px"; - } - else - { - dialog.style.left = (canvas.width * 0.5 + offsetx) + "px"; - dialog.style.top = (canvas.height * 0.5 + offsety) + "px"; - } - - canvas.parentNode.appendChild( dialog ); - input.focus(); - - function select( name ) - { - if(name) - { - if( that.onSearchBoxSelection ) - that.onSearchBoxSelection( name, event, graphcanvas ); - else - { - var node = LiteGraph.createNode( name ); - if(node) - { - node.pos = graphcanvas.convertEventToCanvas( event ); - graphcanvas.graph.add( node ); - } - } - } - - dialog.close(); - } - - function changeSelection( forward ) - { - var prev = selected; - if(selected) - selected.classList.remove("selected"); - if(!selected) - selected = forward ? helper.childNodes[0] : helper.childNodes[ helper.childNodes.length ]; - else - { - selected = forward ? selected.nextSibling : selected.previousSibling; - if(!selected) - selected = prev; - } - if(!selected) - return; - selected.classList.add("selected"); - selected.scrollIntoView(); - } - - function refreshHelper() - { - timeout = null; - var str = input.value; - first = null; - helper.innerHTML = ""; - if(!str) - return; - - if( that.onSearchBox ) - that.onSearchBox( help, str, graphcanvas ); - else - for( var i in LiteGraph.registered_node_types ) - if(i.indexOf(str) != -1) - { - var help = document.createElement("div"); - if(!first) first = i; - help.innerText = i; - help.className = "help-item"; - help.addEventListener("click", function(e){ - select( this.innerText ); - }); - helper.appendChild(help); - } - } - - return dialog; -} - -LGraphCanvas.prototype.showEditPropertyValue = function( node, property, options ) -{ - if(!node || node.properties[ property ] === undefined ) - return; - - options = options || {}; - var that = this; - - var type = "string"; - - if(node.properties[ property ] !== null) - type = typeof(node.properties[ property ]); - - //for arrays - if(type == "object") - { - if( node.properties[ property ].length ) - type = "array"; - } - - var info = null; - if(node.getPropertyInfo) - info = node.getPropertyInfo(property); - if(node.properties_info) - { - for(var i = 0; i < node.properties_info.length; ++i) - { - if( node.properties_info[i].name == property ) - { - info = node.properties_info[i]; - break; - } - } - } - - if(info !== undefined && info !== null && info.type ) - type = info.type; - - var input_html = ""; - - if(type == "string" || type == "number" || type == "array") - input_html = ""; - else if(type == "enum" && info.values) - { - input_html = ""; - } - else if(type == "boolean") - { - input_html = ""; - } - else - { - console.warn("unknown type: " + type ); - return; - } - - var dialog = this.createDialog( "" + property + ""+input_html+"" , options ); - - if(type == "enum" && info.values) - { - var input = dialog.querySelector("select"); - input.addEventListener("change", function(e){ - setValue( e.target.value ); - //var index = e.target.value; - //setValue( e.options[e.selectedIndex].value ); - }); - } - else if(type == "boolean") - { - var input = dialog.querySelector("input"); - if(input) - { - input.addEventListener("click", function(e){ - setValue( !!input.checked ); - }); - } - } - else - { - var input = dialog.querySelector("input"); - if(input) - { - input.value = node.properties[ property ] !== undefined ? node.properties[ property ] : ""; - input.addEventListener("keydown", function(e){ - if(e.keyCode != 13) - return; - inner(); - e.preventDefault(); - e.stopPropagation(); - }); - } - } - - var button = dialog.querySelector("button"); - button.addEventListener("click", inner ); - - function inner() - { - setValue( input.value ); - } - - function setValue(value) - { - if(typeof( node.properties[ property ] ) == "number") - value = Number(value); - if(type == "array") - value = value.split(",").map(Number); - node.properties[ property ] = value; - if(node._graph) - node._graph._version++; - if(node.onPropertyChanged) - node.onPropertyChanged( property, value ); - dialog.close(); - node.setDirtyCanvas(true,true); - } -} - -LGraphCanvas.prototype.createDialog = function( html, options ) -{ - options = options || {}; - - var dialog = document.createElement("div"); - dialog.className = "graphdialog"; - dialog.innerHTML = html; - - var rect = this.canvas.getBoundingClientRect(); - var offsetx = -20; - var offsety = -20; - if(rect) - { - offsetx -= rect.left; - offsety -= rect.top; - } - - if( options.position ) - { - offsetx += options.position[0]; - offsety += options.position[1]; - } - else if( options.event ) - { - offsetx += options.event.pageX; - offsety += options.event.pageY; - } - else //centered - { - offsetx += this.canvas.width * 0.5; - offsety += this.canvas.height * 0.5; - } - - dialog.style.left = offsetx + "px"; - dialog.style.top = offsety + "px"; - - this.canvas.parentNode.appendChild( dialog ); - - dialog.close = function() - { - if(this.parentNode) - this.parentNode.removeChild( this ); - } - - return dialog; -} - -LGraphCanvas.onMenuNodeCollapse = function( value, options, e, menu, node ) -{ - node.collapse(); -} - -LGraphCanvas.onMenuNodePin = function( value, options, e, menu, node ) -{ - node.pin(); -} - -LGraphCanvas.onMenuNodeMode = function( value, options, e, menu, node ) -{ - new LiteGraph.ContextMenu(["Always","On Event","On Trigger","Never"], {event: e, callback: inner_clicked, parentMenu: menu, node: node }); - - function inner_clicked(v) - { - if(!node) - return; - switch(v) - { - case "On Event": node.mode = LiteGraph.ON_EVENT; break; - case "On Trigger": node.mode = LiteGraph.ON_TRIGGER; break; - case "Never": node.mode = LiteGraph.NEVER; break; - case "Always": - default: - node.mode = LiteGraph.ALWAYS; break; - } - } - - return false; -} - -LGraphCanvas.onMenuNodeColors = function( value, options, e, menu, node ) -{ - if(!node) - throw("no node for color"); - - var values = []; - values.push({ value:null, content:"No color" }); - - for(var i in LGraphCanvas.node_colors) - { - var color = LGraphCanvas.node_colors[i]; - var value = { value:i, content:""+i+"" }; - values.push(value); - } - new LiteGraph.ContextMenu( values, { event: e, callback: inner_clicked, parentMenu: menu, node: node }); - - function inner_clicked(v) - { - if(!node) - return; - - var color = v.value ? LGraphCanvas.node_colors[ v.value ] : null; - if(color) - { - if(node.constructor === LiteGraph.LGraphGroup) - node.color = color.groupcolor; - else - { - node.color = color.color; - node.bgcolor = color.bgcolor; - } - } - else - { - delete node.color; - delete node.bgcolor; - } - node.setDirtyCanvas(true,true); - } - - return false; -} - -LGraphCanvas.onMenuNodeShapes = function( value, options, e, menu, node ) -{ - if(!node) - throw("no node passed"); - - new LiteGraph.ContextMenu( LiteGraph.VALID_SHAPES, { event: e, callback: inner_clicked, parentMenu: menu, node: node }); - - function inner_clicked(v) - { - if(!node) - return; - node.shape = v; - node.setDirtyCanvas(true); - } - - return false; -} - -LGraphCanvas.onMenuNodeRemove = function( value, options, e, menu, node ) -{ - if(!node) - throw("no node passed"); - - if(node.removable === false) - return; - - node.graph.remove(node); - node.setDirtyCanvas(true,true); -} - -LGraphCanvas.onMenuNodeClone = function( value, options, e, menu, node ) -{ - if(node.clonable == false) return; - var newnode = node.clone(); - if(!newnode) - return; - newnode.pos = [node.pos[0]+5,node.pos[1]+5]; - node.graph.add(newnode); - node.setDirtyCanvas(true,true); -} - -LGraphCanvas.node_colors = { - "red": { color:"#322", bgcolor:"#533", groupcolor: "#A88" }, - "brown": { color:"#332922", bgcolor:"#593930", groupcolor: "#b06634" }, - "green": { color:"#232", bgcolor:"#353", groupcolor: "#8A8" }, - "blue": { color:"#223", bgcolor:"#335", groupcolor: "#88A" }, - "pale_blue": { color:"#2a363b", bgcolor:"#3f5159", groupcolor: "#3f789e" }, - "cyan": { color:"#233", bgcolor:"#355", groupcolor: "#8AA" }, - "purple": { color:"#323", bgcolor:"#535", groupcolor: "#a1309b" }, - "yellow": { color:"#432", bgcolor:"#653", groupcolor: "#b58b2a" }, - "black": { color:"#222", bgcolor:"#000", groupcolor: "#444" } -}; - -LGraphCanvas.prototype.getCanvasMenuOptions = function() -{ - var options = null; - if(this.getMenuOptions) - options = this.getMenuOptions(); - else - { - options = [ - { content:"Add Node", has_submenu: true, callback: LGraphCanvas.onMenuAdd }, - { content:"Add Group", callback: LGraphCanvas.onGroupAdd } - //{content:"Collapse All", callback: LGraphCanvas.onMenuCollapseAll } - ]; - - if(this._graph_stack && this._graph_stack.length > 0) - options = [{content:"Close subgraph", callback: this.closeSubgraph.bind(this) },null].concat(options); - } - - if(this.getExtraMenuOptions) - { - var extra = this.getExtraMenuOptions(this,options); - if(extra) - options = options.concat( extra ); - } - - return options; -} - -//called by processContextMenu to extract the menu list -LGraphCanvas.prototype.getNodeMenuOptions = function( node ) -{ - var options = null; - - if(node.getMenuOptions) - options = node.getMenuOptions(this); - else - options = [ - {content:"Inputs", has_submenu: true, disabled:true, callback: LGraphCanvas.showMenuNodeOptionalInputs }, - {content:"Outputs", has_submenu: true, disabled:true, callback: LGraphCanvas.showMenuNodeOptionalOutputs }, - null, - {content:"Properties", has_submenu: true, callback: LGraphCanvas.onShowMenuNodeProperties }, - null, - {content:"Title", callback: LGraphCanvas.onShowTitleEditor }, - {content:"Mode", has_submenu: true, callback: LGraphCanvas.onMenuNodeMode }, - {content:"Resize", callback: LGraphCanvas.onResizeNode }, - {content:"Collapse", callback: LGraphCanvas.onMenuNodeCollapse }, - {content:"Pin", callback: LGraphCanvas.onMenuNodePin }, - {content:"Colors", has_submenu: true, callback: LGraphCanvas.onMenuNodeColors }, - {content:"Shapes", has_submenu: true, callback: LGraphCanvas.onMenuNodeShapes }, - null - ]; - - if(node.getExtraMenuOptions) - { - var extra = node.getExtraMenuOptions(this); - if(extra) - { - extra.push(null); - options = extra.concat( options ); - } - } - - if( node.clonable !== false ) - options.push({content:"Clone", callback: LGraphCanvas.onMenuNodeClone }); - if( node.removable !== false ) - options.push(null,{content:"Remove", callback: LGraphCanvas.onMenuNodeRemove }); - - if(node.onGetInputs) - { - var inputs = node.onGetInputs(); - if(inputs && inputs.length) - options[0].disabled = false; - } - - if(node.onGetOutputs) - { - var outputs = node.onGetOutputs(); - if(outputs && outputs.length ) - options[1].disabled = false; - } - - if(node.graph && node.graph.onGetNodeMenuOptions ) - node.graph.onGetNodeMenuOptions( options, node ); - - return options; -} - -LGraphCanvas.prototype.getGroupMenuOptions = function( node ) -{ - var o = [ - {content:"Title", callback: LGraphCanvas.onShowTitleEditor }, - {content:"Color", has_submenu: true, callback: LGraphCanvas.onMenuNodeColors }, - null, - {content:"Remove", callback: LGraphCanvas.onMenuNodeRemove } - ]; - - return o; -} - -LGraphCanvas.prototype.processContextMenu = function( node, event ) -{ - var that = this; - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var menu_info = null; - var options = { event: event, callback: inner_option_clicked, node: node }; - - //check if mouse is in input - var slot = null; - if(node) - { - slot = node.getSlotInPosition( event.canvasX, event.canvasY ); - LGraphCanvas.active_node = node; - } - - if(slot) //on slot - { - menu_info = []; - menu_info.push( slot.locked ? "Cannot remove" : { content: "Remove Slot", slot: slot } ); - menu_info.push( { content: "Rename Slot", slot: slot } ); - options.title = (slot.input ? slot.input.type : slot.output.type) || "*"; - if(slot.input && slot.input.type == LiteGraph.ACTION) - options.title = "Action"; - if(slot.output && slot.output.type == LiteGraph.EVENT) - options.title = "Event"; - } - else - { - if( node ) //on node - menu_info = this.getNodeMenuOptions(node); - else - { - var group = this.graph.getGroupOnPos( event.canvasX, event.canvasY ); - if( group ) //on group - { - options.node = group; - menu_info = this.getGroupMenuOptions( group ); - } - else - menu_info = this.getCanvasMenuOptions(); - } - } - - //show menu - if(!menu_info) - return; - - var menu = new LiteGraph.ContextMenu( menu_info, options, ref_window ); - - function inner_option_clicked( v, options, e ) - { - if(!v) - return; - - if(v.content == "Remove Slot") - { - var info = v.slot; - if(info.input) - node.removeInput( info.slot ); - else if(info.output) - node.removeOutput( info.slot ); - return; - } - else if( v.content == "Rename Slot") - { - var info = v.slot; - var dialog = that.createDialog( "Name" , options ); - var input = dialog.querySelector("input"); - dialog.querySelector("button").addEventListener("click",function(e){ - if(input.value) - { - var slot_info = info.input ? node.getInputInfo( info.slot ) : node.getOutputInfo( info.slot ); - if( slot_info ) - slot_info.label = input.value; - that.setDirty(true); - } - dialog.close(); - }); - } - - //if(v.callback) - // return v.callback.call(that, node, options, e, menu, that, event ); - } -} - - - - - - -//API ************************************************* -//like rect but rounded corners -if(this.CanvasRenderingContext2D) -CanvasRenderingContext2D.prototype.roundRect = function (x, y, width, height, radius, radius_low) { - if ( radius === undefined ) { - radius = 5; - } - - if(radius_low === undefined) - radius_low = radius; - - this.moveTo(x + radius, y); - this.lineTo(x + width - radius, y); - this.quadraticCurveTo(x + width, y, x + width, y + radius); - - this.lineTo(x + width, y + height - radius_low); - this.quadraticCurveTo(x + width, y + height, x + width - radius_low, y + height); - this.lineTo(x + radius_low, y + height); - this.quadraticCurveTo(x, y + height, x, y + height - radius_low); - this.lineTo(x, y + radius); - this.quadraticCurveTo(x, y, x + radius, y); -} - -function compareObjects(a,b) -{ - for(var i in a) - if(a[i] != b[i]) - return false; - return true; -} -LiteGraph.compareObjects = compareObjects; - -function distance(a,b) -{ - return Math.sqrt( (b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]) ); -} -LiteGraph.distance = distance; - -function colorToString(c) -{ - return "rgba(" + Math.round(c[0] * 255).toFixed() + "," + Math.round(c[1] * 255).toFixed() + "," + Math.round(c[2] * 255).toFixed() + "," + (c.length == 4 ? c[3].toFixed(2) : "1.0") + ")"; -} -LiteGraph.colorToString = colorToString; - -function isInsideRectangle( x,y, left, top, width, height) -{ - if (left < x && (left + width) > x && - top < y && (top + height) > y) - return true; - return false; -} -LiteGraph.isInsideRectangle = isInsideRectangle; - -//[minx,miny,maxx,maxy] -function growBounding( bounding, x,y) -{ - if(x < bounding[0]) - bounding[0] = x; - else if(x > bounding[2]) - bounding[2] = x; - - if(y < bounding[1]) - bounding[1] = y; - else if(y > bounding[3]) - bounding[3] = y; -} -LiteGraph.growBounding = growBounding; - -//point inside boundin box -function isInsideBounding(p,bb) -{ - if (p[0] < bb[0][0] || - p[1] < bb[0][1] || - p[0] > bb[1][0] || - p[1] > bb[1][1]) - return false; - return true; -} -LiteGraph.isInsideBounding = isInsideBounding; - -//boundings overlap, format: [ startx, starty, width, height ] -function overlapBounding(a,b) -{ - var A_end_x = a[0] + a[2]; - var A_end_y = a[1] + a[3]; - var B_end_x = b[0] + b[2]; - var B_end_y = b[1] + b[3]; - - if ( a[0] > B_end_x || - a[1] > B_end_y || - A_end_x < b[0] || - A_end_y < b[1]) - return false; - return true; -} -LiteGraph.overlapBounding = overlapBounding; - -//Convert a hex value to its decimal value - the inputted hex must be in the -// format of a hex triplet - the kind we use for HTML colours. The function -// will return an array with three values. -function hex2num(hex) { - if(hex.charAt(0) == "#") hex = hex.slice(1); //Remove the '#' char - if there is one. - hex = hex.toUpperCase(); - var hex_alphabets = "0123456789ABCDEF"; - var value = new Array(3); - var k = 0; - var int1,int2; - for(var i=0;i<6;i+=2) { - int1 = hex_alphabets.indexOf(hex.charAt(i)); - int2 = hex_alphabets.indexOf(hex.charAt(i+1)); - value[k] = (int1 * 16) + int2; - k++; - } - return(value); -} - -LiteGraph.hex2num = hex2num; - -//Give a array with three values as the argument and the function will return -// the corresponding hex triplet. -function num2hex(triplet) { - var hex_alphabets = "0123456789ABCDEF"; - var hex = "#"; - var int1,int2; - for(var i=0;i<3;i++) { - int1 = triplet[i] / 16; - int2 = triplet[i] % 16; - - hex += hex_alphabets.charAt(int1) + hex_alphabets.charAt(int2); - } - return(hex); -} - -LiteGraph.num2hex = num2hex; - -/* LiteGraph GUI elements used for canvas editing *************************************/ - -/** -* ContextMenu from LiteGUI -* -* @class ContextMenu -* @constructor -* @param {Array} values (allows object { title: "Nice text", callback: function ... }) -* @param {Object} options [optional] Some options:\ -* - title: title to show on top of the menu -* - callback: function to call when an option is clicked, it receives the item information -* - ignore_item_callbacks: ignores the callback inside the item, it just calls the options.callback -* - event: you can pass a MouseEvent, this way the ContextMenu appears in that position -*/ -function ContextMenu( values, options ) -{ - options = options || {}; - this.options = options; - var that = this; - - //to link a menu with its parent - if(options.parentMenu) - { - if( options.parentMenu.constructor !== this.constructor ) - { - console.error("parentMenu must be of class ContextMenu, ignoring it"); - options.parentMenu = null; - } - else - { - this.parentMenu = options.parentMenu; - this.parentMenu.lock = true; - this.parentMenu.current_submenu = this; - } - } - - if(options.event && options.event.constructor !== MouseEvent && options.event.constructor !== CustomEvent) - { - console.error("Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it."); - options.event = null; - } - - var root = document.createElement("div"); - root.className = "litegraph litecontextmenu litemenubar-panel"; - root.style.minWidth = 100; - root.style.minHeight = 100; - root.style.pointerEvents = "none"; - setTimeout( function() { root.style.pointerEvents = "auto"; },100); //delay so the mouse up event is not caugh by this element - - //this prevents the default context browser menu to open in case this menu was created when pressing right button - root.addEventListener("mouseup", function(e){ - e.preventDefault(); return true; - }, true); - root.addEventListener("contextmenu", function(e) { - if(e.button != 2) //right button - return false; - e.preventDefault(); - return false; - },true); - - root.addEventListener("mousedown", function(e){ - if(e.button == 2) - { - that.close(); - e.preventDefault(); return true; - } - }, true); - - function on_mouse_wheel(e) - { - var pos = parseInt( root.style.top ); - root.style.top = (pos + e.deltaY * 0.1).toFixed() + "px"; - e.preventDefault(); - return true; - } - - root.addEventListener("wheel", on_mouse_wheel, true); - root.addEventListener("mousewheel", on_mouse_wheel, true); - - - this.root = root; - - //title - if(options.title) - { - var element = document.createElement("div"); - element.className = "litemenu-title"; - element.innerHTML = options.title; - root.appendChild(element); - } - - //entries - var num = 0; - for(var i in values) - { - var name = values.constructor == Array ? values[i] : i; - if( name != null && name.constructor !== String ) - name = name.content === undefined ? String(name) : name.content; - var value = values[i]; - this.addItem( name, value, options ); - num++; - } - - //close on leave - root.addEventListener("mouseleave", function(e) { - if(that.lock) - return; - that.close(e); - }); - - //insert before checking position - var root_document = document; - if(options.event) - root_document = options.event.target.ownerDocument; - - if(!root_document) - root_document = document; - root_document.body.appendChild(root); - - //compute best position - var left = options.left || 0; - var top = options.top || 0; - if(options.event) - { - left = (options.event.pageX - 10); - top = (options.event.pageY - 10); - if(options.title) - top -= 20; - - if(options.parentMenu) - { - var rect = options.parentMenu.root.getBoundingClientRect(); - left = rect.left + rect.width; - } - - var body_rect = document.body.getBoundingClientRect(); - var root_rect = root.getBoundingClientRect(); - - if(left > (body_rect.width - root_rect.width - 10)) - left = (body_rect.width - root_rect.width - 10); - if(top > (body_rect.height - root_rect.height - 10)) - top = (body_rect.height - root_rect.height - 10); - } - - root.style.left = left + "px"; - root.style.top = top + "px"; -} - -ContextMenu.prototype.addItem = function( name, value, options ) -{ - var that = this; - options = options || {}; - - var element = document.createElement("div"); - element.className = "litemenu-entry submenu"; - - var disabled = false; - - if(value === null) - { - element.classList.add("separator"); - //element.innerHTML = "
" - //continue; - } - else - { - element.innerHTML = value && value.title ? value.title : name; - element.value = value; - - if(value) - { - if(value.disabled) - { - disabled = true; - element.classList.add("disabled"); - } - if(value.submenu || value.has_submenu) - element.classList.add("has_submenu"); - } - - if(typeof(value) == "function") - { - element.dataset["value"] = name; - element.onclick_callback = value; - } - else - element.dataset["value"] = value; - - if(value.className) - element.className += " " + value.className; - } - - this.root.appendChild(element); - if(!disabled) - element.addEventListener("click", inner_onclick); - if(options.autoopen) - element.addEventListener("mouseenter", inner_over); - - function inner_over(e) - { - var value = this.value; - if(!value || !value.has_submenu) - return; - inner_onclick.call(this,e); - } - - //menu option clicked - function inner_onclick(e) { - var value = this.value; - var close_parent = true; - - if(that.current_submenu) - that.current_submenu.close(e); - - //global callback - if(options.callback) - { - var r = options.callback.call( this, value, options, e, that, options.node ); - if(r === true) - close_parent = false; - } - - //special cases - if(value) - { - if (value.callback && !options.ignore_item_callbacks && value.disabled !== true ) //item callback - { - var r = value.callback.call( this, value, options, e, that, options.node ); - if(r === true) - close_parent = false; - } - if(value.submenu) - { - if(!value.submenu.options) - throw("ContextMenu submenu needs options"); - var submenu = new that.constructor( value.submenu.options, { - callback: value.submenu.callback, - event: e, - parentMenu: that, - ignore_item_callbacks: value.submenu.ignore_item_callbacks, - title: value.submenu.title, - autoopen: options.autoopen - }); - close_parent = false; - } - } - - if(close_parent && !that.lock) - that.close(); - } - - return element; -} - -ContextMenu.prototype.close = function(e, ignore_parent_menu) -{ - if(this.root.parentNode) - this.root.parentNode.removeChild( this.root ); - if(this.parentMenu && !ignore_parent_menu) - { - this.parentMenu.lock = false; - this.parentMenu.current_submenu = null; - if( e === undefined ) - this.parentMenu.close(); - else if( e && !ContextMenu.isCursorOverElement( e, this.parentMenu.root) ) - { - ContextMenu.trigger( this.parentMenu.root, "mouseleave", e ); - } - } - if(this.current_submenu) - this.current_submenu.close(e, true); -} - -//this code is used to trigger events easily (used in the context menu mouseleave -ContextMenu.trigger = function(element, event_name, params, origin) -{ - var evt = document.createEvent( 'CustomEvent' ); - evt.initCustomEvent( event_name, true,true, params ); //canBubble, cancelable, detail - evt.srcElement = origin; - if( element.dispatchEvent ) - element.dispatchEvent( evt ); - else if( element.__events ) - element.__events.dispatchEvent( evt ); - //else nothing seems binded here so nothing to do - return evt; -} - -//returns the top most menu -ContextMenu.prototype.getTopMenu = function() -{ - if( this.options.parentMenu ) - return this.options.parentMenu.getTopMenu(); - return this; -} - -ContextMenu.prototype.getFirstEvent = function() -{ - if( this.options.parentMenu ) - return this.options.parentMenu.getFirstEvent(); - return this.options.event; -} - - - -ContextMenu.isCursorOverElement = function( event, element ) -{ - var left = event.pageX; - var top = event.pageY; - var rect = element.getBoundingClientRect(); - if(!rect) - return false; - if(top > rect.top && top < (rect.top + rect.height) && - left > rect.left && left < (rect.left + rect.width) ) - return true; - return false; -} - - - -LiteGraph.ContextMenu = ContextMenu; - -LiteGraph.closeAllContextMenus = function( ref_window ) -{ - ref_window = ref_window || window; - - var elements = ref_window.document.querySelectorAll(".litecontextmenu"); - if(!elements.length) - return; - - var result = []; - for(var i = 0; i < elements.length; i++) - result.push(elements[i]); - - for(var i in result) - { - if(result[i].close) - result[i].close(); - else if(result[i].parentNode) - result[i].parentNode.removeChild( result[i] ); - } -} - -LiteGraph.extendClass = function ( target, origin ) -{ - for(var i in origin) //copy class properties - { - if(target.hasOwnProperty(i)) - continue; - target[i] = origin[i]; - } - - if(origin.prototype) //copy prototype properties - for(var i in origin.prototype) //only enumerables - { - if(!origin.prototype.hasOwnProperty(i)) - continue; - - if(target.prototype.hasOwnProperty(i)) //avoid overwritting existing ones - continue; - - //copy getters - if(origin.prototype.__lookupGetter__(i)) - target.prototype.__defineGetter__(i, origin.prototype.__lookupGetter__(i)); - else - target.prototype[i] = origin.prototype[i]; - - //and setters - if(origin.prototype.__lookupSetter__(i)) - target.prototype.__defineSetter__(i, origin.prototype.__lookupSetter__(i)); - } -} - -//used to create nodes from wrapping functions -LiteGraph.getParameterNames = function(func) { - return (func + '') - .replace(/[/][/].*$/mg,'') // strip single-line comments - .replace(/\s+/g, '') // strip white space - .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments /**/ - .split('){', 1)[0].replace(/^[^(]*[(]/, '') // extract the parameters - .replace(/=[^,]+/g, '') // strip any ES6 defaults - .split(',').filter(Boolean); // split & filter [""] -} - -Math.clamp = function(v,a,b) { return (a > v ? a : (b < v ? b : v)); } - -if( typeof(window) != "undefined" && !window["requestAnimationFrame"] ) -{ - window.requestAnimationFrame = window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - (function( callback ){ - window.setTimeout(callback, 1000 / 60); - }); -} - -})(this); - -if(typeof(exports) != "undefined") - exports.LiteGraph = this.LiteGraph; +(function(global){ +// ************************************************************* +// LiteGraph CLASS ******* +// ************************************************************* + +/* FYI: links are stored in graph.links with this structure per object +{ + id: number + type: string, + origin_id: number, + origin_slot: number, + target_id: number, + target_slot: number, + data: * +}; +*/ + +/** +* The Global Scope. It contains all the registered node classes. +* +* @class LiteGraph +* @constructor +*/ + +var LiteGraph = global.LiteGraph = { + + NODE_TITLE_HEIGHT: 20, + NODE_SLOT_HEIGHT: 15, + NODE_WIDGET_HEIGHT: 20, + NODE_WIDTH: 140, + NODE_MIN_WIDTH: 50, + NODE_COLLAPSED_RADIUS: 10, + NODE_COLLAPSED_WIDTH: 80, + CANVAS_GRID_SIZE: 10, + NODE_TITLE_COLOR: "#999", + NODE_TEXT_SIZE: 14, + NODE_TEXT_COLOR: "#AAA", + NODE_SUBTEXT_SIZE: 12, + NODE_DEFAULT_COLOR: "#333", + NODE_DEFAULT_BGCOLOR: "#444", + NODE_DEFAULT_BOXCOLOR: "#888", + NODE_DEFAULT_SHAPE: "box", + MAX_NUMBER_OF_NODES: 1000, //avoid infinite loops + DEFAULT_POSITION: [100,100],//default node position + node_images_path: "", + + VALID_SHAPES: ["default","box","round","card"], //,"circle" + + //shapes are used for nodes but also for slots + BOX_SHAPE: 1, + ROUND_SHAPE: 2, + CIRCLE_SHAPE: 3, + CARD_SHAPE: 4, + ARROW_SHAPE: 5, + + //enums + INPUT: 1, + OUTPUT: 2, + + EVENT: -1, //for outputs + ACTION: -1, //for inputs + + ALWAYS: 0, + ON_EVENT: 1, + NEVER: 2, + ON_TRIGGER: 3, + + NORMAL_TITLE: 0, + NO_TITLE: 1, + TRANSPARENT_TITLE: 2, + AUTOHIDE_TITLE: 3, + + proxy: null, //used to redirect calls + + debug: false, + throw_errors: true, + allow_scripts: true, + registered_node_types: {}, //nodetypes by string + node_types_by_file_extension: {}, //used for droping files in the canvas + Nodes: {}, //node types by classname + + /** + * Register a node class so it can be listed when the user wants to create a new one + * @method registerNodeType + * @param {String} type name of the node and path + * @param {Class} base_class class containing the structure of a node + */ + + registerNodeType: function(type, base_class) + { + if(!base_class.prototype) + throw("Cannot register a simple object, it must be a class with a prototype"); + base_class.type = type; + + if(LiteGraph.debug) + console.log("Node registered: " + type); + + var categories = type.split("/"); + var classname = base_class.name; + + var pos = type.lastIndexOf("/"); + base_class.category = type.substr(0,pos); + + if(!base_class.title) + base_class.title = classname; + //info.name = name.substr(pos+1,name.length - pos); + + //extend class + if(base_class.prototype) //is a class + for(var i in LGraphNode.prototype) + if(!base_class.prototype[i]) + base_class.prototype[i] = LGraphNode.prototype[i]; + + Object.defineProperty( base_class.prototype, "shape",{ + set: function(v) { + switch(v) + { + case "default": delete this._shape; break; + case "box": this._shape = LiteGraph.BOX_SHAPE; break; + case "round": this._shape = LiteGraph.ROUND_SHAPE; break; + case "circle": this._shape = LiteGraph.CIRCLE_SHAPE; break; + case "card": this._shape = LiteGraph.CARD_SHAPE; break; + default: + this._shape = v; + } + }, + get: function(v) + { + return this._shape; + }, + enumerable: true + }); + + this.registered_node_types[ type ] = base_class; + if(base_class.constructor.name) + this.Nodes[ classname ] = base_class; + + //warnings + if(base_class.prototype.onPropertyChange) + console.warn("LiteGraph node class " + type + " has onPropertyChange method, it must be called onPropertyChanged with d at the end"); + + if( base_class.supported_extensions ) + { + for(var i in base_class.supported_extensions ) + this.node_types_by_file_extension[ base_class.supported_extensions[i].toLowerCase() ] = base_class; + } + }, + + /** + * Create a new node type by passing a function, it wraps it with a propper class and generates inputs according to the parameters of the function. + * Useful to wrap simple methods that do not require properties, and that only process some input to generate an output. + * @method wrapFunctionAsNode + * @param {String} name node name with namespace (p.e.: 'math/sum') + * @param {Function} func + * @param {Array} param_types [optional] an array containing the type of every parameter, otherwise parameters will accept any type + * @param {String} return_type [optional] string with the return type, otherwise it will be generic + */ + wrapFunctionAsNode: function( name, func, param_types, return_type ) + { + var params = Array(func.length); + var code = ""; + var names = LiteGraph.getParameterNames( func ); + for(var i = 0; i < names.length; ++i) + code += "this.addInput('"+names[i]+"',"+(param_types && param_types[i] ? "'" + param_types[i] + "'" : "0") + ");\n"; + code += "this.addOutput('out',"+( return_type ? "'" + return_type + "'" : 0 )+");\n"; + var classobj = Function(code); + classobj.title = name.split("/").pop(); + classobj.desc = "Generated from " + func.name; + classobj.prototype.onExecute = function onExecute() + { + for(var i = 0; i < params.length; ++i) + params[i] = this.getInputData(i); + var r = func.apply( this, params ); + this.setOutputData(0,r); + } + this.registerNodeType( name, classobj ); + }, + + /** + * Adds this method to all nodetypes, existing and to be created + * (You can add it to LGraphNode.prototype but then existing node types wont have it) + * @method addNodeMethod + * @param {Function} func + */ + addNodeMethod: function( name, func ) + { + LGraphNode.prototype[name] = func; + for(var i in this.registered_node_types) + { + var type = this.registered_node_types[i]; + if(type.prototype[name]) + type.prototype["_" + name] = type.prototype[name]; //keep old in case of replacing + type.prototype[name] = func; + } + }, + + /** + * Create a node of a given type with a name. The node is not attached to any graph yet. + * @method createNode + * @param {String} type full name of the node class. p.e. "math/sin" + * @param {String} name a name to distinguish from other nodes + * @param {Object} options to set options + */ + + createNode: function( type, title, options ) + { + var base_class = this.registered_node_types[type]; + if (!base_class) + { + if(LiteGraph.debug) + console.log("GraphNode type \"" + type + "\" not registered."); + return null; + } + + var prototype = base_class.prototype || base_class; + + title = title || base_class.title || type; + + var node = new base_class( title ); + node.type = type; + + if(!node.title && title) node.title = title; + if(!node.properties) node.properties = {}; + if(!node.properties_info) node.properties_info = []; + if(!node.flags) node.flags = {}; + if(!node.size) node.size = node.computeSize(); + if(!node.pos) node.pos = LiteGraph.DEFAULT_POSITION.concat(); + if(!node.mode) node.mode = LiteGraph.ALWAYS; + + //extra options + if(options) + { + for(var i in options) + node[i] = options[i]; + } + + return node; + }, + + /** + * Returns a registered node type with a given name + * @method getNodeType + * @param {String} type full name of the node class. p.e. "math/sin" + * @return {Class} the node class + */ + + getNodeType: function(type) + { + return this.registered_node_types[type]; + }, + + + /** + * Returns a list of node types matching one category + * @method getNodeType + * @param {String} category category name + * @return {Array} array with all the node classes + */ + + getNodeTypesInCategory: function( category, filter ) + { + var r = []; + for(var i in this.registered_node_types) + { + var type = this.registered_node_types[i]; + if(filter && type.filter && type.filter != filter) + continue; + + if(category == "" ) + { + if (type.category == null) + r.push(type); + } + else if (type.category == category) + r.push(type); + } + + return r; + }, + + /** + * Returns a list with all the node type categories + * @method getNodeTypesCategories + * @return {Array} array with all the names of the categories + */ + + getNodeTypesCategories: function() + { + var categories = {"":1}; + for(var i in this.registered_node_types) + if(this.registered_node_types[i].category && !this.registered_node_types[i].skip_list) + categories[ this.registered_node_types[i].category ] = 1; + var result = []; + for(var i in categories) + result.push(i); + return result; + }, + + //debug purposes: reloads all the js scripts that matches a wilcard + reloadNodes: function (folder_wildcard) + { + var tmp = document.getElementsByTagName("script"); + //weird, this array changes by its own, so we use a copy + var script_files = []; + for(var i in tmp) + script_files.push(tmp[i]); + + + var docHeadObj = document.getElementsByTagName("head")[0]; + folder_wildcard = document.location.href + folder_wildcard; + + for(var i in script_files) + { + var src = script_files[i].src; + if( !src || src.substr(0,folder_wildcard.length ) != folder_wildcard) + continue; + + try + { + if(LiteGraph.debug) + console.log("Reloading: " + src); + var dynamicScript = document.createElement("script"); + dynamicScript.type = "text/javascript"; + dynamicScript.src = src; + docHeadObj.appendChild(dynamicScript); + docHeadObj.removeChild(script_files[i]); + } + catch (err) + { + if(LiteGraph.throw_errors) + throw err; + if(LiteGraph.debug) + console.log("Error while reloading " + src); + } + } + + if(LiteGraph.debug) + console.log("Nodes reloaded"); + }, + + //separated just to improve if it doesnt work + cloneObject: function(obj, target) + { + if(obj == null) return null; + var r = JSON.parse( JSON.stringify( obj ) ); + if(!target) return r; + + for(var i in r) + target[i] = r[i]; + return target; + }, + + isValidConnection: function( type_a, type_b ) + { + if( !type_a || //generic output + !type_b || //generic input + type_a == type_b || //same type (is valid for triggers) + type_a == LiteGraph.EVENT && type_b == LiteGraph.ACTION ) + return true; + + // Enforce string type to handle toLowerCase call (-1 number not ok) + type_a = String(type_a); + type_b = String(type_b); + type_a = type_a.toLowerCase(); + type_b = type_b.toLowerCase(); + + // For nodes supporting multiple connection types + if( type_a.indexOf(",") == -1 && type_b.indexOf(",") == -1 ) + return type_a == type_b; + + // Check all permutations to see if one is valid + var supported_types_a = type_a.split(","); + var supported_types_b = type_b.split(","); + for(var i = 0; i < supported_types_a.length; ++i) + for(var j = 0; j < supported_types_b.length; ++j) + if( supported_types_a[i] == supported_types_b[j] ) + return true; + + return false; + } +}; + +//timer that works everywhere +if(typeof(performance) != "undefined") + LiteGraph.getTime = performance.now.bind(performance); +else if(typeof(Date) != "undefined" && Date.now) + LiteGraph.getTime = Date.now.bind(Date); +else if(typeof(process) != "undefined") + LiteGraph.getTime = function(){ + var t = process.hrtime(); + return t[0]*0.001 + t[1]*(1e-6); + } +else + LiteGraph.getTime = function getTime() { return (new Date).getTime(); } + + + + + + +//********************************************************************************* +// LGraph CLASS +//********************************************************************************* + +/** +* LGraph is the class that contain a full graph. We instantiate one and add nodes to it, and then we can run the execution loop. +* +* @class LGraph +* @constructor +* @param {Object} o data from previous serialization [optional] +*/ + +function LGraph( o ) +{ + if (LiteGraph.debug) + console.log("Graph created"); + this.list_of_graphcanvas = null; + this.clear(); + + if(o) + this.configure(o); +} + +global.LGraph = LiteGraph.LGraph = LGraph; + +//default supported types +LGraph.supported_types = ["number","string","boolean"]; + +//used to know which types of connections support this graph (some graphs do not allow certain types) +LGraph.prototype.getSupportedTypes = function() { return this.supported_types || LGraph.supported_types; } + +LGraph.STATUS_STOPPED = 1; +LGraph.STATUS_RUNNING = 2; + +/** +* Removes all nodes from this graph +* @method clear +*/ + +LGraph.prototype.clear = function() +{ + this.stop(); + this.status = LGraph.STATUS_STOPPED; + + this.last_node_id = 1; + this.last_link_id = 1; + + this._version = -1; //used to detect changes + + //nodes + this._nodes = []; + this._nodes_by_id = {}; + this._nodes_in_order = []; //nodes that are executable sorted in execution order + this._nodes_executable = null; //nodes that contain onExecute + + //other scene stuff + this._groups = []; + + //links + this.links = {}; //container with all the links + + //iterations + this.iteration = 0; + + //custom data + this.config = {}; + + //timing + this.globaltime = 0; + this.runningtime = 0; + this.fixedtime = 0; + this.fixedtime_lapse = 0.01; + this.elapsed_time = 0.01; + this.last_update_time = 0; + this.starttime = 0; + + this.catch_errors = true; + + //subgraph_data + this.global_inputs = {}; + this.global_outputs = {}; + + //notify canvas to redraw + this.change(); + + this.sendActionToCanvas("clear"); +} + +/** +* Attach Canvas to this graph +* @method attachCanvas +* @param {GraphCanvas} graph_canvas +*/ + +LGraph.prototype.attachCanvas = function(graphcanvas) +{ + if(graphcanvas.constructor != LGraphCanvas) + throw("attachCanvas expects a LGraphCanvas instance"); + if(graphcanvas.graph && graphcanvas.graph != this) + graphcanvas.graph.detachCanvas( graphcanvas ); + + graphcanvas.graph = this; + if(!this.list_of_graphcanvas) + this.list_of_graphcanvas = []; + this.list_of_graphcanvas.push(graphcanvas); +} + +/** +* Detach Canvas from this graph +* @method detachCanvas +* @param {GraphCanvas} graph_canvas +*/ +LGraph.prototype.detachCanvas = function(graphcanvas) +{ + if(!this.list_of_graphcanvas) + return; + + var pos = this.list_of_graphcanvas.indexOf( graphcanvas ); + if(pos == -1) + return; + graphcanvas.graph = null; + this.list_of_graphcanvas.splice(pos,1); +} + +/** +* Starts running this graph every interval milliseconds. +* @method start +* @param {number} interval amount of milliseconds between executions, default is 1 +*/ + +LGraph.prototype.start = function(interval) +{ + if( this.status == LGraph.STATUS_RUNNING ) + return; + this.status = LGraph.STATUS_RUNNING; + + if(this.onPlayEvent) + this.onPlayEvent(); + + this.sendEventToAllNodes("onStart"); + + //launch + this.starttime = LiteGraph.getTime(); + this.last_update_time = this.starttime; + interval = interval || 1; + var that = this; + + this.execution_timer_id = setInterval( function() { + //execute + that.runStep(1, !this.catch_errors ); + },interval); +} + +/** +* Stops the execution loop of the graph +* @method stop execution +*/ + +LGraph.prototype.stop = function() +{ + if( this.status == LGraph.STATUS_STOPPED ) + return; + + this.status = LGraph.STATUS_STOPPED; + + if(this.onStopEvent) + this.onStopEvent(); + + if(this.execution_timer_id != null) + clearInterval(this.execution_timer_id); + this.execution_timer_id = null; + + this.sendEventToAllNodes("onStop"); +} + +/** +* Run N steps (cycles) of the graph +* @method runStep +* @param {number} num number of steps to run, default is 1 +*/ + +LGraph.prototype.runStep = function( num, do_not_catch_errors ) +{ + num = num || 1; + + var start = LiteGraph.getTime(); + this.globaltime = 0.001 * (start - this.starttime); + + var nodes = this._nodes_executable ? this._nodes_executable : this._nodes; + if(!nodes) + return; + + if( do_not_catch_errors ) + { + //iterations + for(var i = 0; i < num; i++) + { + for( var j = 0, l = nodes.length; j < l; ++j ) + { + var node = nodes[j]; + if( node.mode == LiteGraph.ALWAYS && node.onExecute ) + node.onExecute(); + } + + this.fixedtime += this.fixedtime_lapse; + if( this.onExecuteStep ) + this.onExecuteStep(); + } + + if( this.onAfterExecute ) + this.onAfterExecute(); + } + else + { + try + { + //iterations + for(var i = 0; i < num; i++) + { + for( var j = 0, l = nodes.length; j < l; ++j ) + { + var node = nodes[j]; + if( node.mode == LiteGraph.ALWAYS && node.onExecute ) + node.onExecute(); + } + + this.fixedtime += this.fixedtime_lapse; + if( this.onExecuteStep ) + this.onExecuteStep(); + } + + if( this.onAfterExecute ) + this.onAfterExecute(); + this.errors_in_execution = false; + } + catch (err) + { + this.errors_in_execution = true; + if(LiteGraph.throw_errors) + throw err; + if(LiteGraph.debug) + console.log("Error during execution: " + err); + this.stop(); + } + } + + var now = LiteGraph.getTime(); + var elapsed = now - start; + if (elapsed == 0) + elapsed = 1; + this.execution_time = 0.001 * elapsed; + this.globaltime += 0.001 * elapsed; + this.iteration += 1; + this.elapsed_time = (now - this.last_update_time) * 0.001; + this.last_update_time = now; +} + +/** +* Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than +* nodes with only inputs. +* @method updateExecutionOrder +*/ +LGraph.prototype.updateExecutionOrder = function() +{ + this._nodes_in_order = this.computeExecutionOrder( false ); + this._nodes_executable = []; + for(var i = 0; i < this._nodes_in_order.length; ++i) + if( this._nodes_in_order[i].onExecute ) + this._nodes_executable.push( this._nodes_in_order[i] ); +} + +//This is more internal, it computes the order and returns it +LGraph.prototype.computeExecutionOrder = function( only_onExecute, set_level ) +{ + var L = []; + var S = []; + var M = {}; + var visited_links = {}; //to avoid repeating links + var remaining_links = {}; //to a + + //search for the nodes without inputs (starting nodes) + for (var i = 0, l = this._nodes.length; i < l; ++i) + { + var node = this._nodes[i]; + if( only_onExecute && !node.onExecute ) + continue; + + M[node.id] = node; //add to pending nodes + + var num = 0; //num of input connections + if(node.inputs) + for(var j = 0, l2 = node.inputs.length; j < l2; j++) + if(node.inputs[j] && node.inputs[j].link != null) + num += 1; + + if(num == 0) //is a starting node + { + S.push(node); + if(set_level) + node._level = 1; + } + else //num of input links + { + if(set_level) + node._level = 0; + remaining_links[node.id] = num; + } + } + + while(true) + { + if(S.length == 0) + break; + + //get an starting node + var node = S.shift(); + L.push(node); //add to ordered list + delete M[node.id]; //remove from the pending nodes + + if(!node.outputs) + continue; + + //for every output + for(var i = 0; i < node.outputs.length; i++) + { + var output = node.outputs[i]; + //not connected + if(output == null || output.links == null || output.links.length == 0) + continue; + + //for every connection + for(var j = 0; j < output.links.length; j++) + { + var link_id = output.links[j]; + var link = this.links[link_id]; + if(!link) + continue; + + //already visited link (ignore it) + if(visited_links[ link.id ]) + continue; + + var target_node = this.getNodeById( link.target_id ); + if(target_node == null) + { + visited_links[ link.id ] = true; + continue; + } + + if(set_level && (!target_node._level || target_node._level <= node._level)) + target_node._level = node._level + 1; + + visited_links[link.id] = true; //mark as visited + remaining_links[target_node.id] -= 1; //reduce the number of links remaining + if (remaining_links[ target_node.id ] == 0) + S.push(target_node); //if no more links, then add to starters array + } + } + } + + //the remaining ones (loops) + for(var i in M) + L.push( M[i] ); + + if( L.length != this._nodes.length && LiteGraph.debug ) + console.warn("something went wrong, nodes missing"); + + var l = L.length; + + //save order number in the node + for(var i = 0; i < l; ++i) + L[i].order = i; + + //sort now by priority + L = L.sort(function(A,B){ + var Ap = A.constructor.priority || A.priority || 0; + var Bp = B.constructor.priority || B.priority || 0; + if(Ap == Bp) + return A.order - B.order; + return Ap - Bp; + }); + + //save order number in the node, again... + for(var i = 0; i < l; ++i) + L[i].order = i; + + return L; +} + +/** +* Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively. +* It doesnt include the node itself +* @method getAncestors +* @return {Array} an array with all the LGraphNodes that affect this node, in order of execution +*/ +LGraph.prototype.getAncestors = function( node ) +{ + var ancestors = []; + var pending = [node]; + var visited = {}; + + while (pending.length) + { + var current = pending.shift(); + if(!current.inputs) + continue; + if( !visited[ current.id ] && current != node ) + { + visited[ current.id ] = true; + ancestors.push( current ); + } + + for(var i = 0; i < current.inputs.length;++i) + { + var input = current.getInputNode(i); + if( input && ancestors.indexOf( input ) == -1) + { + pending.push( input ); + } + } + } + + ancestors.sort(function(a,b){ return a.order - b.order;}); + return ancestors; +} + +/** +* Positions every node in a more readable manner +* @method arrange +*/ +LGraph.prototype.arrange = function( margin ) +{ + margin = margin || 40; + + var nodes = this.computeExecutionOrder( false, true ); + var columns = []; + for(var i = 0; i < nodes.length; ++i) + { + var node = nodes[i]; + var col = node._level || 1; + if(!columns[col]) + columns[col] = []; + columns[col].push( node ); + } + + var x = margin; + + for(var i = 0; i < columns.length; ++i) + { + var column = columns[i]; + if(!column) + continue; + var max_size = 100; + var y = margin; + for(var j = 0; j < column.length; ++j) + { + var node = column[j]; + node.pos[0] = x; + node.pos[1] = y; + if(node.size[0] > max_size) + max_size = node.size[0]; + y += node.size[1] + margin; + } + x += max_size + margin; + } + + this.setDirtyCanvas(true,true); +} + + +/** +* Returns the amount of time the graph has been running in milliseconds +* @method getTime +* @return {number} number of milliseconds the graph has been running +*/ +LGraph.prototype.getTime = function() +{ + return this.globaltime; +} + +/** +* Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant +* @method getFixedTime +* @return {number} number of milliseconds the graph has been running +*/ + +LGraph.prototype.getFixedTime = function() +{ + return this.fixedtime; +} + +/** +* Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct +* if the nodes are using graphical actions +* @method getElapsedTime +* @return {number} number of milliseconds it took the last cycle +*/ + +LGraph.prototype.getElapsedTime = function() +{ + return this.elapsed_time; +} + +/** +* Sends an event to all the nodes, useful to trigger stuff +* @method sendEventToAllNodes +* @param {String} eventname the name of the event (function to be called) +* @param {Array} params parameters in array format +*/ + +LGraph.prototype.sendEventToAllNodes = function( eventname, params, mode ) +{ + mode = mode || LiteGraph.ALWAYS; + + var nodes = this._nodes_in_order ? this._nodes_in_order : this._nodes; + if(!nodes) + return; + + for( var j = 0, l = nodes.length; j < l; ++j ) + { + var node = nodes[j]; + if(node[eventname] && node.mode == mode ) + { + if(params === undefined) + node[eventname](); + else if(params && params.constructor === Array) + node[eventname].apply( node, params ); + else + node[eventname](params); + } + } +} + +LGraph.prototype.sendActionToCanvas = function(action, params) +{ + if(!this.list_of_graphcanvas) + return; + + for(var i = 0; i < this.list_of_graphcanvas.length; ++i) + { + var c = this.list_of_graphcanvas[i]; + if( c[action] ) + c[action].apply(c, params); + } +} + +/** +* Adds a new node instasnce to this graph +* @method add +* @param {LGraphNode} node the instance of the node +*/ + +LGraph.prototype.add = function( node, skip_compute_order) +{ + if(!node) + return; + + //groups + if( node.constructor === LGraphGroup ) + { + this._groups.push( node ); + this.setDirtyCanvas(true); + this.change(); + node.graph = this; + this._version++; + return; + } + + //nodes + if(node.id != -1 && this._nodes_by_id[node.id] != null) + { + console.warn("LiteGraph: there is already a node with this ID, changing it"); + node.id = ++this.last_node_id; + } + + if(this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) + throw("LiteGraph: max number of nodes in a graph reached"); + + //give him an id + if(node.id == null || node.id == -1) + node.id = ++this.last_node_id; + else if (this.last_node_id < node.id) + this.last_node_id = node.id; + + + node.graph = this; + this._version++; + + this._nodes.push(node); + this._nodes_by_id[node.id] = node; + + if(node.onAdded) + node.onAdded( this ); + + if(this.config.align_to_grid) + node.alignToGrid(); + + if(!skip_compute_order) + this.updateExecutionOrder(); + + if(this.onNodeAdded) + this.onNodeAdded(node); + + + this.setDirtyCanvas(true); + this.change(); + + return node; //to chain actions +} + +/** +* Removes a node from the graph +* @method remove +* @param {LGraphNode} node the instance of the node +*/ + +LGraph.prototype.remove = function(node) +{ + if(node.constructor === LiteGraph.LGraphGroup) + { + var index = this._groups.indexOf(node); + if(index != -1) + this._groups.splice(index,1); + node.graph = null; + this._version++; + this.setDirtyCanvas(true,true); + this.change(); + return; + } + + if(this._nodes_by_id[node.id] == null) + return; //not found + + if(node.ignore_remove) + return; //cannot be removed + + //disconnect inputs + if(node.inputs) + for(var i = 0; i < node.inputs.length; i++) + { + var slot = node.inputs[i]; + if(slot.link != null) + node.disconnectInput(i); + } + + //disconnect outputs + if(node.outputs) + for(var i = 0; i < node.outputs.length; i++) + { + var slot = node.outputs[i]; + if(slot.links != null && slot.links.length) + node.disconnectOutput(i); + } + + //node.id = -1; //why? + + //callback + if(node.onRemoved) + node.onRemoved(); + + node.graph = null; + this._version++; + + //remove from canvas render + if(this.list_of_graphcanvas) + { + for(var i = 0; i < this.list_of_graphcanvas.length; ++i) + { + var canvas = this.list_of_graphcanvas[i]; + if(canvas.selected_nodes[node.id]) + delete canvas.selected_nodes[node.id]; + if(canvas.node_dragged == node) + canvas.node_dragged = null; + } + } + + //remove from containers + var pos = this._nodes.indexOf(node); + if(pos != -1) + this._nodes.splice(pos,1); + delete this._nodes_by_id[node.id]; + + if(this.onNodeRemoved) + this.onNodeRemoved(node); + + this.setDirtyCanvas(true,true); + this.change(); + + this.updateExecutionOrder(); +} + +/** +* Returns a node by its id. +* @method getNodeById +* @param {Number} id +*/ + +LGraph.prototype.getNodeById = function( id ) +{ + if( id == null ) + return null; + return this._nodes_by_id[ id ]; +} + +/** +* Returns a list of nodes that matches a class +* @method findNodesByClass +* @param {Class} classObject the class itself (not an string) +* @return {Array} a list with all the nodes of this type +*/ + +LGraph.prototype.findNodesByClass = function(classObject) +{ + var r = []; + for(var i = 0, l = this._nodes.length; i < l; ++i) + if(this._nodes[i].constructor === classObject) + r.push(this._nodes[i]); + return r; +} + +/** +* Returns a list of nodes that matches a type +* @method findNodesByType +* @param {String} type the name of the node type +* @return {Array} a list with all the nodes of this type +*/ + +LGraph.prototype.findNodesByType = function(type) +{ + var type = type.toLowerCase(); + var r = []; + for(var i = 0, l = this._nodes.length; i < l; ++i) + if(this._nodes[i].type.toLowerCase() == type ) + r.push(this._nodes[i]); + return r; +} + +/** +* Returns a list of nodes that matches a name +* @method findNodesByTitle +* @param {String} name the name of the node to search +* @return {Array} a list with all the nodes with this name +*/ + +LGraph.prototype.findNodesByTitle = function(title) +{ + var result = []; + for(var i = 0, l = this._nodes.length; i < l; ++i) + if(this._nodes[i].title == title) + result.push(this._nodes[i]); + return result; +} + +/** +* Returns the top-most node in this position of the canvas +* @method getNodeOnPos +* @param {number} x the x coordinate in canvas space +* @param {number} y the y coordinate in canvas space +* @param {Array} nodes_list a list with all the nodes to search from, by default is all the nodes in the graph +* @return {LGraphNode} the node at this position or null +*/ +LGraph.prototype.getNodeOnPos = function(x,y, nodes_list) +{ + nodes_list = nodes_list || this._nodes; + for (var i = nodes_list.length - 1; i >= 0; i--) + { + var n = nodes_list[i]; + if(n.isPointInside( x, y, 2 )) + return n; + } + return null; +} + +/** +* Returns the top-most group in that position +* @method getGroupOnPos +* @param {number} x the x coordinate in canvas space +* @param {number} y the y coordinate in canvas space +* @return {LGraphGroup} the group or null +*/ +LGraph.prototype.getGroupOnPos = function(x,y) +{ + for (var i = this._groups.length - 1; i >= 0; i--) + { + var g = this._groups[i]; + if(g.isPointInside( x, y, 2 )) + return g; + } + return null; +} + +// ********** GLOBALS ***************** + +/** +* Tell this graph it has a global graph input of this type +* @method addGlobalInput +* @param {String} name +* @param {String} type +* @param {*} value [optional] +*/ +LGraph.prototype.addGlobalInput = function(name, type, value) +{ + this.global_inputs[name] = { name: name, type: type, value: value }; + this._version++; + + if(this.onGlobalInputAdded) + this.onGlobalInputAdded(name, type); + + if(this.onGlobalsChange) + this.onGlobalsChange(); +} + +/** +* Assign a data to the global graph input +* @method setGlobalInputData +* @param {String} name +* @param {*} data +*/ +LGraph.prototype.setGlobalInputData = function(name, data) +{ + var input = this.global_inputs[name]; + if (!input) + return; + input.value = data; +} + +/** +* Assign a data to the global graph input (same as setGlobalInputData) +* @method setInputData +* @param {String} name +* @param {*} data +*/ +LGraph.prototype.setInputData = LGraph.prototype.setGlobalInputData; + + +/** +* Returns the current value of a global graph input +* @method getGlobalInputData +* @param {String} name +* @return {*} the data +*/ +LGraph.prototype.getGlobalInputData = function(name) +{ + var input = this.global_inputs[name]; + if (!input) + return null; + return input.value; +} + +/** +* Changes the name of a global graph input +* @method renameGlobalInput +* @param {String} old_name +* @param {String} new_name +*/ +LGraph.prototype.renameGlobalInput = function(old_name, name) +{ + if(name == old_name) + return; + + if(!this.global_inputs[old_name]) + return false; + + if(this.global_inputs[name]) + { + console.error("there is already one input with that name"); + return false; + } + + this.global_inputs[name] = this.global_inputs[old_name]; + delete this.global_inputs[old_name]; + this._version++; + + if(this.onGlobalInputRenamed) + this.onGlobalInputRenamed(old_name, name); + + if(this.onGlobalsChange) + this.onGlobalsChange(); +} + +/** +* Changes the type of a global graph input +* @method changeGlobalInputType +* @param {String} name +* @param {String} type +*/ +LGraph.prototype.changeGlobalInputType = function(name, type) +{ + if(!this.global_inputs[name]) + return false; + + if(this.global_inputs[name].type && this.global_inputs[name].type.toLowerCase() == type.toLowerCase() ) + return; + + this.global_inputs[name].type = type; + this._version++; + if(this.onGlobalInputTypeChanged) + this.onGlobalInputTypeChanged(name, type); +} + +/** +* Removes a global graph input +* @method removeGlobalInput +* @param {String} name +* @param {String} type +*/ +LGraph.prototype.removeGlobalInput = function(name) +{ + if(!this.global_inputs[name]) + return false; + + delete this.global_inputs[name]; + this._version++; + + if(this.onGlobalInputRemoved) + this.onGlobalInputRemoved(name); + + if(this.onGlobalsChange) + this.onGlobalsChange(); + return true; +} + +/** +* Creates a global graph output +* @method addGlobalOutput +* @param {String} name +* @param {String} type +* @param {*} value +*/ +LGraph.prototype.addGlobalOutput = function(name, type, value) +{ + this.global_outputs[name] = { name: name, type: type, value: value }; + this._version++; + + if(this.onGlobalOutputAdded) + this.onGlobalOutputAdded(name, type); + + if(this.onGlobalsChange) + this.onGlobalsChange(); +} + +/** +* Assign a data to the global output +* @method setGlobalOutputData +* @param {String} name +* @param {String} value +*/ +LGraph.prototype.setGlobalOutputData = function(name, value) +{ + var output = this.global_outputs[ name ]; + if (!output) + return; + output.value = value; +} + +/** +* Returns the current value of a global graph output +* @method getGlobalOutputData +* @param {String} name +* @return {*} the data +*/ +LGraph.prototype.getGlobalOutputData = function(name) +{ + var output = this.global_outputs[name]; + if (!output) + return null; + return output.value; +} + +/** +* Returns the current value of a global graph output (sames as getGlobalOutputData) +* @method getOutputData +* @param {String} name +* @return {*} the data +*/ +LGraph.prototype.getOutputData = LGraph.prototype.getGlobalOutputData; + + +/** +* Renames a global graph output +* @method renameGlobalOutput +* @param {String} old_name +* @param {String} new_name +*/ +LGraph.prototype.renameGlobalOutput = function(old_name, name) +{ + if(!this.global_outputs[old_name]) + return false; + + if(this.global_outputs[name]) + { + console.error("there is already one output with that name"); + return false; + } + + this.global_outputs[name] = this.global_outputs[old_name]; + delete this.global_outputs[old_name]; + this._version++; + + if(this.onGlobalOutputRenamed) + this.onGlobalOutputRenamed(old_name, name); + + if(this.onGlobalsChange) + this.onGlobalsChange(); +} + +/** +* Changes the type of a global graph output +* @method changeGlobalOutputType +* @param {String} name +* @param {String} type +*/ +LGraph.prototype.changeGlobalOutputType = function(name, type) +{ + if(!this.global_outputs[name]) + return false; + + if(this.global_outputs[name].type && this.global_outputs[name].type.toLowerCase() == type.toLowerCase() ) + return; + + this.global_outputs[name].type = type; + this._version++; + if(this.onGlobalOutputTypeChanged) + this.onGlobalOutputTypeChanged(name, type); +} + +/** +* Removes a global graph output +* @method removeGlobalOutput +* @param {String} name +*/ +LGraph.prototype.removeGlobalOutput = function(name) +{ + if(!this.global_outputs[name]) + return false; + delete this.global_outputs[name]; + this._version++; + + if(this.onGlobalOutputRemoved) + this.onGlobalOutputRemoved(name); + + if(this.onGlobalsChange) + this.onGlobalsChange(); + return true; +} + +LGraph.prototype.triggerInput = function(name,value) +{ + var nodes = this.findNodesByTitle(name); + for(var i = 0; i < nodes.length; ++i) + nodes[i].onTrigger(value); +} + +LGraph.prototype.setCallback = function(name,func) +{ + var nodes = this.findNodesByTitle(name); + for(var i = 0; i < nodes.length; ++i) + nodes[i].setTrigger(func); +} + + +LGraph.prototype.connectionChange = function( node ) +{ + this.updateExecutionOrder(); + if( this.onConnectionChange ) + this.onConnectionChange( node ); + this._version++; + this.sendActionToCanvas("onConnectionChange"); +} + +/** +* returns if the graph is in live mode +* @method isLive +*/ + +LGraph.prototype.isLive = function() +{ + if(!this.list_of_graphcanvas) + return false; + + for(var i = 0; i < this.list_of_graphcanvas.length; ++i) + { + var c = this.list_of_graphcanvas[i]; + if(c.live_mode) + return true; + } + return false; +} + +/* Called when something visually changed (not the graph!) */ +LGraph.prototype.change = function() +{ + if(LiteGraph.debug) + console.log("Graph changed"); + this.sendActionToCanvas("setDirty",[true,true]); + if(this.on_change) + this.on_change(this); +} + +LGraph.prototype.setDirtyCanvas = function(fg,bg) +{ + this.sendActionToCanvas("setDirty",[fg,bg]); +} + +//save and recover app state *************************************** +/** +* Creates a Object containing all the info about this graph, it can be serialized +* @method serialize +* @return {Object} value of the node +*/ +LGraph.prototype.serialize = function() +{ + var nodes_info = []; + for(var i = 0, l = this._nodes.length; i < l; ++i) + nodes_info.push( this._nodes[i].serialize() ); + + //pack link info into a non-verbose format + var links = []; + for(var i in this.links) //links is an OBJECT + { + var link = this.links[i]; + links.push([ link.id, link.origin_id, link.origin_slot, link.target_id, link.target_slot, link.type ]); + } + + var groups_info = []; + for(var i = 0; i < this._groups.length; ++i) + groups_info.push( this._groups[i].serialize() ); + + var data = { + last_node_id: this.last_node_id, + last_link_id: this.last_link_id, + nodes: nodes_info, + links: links, + groups: groups_info, + config: this.config + }; + + return data; +} + + +/** +* Configure a graph from a JSON string +* @method configure +* @param {String} str configure a graph from a JSON string +* @param {Boolean} returns if there was any error parsing +*/ +LGraph.prototype.configure = function( data, keep_old ) +{ + if(!data) + return; + + if(!keep_old) + this.clear(); + + var nodes = data.nodes; + + //decode links info (they are very verbose) + if(data.links && data.links.constructor === Array) + { + var links = []; + for(var i = 0; i < data.links.length; ++i) + { + var link = data.links[i]; + links[ link[0] ] = { id: link[0], origin_id: link[1], origin_slot: link[2], target_id: link[3], target_slot: link[4], type: link[5] }; + } + data.links = links; + } + + //copy all stored fields + for (var i in data) + this[i] = data[i]; + + var error = false; + + //create nodes + this._nodes = []; + if(nodes) + { + for(var i = 0, l = nodes.length; i < l; ++i) + { + var n_info = nodes[i]; //stored info + var node = LiteGraph.createNode( n_info.type, n_info.title ); + if(!node) + { + if(LiteGraph.debug) + console.log("Node not found: " + n_info.type); + error = true; + continue; + } + + node.id = n_info.id; //id it or it will create a new id + this.add(node, true); //add before configure, otherwise configure cannot create links + } + + //configure nodes afterwards so they can reach each other + for(var i = 0, l = nodes.length; i < l; ++i) + { + var n_info = nodes[i]; + var node = this.getNodeById( n_info.id ); + if(node) + node.configure( n_info ); + } + } + + //groups + this._groups.length = 0; + if( data.groups ) + for(var i = 0; i < data.groups.length; ++i ) + { + var group = new LiteGraph.LGraphGroup(); + group.configure( data.groups[i] ); + this.add( group ); + } + + this.updateExecutionOrder(); + this._version++; + this.setDirtyCanvas(true,true); + return error; +} + +LGraph.prototype.load = function(url) +{ + var that = this; + var req = new XMLHttpRequest(); + req.open('GET', url, true); + req.send(null); + req.onload = function (oEvent) { + if(req.status !== 200) + { + console.error("Error loading graph:",req.status,req.response); + return; + } + var data = JSON.parse( req.response ); + that.configure(data); + } + req.onerror = function(err) + { + console.error("Error loading graph:",err); + } +} + +LGraph.prototype.onNodeTrace = function(node, msg, color) +{ + //TODO +} + +// ************************************************************* +// Node CLASS ******* +// ************************************************************* + +/* + title: string + pos: [x,y] + size: [x,y] + + input|output: every connection + + { name:string, type:string, pos: [x,y]=Optional, direction: "input"|"output", links: Array }); + + flags: + + clip_area: if you render outside the node, it will be cliped + + unsafe_execution: not allowed for safe execution + + skip_repeated_outputs: when adding new outputs, it wont show if there is one already connected + + resizable: if set to false it wont be resizable with the mouse + + supported callbacks: + + onAdded: when added to graph + + onRemoved: when removed from graph + + onStart: when the graph starts playing + + onStop: when the graph stops playing + + onDrawForeground: render the inside widgets inside the node + + onDrawBackground: render the background area inside the node (only in edit mode) + + onMouseDown + + onMouseMove + + onMouseUp + + onMouseEnter + + onMouseLeave + + onExecute: execute the node + + onPropertyChanged: when a property is changed in the panel (return true to skip default behaviour) + + onGetInputs: returns an array of possible inputs + + onGetOutputs: returns an array of possible outputs + + onDblClick + + onSerialize + + onSelected + + onDeselected + + onDropItem : DOM item dropped over the node + + onDropFile : file dropped over the node + + onConnectInput : if returns false the incoming connection will be canceled + + onConnectionsChange : a connection changed (new one or removed) (LiteGraph.INPUT or LiteGraph.OUTPUT, slot, true if connected, link_info, input_info ) +*/ + +/** +* Base Class for all the node type classes +* @class LGraphNode +* @param {String} name a name for the node +*/ + +function LGraphNode(title) +{ + this._ctor(title); +} + +global.LGraphNode = LiteGraph.LGraphNode = LGraphNode; + +LGraphNode.prototype._ctor = function( title ) +{ + this.title = title || "Unnamed"; + this.size = [LiteGraph.NODE_WIDTH,60]; + this.graph = null; + + this._pos = new Float32Array(10,10); + + Object.defineProperty( this, "pos", { + set: function(v) + { + if(!v || v.length < 2) + return; + this._pos[0] = v[0]; + this._pos[1] = v[1]; + }, + get: function() + { + return this._pos; + }, + enumerable: true + }); + + this.id = -1; //not know till not added + this.type = null; + + //inputs available: array of inputs + this.inputs = []; + this.outputs = []; + this.connections = []; + + //local data + this.properties = {}; //for the values + this.properties_info = []; //for the info + + this.data = null; //persistent local data + this.flags = {}; +} + +/** +* configure a node from an object containing the serialized info +* @method configure +*/ +LGraphNode.prototype.configure = function(info) +{ + if(this.graph) + this.graph._version++; + + for (var j in info) + { + if(j == "console") + continue; + + if(j == "properties") + { + //i dont want to clone properties, I want to reuse the old container + for(var k in info.properties) + { + this.properties[k] = info.properties[k]; + if(this.onPropertyChanged) + this.onPropertyChanged(k,info.properties[k]); + } + continue; + } + + if(info[j] == null) + continue; + + else if (typeof(info[j]) == 'object') //object + { + if(this[j] && this[j].configure) + this[j].configure( info[j] ); + else + this[j] = LiteGraph.cloneObject(info[j], this[j]); + } + else //value + this[j] = info[j]; + } + + if(!info.title) + this.title = this.constructor.title; + + if(this.onConnectionsChange) + { + if(this.inputs) + for(var i = 0; i < this.inputs.length; ++i) + { + var input = this.inputs[i]; + var link_info = this.graph ? this.graph.links[ input.link ] : null; + this.onConnectionsChange( LiteGraph.INPUT, i, true, link_info, input ); //link_info has been created now, so its updated + } + + if(this.outputs) + for(var i = 0; i < this.outputs.length; ++i) + { + var output = this.outputs[i]; + if(!output.links) + continue; + for(var j = 0; j < output.links.length; ++j) + { + var link_info = this.graph ? this.graph.links[ output.links[j] ] : null; + this.onConnectionsChange( LiteGraph.OUTPUT, i, true, link_info, output ); //link_info has been created now, so its updated + } + } + } + + //FOR LEGACY, PLEASE REMOVE ON NEXT VERSION + for(var i in this.inputs) + { + var input = this.inputs[i]; + if(!input.link || !input.link.length ) + continue; + var link = input.link; + if(typeof(link) != "object") + continue; + input.link = link[0]; + if(this.graph) + this.graph.links[ link[0] ] = { + id: link[0], + origin_id: link[1], + origin_slot: link[2], + target_id: link[3], + target_slot: link[4] + }; + } + for(var i in this.outputs) + { + var output = this.outputs[i]; + if(!output.links || output.links.length == 0) + continue; + for(var j in output.links) + { + var link = output.links[j]; + if(typeof(link) != "object") + continue; + output.links[j] = link[0]; + } + } + + if( this.onConfigure ) + this.onConfigure( info ); +} + +/** +* serialize the content +* @method serialize +*/ + +LGraphNode.prototype.serialize = function() +{ + //create serialization object + var o = { + id: this.id, + type: this.type, + pos: this.pos, + size: this.size, + data: this.data, + flags: LiteGraph.cloneObject(this.flags), + mode: this.mode + }; + + if( this.inputs ) + o.inputs = this.inputs; + + if( this.outputs ) + { + //clear outputs last data (because data in connections is never serialized but stored inside the outputs info) + for(var i = 0; i < this.outputs.length; i++) + delete this.outputs[i]._data; + o.outputs = this.outputs; + } + + if( this.title && this.title != this.constructor.title ) + o.title = this.title; + + if( this.properties ) + o.properties = LiteGraph.cloneObject( this.properties ); + + if( !o.type ) + o.type = this.constructor.type; + + if( this.color ) + o.color = this.color; + if( this.bgcolor ) + o.bgcolor = this.bgcolor; + if( this.boxcolor ) + o.boxcolor = this.boxcolor; + if( this.shape ) + o.shape = this.shape; + + if(this.onSerialize) + this.onSerialize(o); + + return o; +} + + +/* Creates a clone of this node */ +LGraphNode.prototype.clone = function() +{ + var node = LiteGraph.createNode(this.type); + + //we clone it because serialize returns shared containers + var data = LiteGraph.cloneObject( this.serialize() ); + + //remove links + if(data.inputs) + for(var i = 0; i < data.inputs.length; ++i) + data.inputs[i].link = null; + + if(data.outputs) + for(var i = 0; i < data.outputs.length; ++i) + { + if(data.outputs[i].links) + data.outputs[i].links.length = 0; + } + + delete data["id"]; + //remove links + node.configure(data); + + return node; +} + + +/** +* serialize and stringify +* @method toString +*/ + +LGraphNode.prototype.toString = function() +{ + return JSON.stringify( this.serialize() ); +} +//LGraphNode.prototype.unserialize = function(info) {} //this cannot be done from within, must be done in LiteGraph + + +/** +* get the title string +* @method getTitle +*/ + +LGraphNode.prototype.getTitle = function() +{ + return this.title || this.constructor.title; +} + + + +// Execution ************************* +/** +* sets the output data +* @method setOutputData +* @param {number} slot +* @param {*} data +*/ +LGraphNode.prototype.setOutputData = function(slot, data) +{ + if(!this.outputs) + return; + + //this maybe slow and a niche case + //if(slot && slot.constructor === String) + // slot = this.findOutputSlot(slot); + + if(slot == -1 || slot >= this.outputs.length) + return; + + var output_info = this.outputs[slot]; + if(!output_info) + return; + + //store data in the output itself in case we want to debug + output_info._data = data; + + //if there are connections, pass the data to the connections + if( this.outputs[slot].links ) + { + for(var i = 0; i < this.outputs[slot].links.length; i++) + { + var link_id = this.outputs[slot].links[i]; + this.graph.links[ link_id ].data = data; + } + } +} + +/** +* Retrieves the input data (data traveling through the connection) from one slot +* @method getInputData +* @param {number} slot +* @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link +* @return {*} data or if it is not connected returns undefined +*/ +LGraphNode.prototype.getInputData = function( slot, force_update ) +{ + if(!this.inputs) + return; //undefined; + + if(slot >= this.inputs.length || this.inputs[slot].link == null) + return; + + var link_id = this.inputs[slot].link; + var link = this.graph.links[ link_id ]; + if(!link) //bug: weird case but it happens sometimes + return null; + + if(!force_update) + return link.data; + + //special case: used to extract data from the incomming connection before the graph has been executed + var node = this.graph.getNodeById( link.origin_id ); + if(!node) + return link.data; + + if(node.updateOutputData) + node.updateOutputData( link.origin_slot ); + else if(node.onExecute) + node.onExecute(); + + return link.data; +} + +/** +* Retrieves the input data from one slot using its name instead of slot number +* @method getInputDataByName +* @param {String} slot_name +* @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link +* @return {*} data or if it is not connected returns null +*/ +LGraphNode.prototype.getInputDataByName = function( slot_name, force_update ) +{ + var slot = this.findInputSlot( slot_name ); + if( slot == -1 ) + return null; + return this.getInputData( slot, force_update ); +} + + +/** +* tells you if there is a connection in one input slot +* @method isInputConnected +* @param {number} slot +* @return {boolean} +*/ +LGraphNode.prototype.isInputConnected = function(slot) +{ + if(!this.inputs) + return false; + return (slot < this.inputs.length && this.inputs[slot].link != null); +} + +/** +* tells you info about an input connection (which node, type, etc) +* @method getInputInfo +* @param {number} slot +* @return {Object} object or null { link: id, name: string, type: string or 0 } +*/ +LGraphNode.prototype.getInputInfo = function(slot) +{ + if(!this.inputs) + return null; + if(slot < this.inputs.length) + return this.inputs[slot]; + return null; +} + +/** +* returns the node connected in the input slot +* @method getInputNode +* @param {number} slot +* @return {LGraphNode} node or null +*/ +LGraphNode.prototype.getInputNode = function( slot ) +{ + if(!this.inputs) + return null; + if(slot >= this.inputs.length) + return null; + var input = this.inputs[slot]; + if(!input || input.link === null) + return null; + var link_info = this.graph.links[ input.link ]; + if(!link_info) + return null; + return this.graph.getNodeById( link_info.origin_id ); +} + + +/** +* returns the value of an input with this name, otherwise checks if there is a property with that name +* @method getInputOrProperty +* @param {string} name +* @return {*} value +*/ +LGraphNode.prototype.getInputOrProperty = function( name ) +{ + if(!this.inputs || !this.inputs.length) + return this.properties ? this.properties[name] : null; + + for(var i = 0, l = this.inputs.length; i < l; ++i) + if(name == this.inputs[i].name) + { + var link_id = this.inputs[i].link; + var link = this.graph.links[ link_id ]; + return link ? link.data : null; + } + return this.properties[name]; +} + + + + +/** +* tells you the last output data that went in that slot +* @method getOutputData +* @param {number} slot +* @return {Object} object or null +*/ +LGraphNode.prototype.getOutputData = function(slot) +{ + if(!this.outputs) + return null; + if(slot >= this.outputs.length) + return null; + + var info = this.outputs[slot]; + return info._data; +} + + +/** +* tells you info about an output connection (which node, type, etc) +* @method getOutputInfo +* @param {number} slot +* @return {Object} object or null { name: string, type: string, links: [ ids of links in number ] } +*/ +LGraphNode.prototype.getOutputInfo = function(slot) +{ + if(!this.outputs) + return null; + if(slot < this.outputs.length) + return this.outputs[slot]; + return null; +} + + +/** +* tells you if there is a connection in one output slot +* @method isOutputConnected +* @param {number} slot +* @return {boolean} +*/ +LGraphNode.prototype.isOutputConnected = function(slot) +{ + if(!this.outputs) + return false; + return (slot < this.outputs.length && this.outputs[slot].links && this.outputs[slot].links.length); +} + +/** +* tells you if there is any connection in the output slots +* @method isAnyOutputConnected +* @return {boolean} +*/ +LGraphNode.prototype.isAnyOutputConnected = function() +{ + if(!this.outputs) + return false; + for(var i = 0; i < this.outputs.length; ++i) + if( this.outputs[i].links && this.outputs[i].links.length ) + return true; + return false; +} + + +/** +* retrieves all the nodes connected to this output slot +* @method getOutputNodes +* @param {number} slot +* @return {array} +*/ +LGraphNode.prototype.getOutputNodes = function(slot) +{ + if(!this.outputs || this.outputs.length == 0) + return null; + + if(slot >= this.outputs.length) + return null; + + var output = this.outputs[slot]; + if(!output.links || output.links.length == 0) + return null; + + var r = []; + for(var i = 0; i < output.links.length; i++) + { + var link_id = output.links[i]; + var link = this.graph.links[ link_id ]; + if(link) + { + var target_node = this.graph.getNodeById( link.target_id ); + if( target_node ) + r.push( target_node ); + } + } + return r; +} + +/** +* Triggers an event in this node, this will trigger any output with the same name +* @method trigger +* @param {String} event name ( "on_play", ... ) if action is equivalent to false then the event is send to all +* @param {*} param +*/ +LGraphNode.prototype.trigger = function( action, param ) +{ + if( !this.outputs || !this.outputs.length ) + return; + + if(this.graph) + this.graph._last_trigger_time = LiteGraph.getTime(); + + for(var i = 0; i < this.outputs.length; ++i) + { + var output = this.outputs[ i ]; + if(!output || output.type !== LiteGraph.EVENT || (action && output.name != action) ) + continue; + this.triggerSlot( i, param ); + } +} + +/** +* Triggers an slot event in this node +* @method triggerSlot +* @param {Number} slot the index of the output slot +* @param {*} param +*/ +LGraphNode.prototype.triggerSlot = function( slot, param ) +{ + if( !this.outputs ) + return; + + var output = this.outputs[ slot ]; + if( !output ) + return; + + var links = output.links; + if(!links || !links.length) + return; + + if(this.graph) + this.graph._last_trigger_time = LiteGraph.getTime(); + + //for every link attached here + for(var k = 0; k < links.length; ++k) + { + var link_info = this.graph.links[ links[k] ]; + if(!link_info) //not connected + continue; + var node = this.graph.getNodeById( link_info.target_id ); + if(!node) //node not found? + continue; + + //used to mark events in graph + link_info._last_time = LiteGraph.getTime(); + + var target_connection = node.inputs[ link_info.target_slot ]; + + if(node.onAction) + node.onAction( target_connection.name, param ); + else if(node.mode === LiteGraph.ON_TRIGGER) + { + if(node.onExecute) + node.onExecute(param); + } + } +} + +/** +* add a new property to this node +* @method addProperty +* @param {string} name +* @param {*} default_value +* @param {string} type string defining the output type ("vec3","number",...) +* @param {Object} extra_info this can be used to have special properties of the property (like values, etc) +*/ +LGraphNode.prototype.addProperty = function( name, default_value, type, extra_info ) +{ + var o = { name: name, type: type, default_value: default_value }; + if(extra_info) + for(var i in extra_info) + o[i] = extra_info[i]; + if(!this.properties_info) + this.properties_info = []; + this.properties_info.push(o); + if(!this.properties) + this.properties = {}; + this.properties[ name ] = default_value; + return o; +} + + +//connections + +/** +* add a new output slot to use in this node +* @method addOutput +* @param {string} name +* @param {string} type string defining the output type ("vec3","number",...) +* @param {Object} extra_info this can be used to have special properties of an output (label, special color, position, etc) +*/ +LGraphNode.prototype.addOutput = function(name,type,extra_info) +{ + var o = { name: name, type: type, links: null }; + if(extra_info) + for(var i in extra_info) + o[i] = extra_info[i]; + + if(!this.outputs) + this.outputs = []; + this.outputs.push(o); + if(this.onOutputAdded) + this.onOutputAdded(o); + this.size = this.computeSize(); + return o; +} + +/** +* add a new output slot to use in this node +* @method addOutputs +* @param {Array} array of triplets like [[name,type,extra_info],[...]] +*/ +LGraphNode.prototype.addOutputs = function(array) +{ + for(var i = 0; i < array.length; ++i) + { + var info = array[i]; + var o = {name:info[0],type:info[1],link:null}; + if(array[2]) + for(var j in info[2]) + o[j] = info[2][j]; + + if(!this.outputs) + this.outputs = []; + this.outputs.push(o); + if(this.onOutputAdded) + this.onOutputAdded(o); + } + + this.size = this.computeSize(); +} + +/** +* remove an existing output slot +* @method removeOutput +* @param {number} slot +*/ +LGraphNode.prototype.removeOutput = function(slot) +{ + this.disconnectOutput(slot); + this.outputs.splice(slot,1); + this.size = this.computeSize(); + if(this.onOutputRemoved) + this.onOutputRemoved(slot); +} + +/** +* add a new input slot to use in this node +* @method addInput +* @param {string} name +* @param {string} type string defining the input type ("vec3","number",...), it its a generic one use 0 +* @param {Object} extra_info this can be used to have special properties of an input (label, color, position, etc) +*/ +LGraphNode.prototype.addInput = function(name,type,extra_info) +{ + type = type || 0; + var o = {name:name,type:type,link:null}; + if(extra_info) + for(var i in extra_info) + o[i] = extra_info[i]; + + if(!this.inputs) + this.inputs = []; + this.inputs.push(o); + this.size = this.computeSize(); + if(this.onInputAdded) + this.onInputAdded(o); + return o; +} + +/** +* add several new input slots in this node +* @method addInputs +* @param {Array} array of triplets like [[name,type,extra_info],[...]] +*/ +LGraphNode.prototype.addInputs = function(array) +{ + for(var i = 0; i < array.length; ++i) + { + var info = array[i]; + var o = {name:info[0], type:info[1], link:null}; + if(array[2]) + for(var j in info[2]) + o[j] = info[2][j]; + + if(!this.inputs) + this.inputs = []; + this.inputs.push(o); + if(this.onInputAdded) + this.onInputAdded(o); + } + + this.size = this.computeSize(); +} + +/** +* remove an existing input slot +* @method removeInput +* @param {number} slot +*/ +LGraphNode.prototype.removeInput = function(slot) +{ + this.disconnectInput(slot); + this.inputs.splice(slot,1); + this.size = this.computeSize(); + if(this.onInputRemoved) + this.onInputRemoved(slot); +} + +/** +* add an special connection to this node (used for special kinds of graphs) +* @method addConnection +* @param {string} name +* @param {string} type string defining the input type ("vec3","number",...) +* @param {[x,y]} pos position of the connection inside the node +* @param {string} direction if is input or output +*/ +LGraphNode.prototype.addConnection = function(name,type,pos,direction) +{ + var o = { + name: name, + type: type, + pos: pos, + direction: direction, + links: null + }; + this.connections.push( o ); + return o; +} + +/** +* computes the size of a node according to its inputs and output slots +* @method computeSize +* @param {number} minHeight +* @return {number} the total size +*/ +LGraphNode.prototype.computeSize = function( minHeight, out ) +{ + var rows = Math.max( this.inputs ? this.inputs.length : 1, this.outputs ? this.outputs.length : 1); + var size = out || new Float32Array([0,0]); + rows = Math.max(rows, 1); + var font_size = LiteGraph.NODE_TEXT_SIZE; //although it should be graphcanvas.inner_text_font size + size[1] = (this.constructor.slot_start_y || 0) + rows * (font_size + 1) + ( this.widgets ? this.widgets.length : 0 ) * (LiteGraph.NODE_WIDGET_HEIGHT + 4 ) + 4; + + var font_size = font_size; + var title_width = compute_text_size( this.title ); + var input_width = 0; + var output_width = 0; + + if(this.inputs) + for(var i = 0, l = this.inputs.length; i < l; ++i) + { + var input = this.inputs[i]; + var text = input.label || input.name || ""; + var text_width = compute_text_size( text ); + if(input_width < text_width) + input_width = text_width; + } + + if(this.outputs) + for(var i = 0, l = this.outputs.length; i < l; ++i) + { + var output = this.outputs[i]; + var text = output.label || output.name || ""; + var text_width = compute_text_size( text ); + if(output_width < text_width) + output_width = text_width; + } + + size[0] = Math.max( input_width + output_width + 10, title_width ); + size[0] = Math.max( size[0], LiteGraph.NODE_WIDTH ); + + if(this.onResize) + this.onResize(size); + + function compute_text_size( text ) + { + if(!text) + return 0; + return font_size * text.length * 0.6; + } + + return size; +} + +/** +* Allows to pass +* +* @method addWidget +* @return {Float32Array[4]} the total size +*/ +LGraphNode.prototype.addWidget = function( type, name, value, callback, options ) +{ + if(!this.widgets) + this.widgets = []; + var w = { + type: type.toLowerCase(), + name: name, + value: value, + callback: callback, + options: options || {} + }; + + if(w.options.y !== undefined ) + w.y = w.options.y; + + if( type == "combo" && !w.options.values ) + throw("LiteGraph addWidget('combo',...) requires to pass values in options: { values:['red','blue'] }"); + this.widgets.push(w); + return w; +} + + +/** +* returns the bounding of the object, used for rendering purposes +* bounding is: [topleft_cornerx, topleft_cornery, width, height] +* @method getBounding +* @return {Float32Array[4]} the total size +*/ +LGraphNode.prototype.getBounding = function( out ) +{ + out = out || new Float32Array(4); + out[0] = this.pos[0] - 4; + out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT; + out[2] = this.size[0] + 4; + out[3] = this.size[1] + LiteGraph.NODE_TITLE_HEIGHT; + return out; +} + +/** +* checks if a point is inside the shape of a node +* @method isPointInside +* @param {number} x +* @param {number} y +* @return {boolean} +*/ +LGraphNode.prototype.isPointInside = function(x,y, margin) +{ + margin = margin || 0; + + var margin_top = this.graph && this.graph.isLive() ? 0 : 20; + if(this.flags && this.flags.collapsed) + { + //if ( distance([x,y], [this.pos[0] + this.size[0]*0.5, this.pos[1] + this.size[1]*0.5]) < LiteGraph.NODE_COLLAPSED_RADIUS) + if( isInsideRectangle( x, y, this.pos[0] - margin, this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT - margin, (this._collapsed_width||LiteGraph.NODE_COLLAPSED_WIDTH) + 2 * margin, LiteGraph.NODE_TITLE_HEIGHT + 2 * margin ) ) + return true; + } + else if ( (this.pos[0] - 4 - margin) < x && (this.pos[0] + this.size[0] + 4 + margin) > x + && (this.pos[1] - margin_top - margin) < y && (this.pos[1] + this.size[1] + margin) > y) + return true; + return false; +} + +/** +* checks if a point is inside a node slot, and returns info about which slot +* @method getSlotInPosition +* @param {number} x +* @param {number} y +* @return {Object} if found the object contains { input|output: slot object, slot: number, link_pos: [x,y] } +*/ +LGraphNode.prototype.getSlotInPosition = function( x, y ) +{ + //search for inputs + if(this.inputs) + for(var i = 0, l = this.inputs.length; i < l; ++i) + { + var input = this.inputs[i]; + var link_pos = this.getConnectionPos( true,i ); + if( isInsideRectangle(x, y, link_pos[0] - 10, link_pos[1] - 5, 20,10) ) + return { input: input, slot: i, link_pos: link_pos, locked: input.locked }; + } + + if(this.outputs) + for(var i = 0, l = this.outputs.length; i < l; ++i) + { + var output = this.outputs[i]; + var link_pos = this.getConnectionPos(false,i); + if( isInsideRectangle(x, y, link_pos[0] - 10, link_pos[1] - 5, 20,10) ) + return { output: output, slot: i, link_pos: link_pos, locked: output.locked }; + } + + return null; +} + +/** +* returns the input slot with a given name (used for dynamic slots), -1 if not found +* @method findInputSlot +* @param {string} name the name of the slot +* @return {number} the slot (-1 if not found) +*/ +LGraphNode.prototype.findInputSlot = function(name) +{ + if(!this.inputs) + return -1; + for(var i = 0, l = this.inputs.length; i < l; ++i) + if(name == this.inputs[i].name) + return i; + return -1; +} + +/** +* returns the output slot with a given name (used for dynamic slots), -1 if not found +* @method findOutputSlot +* @param {string} name the name of the slot +* @return {number} the slot (-1 if not found) +*/ +LGraphNode.prototype.findOutputSlot = function(name) +{ + if(!this.outputs) return -1; + for(var i = 0, l = this.outputs.length; i < l; ++i) + if(name == this.outputs[i].name) + return i; + return -1; +} + +/** +* connect this node output to the input of another node +* @method connect +* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) +* @param {LGraphNode} node the target node +* @param {number_or_string} target_slot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger) +* @return {boolean} if it was connected succesfully +*/ +LGraphNode.prototype.connect = function( slot, target_node, target_slot ) +{ + target_slot = target_slot || 0; + + if(!this.graph) //could be connected before adding it to a graph + { + console.log("Connect: Error, node doesnt belong to any graph. Nodes must be added first to a graph before connecting them."); //due to link ids being associated with graphs + return false; + } + + + //seek for the output slot + if( slot.constructor === String ) + { + slot = this.findOutputSlot(slot); + if(slot == -1) + { + if(LiteGraph.debug) + console.log("Connect: Error, no slot of name " + slot); + return false; + } + } + else if(!this.outputs || slot >= this.outputs.length) + { + if(LiteGraph.debug) + console.log("Connect: Error, slot number not found"); + return false; + } + + if(target_node && target_node.constructor === Number) + target_node = this.graph.getNodeById( target_node ); + if(!target_node) + throw("target node is null"); + + //avoid loopback + if(target_node == this) + return false; + + //you can specify the slot by name + if(target_slot.constructor === String) + { + target_slot = target_node.findInputSlot( target_slot ); + if(target_slot == -1) + { + if(LiteGraph.debug) + console.log("Connect: Error, no slot of name " + target_slot); + return false; + } + } + else if( target_slot === LiteGraph.EVENT ) + { + //search for first slot with event? + /* + //create input for trigger + var input = target_node.addInput("onTrigger", LiteGraph.EVENT ); + target_slot = target_node.inputs.length - 1; //last one is the one created + target_node.mode = LiteGraph.ON_TRIGGER; + */ + return false; + } + else if( !target_node.inputs || target_slot >= target_node.inputs.length ) + { + if(LiteGraph.debug) + console.log("Connect: Error, slot number not found"); + return false; + } + + //if there is something already plugged there, disconnect + if(target_node.inputs[ target_slot ].link != null ) + target_node.disconnectInput( target_slot ); + + //why here?? + //this.setDirtyCanvas(false,true); + //this.graph.connectionChange( this ); + + var output = this.outputs[slot]; + + //allows nodes to block connection + if(target_node.onConnectInput) + if( target_node.onConnectInput( target_slot, output.type, output ) === false) + return false; + + var input = target_node.inputs[target_slot]; + + if( LiteGraph.isValidConnection( output.type, input.type ) ) + { + var link_info = { + id: this.graph.last_link_id++, + type: input.type, + origin_id: this.id, + origin_slot: slot, + target_id: target_node.id, + target_slot: target_slot + }; + + //add to graph links list + this.graph.links[ link_info.id ] = link_info; + + //connect in output + if( output.links == null ) + output.links = []; + output.links.push( link_info.id ); + //connect in input + target_node.inputs[target_slot].link = link_info.id; + if(this.graph) + this.graph._version++; + if(this.onConnectionsChange) + this.onConnectionsChange( LiteGraph.OUTPUT, slot, true, link_info, output ); //link_info has been created now, so its updated + if(target_node.onConnectionsChange) + target_node.onConnectionsChange( LiteGraph.INPUT, target_slot, true, link_info, input ); + if( this.graph && this.graph.onNodeConnectionChange ) + this.graph.onNodeConnectionChange( LiteGraph.OUTPUT, this, slot, target_node, target_slot ); + } + + this.setDirtyCanvas(false,true); + this.graph.connectionChange( this ); + + return true; +} + +/** +* disconnect one output to an specific node +* @method disconnectOutput +* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) +* @param {LGraphNode} target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected] +* @return {boolean} if it was disconnected succesfully +*/ +LGraphNode.prototype.disconnectOutput = function( slot, target_node ) +{ + if( slot.constructor === String ) + { + slot = this.findOutputSlot(slot); + if(slot == -1) + { + if(LiteGraph.debug) + console.log("Connect: Error, no slot of name " + slot); + return false; + } + } + else if(!this.outputs || slot >= this.outputs.length) + { + if(LiteGraph.debug) + console.log("Connect: Error, slot number not found"); + return false; + } + + //get output slot + var output = this.outputs[slot]; + if(!output.links || output.links.length == 0) + return false; + + //one of the output links in this slot + if(target_node) + { + if(target_node.constructor === Number) + target_node = this.graph.getNodeById( target_node ); + if(!target_node) + throw("Target Node not found"); + + for(var i = 0, l = output.links.length; i < l; i++) + { + var link_id = output.links[i]; + var link_info = this.graph.links[ link_id ]; + + //is the link we are searching for... + if( link_info.target_id == target_node.id ) + { + output.links.splice(i,1); //remove here + var input = target_node.inputs[ link_info.target_slot ]; + input.link = null; //remove there + delete this.graph.links[ link_id ]; //remove the link from the links pool + if(this.graph) + this.graph._version++; + if(target_node.onConnectionsChange) + target_node.onConnectionsChange( LiteGraph.INPUT, link_info.target_slot, false, link_info, input ); //link_info hasnt been modified so its ok + if(this.onConnectionsChange) + this.onConnectionsChange( LiteGraph.OUTPUT, slot, false, link_info, output ); + if( this.graph && this.graph.onNodeConnectionChange ) + this.graph.onNodeConnectionChange( LiteGraph.OUTPUT, this, slot ); + if( this.graph && this.graph.onNodeConnectionChange ) + this.graph.onNodeConnectionChange( LiteGraph.INPUT, target_node, link_info.target_slot ); + break; + } + } + } + else //all the links in this output slot + { + for(var i = 0, l = output.links.length; i < l; i++) + { + var link_id = output.links[i]; + var link_info = this.graph.links[ link_id ]; + if(!link_info) //bug: it happens sometimes + continue; + + var target_node = this.graph.getNodeById( link_info.target_id ); + var input = null; + if(this.graph) + this.graph._version++; + if(target_node) + { + input = target_node.inputs[ link_info.target_slot ]; + input.link = null; //remove other side link + if(target_node.onConnectionsChange) + target_node.onConnectionsChange( LiteGraph.INPUT, link_info.target_slot, false, link_info, input ); //link_info hasnt been modified so its ok + if( this.graph && this.graph.onNodeConnectionChange ) + this.graph.onNodeConnectionChange( LiteGraph.INPUT, target_node, link_info.target_slot ); + } + delete this.graph.links[ link_id ]; //remove the link from the links pool + if(this.onConnectionsChange) + this.onConnectionsChange( LiteGraph.OUTPUT, slot, false, link_info, output ); + if( this.graph && this.graph.onNodeConnectionChange ) + this.graph.onNodeConnectionChange( LiteGraph.OUTPUT, this, slot ); + } + output.links = null; + } + + + this.setDirtyCanvas(false,true); + this.graph.connectionChange( this ); + return true; +} + +/** +* disconnect one input +* @method disconnectInput +* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) +* @return {boolean} if it was disconnected succesfully +*/ +LGraphNode.prototype.disconnectInput = function( slot ) +{ + //seek for the output slot + if( slot.constructor === String ) + { + slot = this.findInputSlot(slot); + if(slot == -1) + { + if(LiteGraph.debug) + console.log("Connect: Error, no slot of name " + slot); + return false; + } + } + else if(!this.inputs || slot >= this.inputs.length) + { + if(LiteGraph.debug) + console.log("Connect: Error, slot number not found"); + return false; + } + + var input = this.inputs[slot]; + if(!input) + return false; + + var link_id = this.inputs[slot].link; + this.inputs[slot].link = null; + + //remove other side + var link_info = this.graph.links[ link_id ]; + if( link_info ) + { + var target_node = this.graph.getNodeById( link_info.origin_id ); + if(!target_node) + return false; + + var output = target_node.outputs[ link_info.origin_slot ]; + if(!output || !output.links || output.links.length == 0) + return false; + + //search in the inputs list for this link + for(var i = 0, l = output.links.length; i < l; i++) + { + if( output.links[i] == link_id ) + { + output.links.splice(i,1); + break; + } + } + + delete this.graph.links[ link_id ]; //remove from the pool + if(this.graph) + this.graph._version++; + if( this.onConnectionsChange ) + this.onConnectionsChange( LiteGraph.INPUT, slot, false, link_info, input ); + if( target_node.onConnectionsChange ) + target_node.onConnectionsChange( LiteGraph.OUTPUT, i, false, link_info, output ); + } + + this.setDirtyCanvas(false,true); + this.graph.connectionChange( this ); + return true; +} + +/** +* returns the center of a connection point in canvas coords +* @method getConnectionPos +* @param {boolean} is_input true if if a input slot, false if it is an output +* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) +* @return {[x,y]} the position +**/ +LGraphNode.prototype.getConnectionPos = function( is_input, slot_number ) +{ + if(this.flags.collapsed) + { + if(is_input) + return [this.pos[0], this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5]; + else + return [this.pos[0] + (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH), this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5]; + } + + if(is_input && slot_number == -1) + { + return [this.pos[0] + 10, this.pos[1] + 10]; + } + + if(is_input && this.inputs.length > slot_number && this.inputs[slot_number].pos) + return [this.pos[0] + this.inputs[slot_number].pos[0],this.pos[1] + this.inputs[slot_number].pos[1]]; + else if(!is_input && this.outputs.length > slot_number && this.outputs[slot_number].pos) + return [this.pos[0] + this.outputs[slot_number].pos[0],this.pos[1] + this.outputs[slot_number].pos[1]]; + + if(!is_input) //output + return [this.pos[0] + this.size[0] + 1, this.pos[1] + 10 + slot_number * LiteGraph.NODE_SLOT_HEIGHT + (this.constructor.slot_start_y || 0)]; + return [this.pos[0] , this.pos[1] + 10 + slot_number * LiteGraph.NODE_SLOT_HEIGHT + (this.constructor.slot_start_y || 0) ]; +} + +/* Force align to grid */ +LGraphNode.prototype.alignToGrid = function() +{ + this.pos[0] = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.pos[0] / LiteGraph.CANVAS_GRID_SIZE); + this.pos[1] = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.pos[1] / LiteGraph.CANVAS_GRID_SIZE); +} + + +/* Console output */ +LGraphNode.prototype.trace = function(msg) +{ + if(!this.console) + this.console = []; + this.console.push(msg); + if(this.console.length > LGraphNode.MAX_CONSOLE) + this.console.shift(); + + this.graph.onNodeTrace(this,msg); +} + +/* Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */ +LGraphNode.prototype.setDirtyCanvas = function(dirty_foreground, dirty_background) +{ + if(!this.graph) + return; + this.graph.sendActionToCanvas("setDirty",[dirty_foreground, dirty_background]); +} + +LGraphNode.prototype.loadImage = function(url) +{ + var img = new Image(); + img.src = LiteGraph.node_images_path + url; + img.ready = false; + + var that = this; + img.onload = function() { + this.ready = true; + that.setDirtyCanvas(true); + } + return img; +} + +//safe LGraphNode action execution (not sure if safe) +/* +LGraphNode.prototype.executeAction = function(action) +{ + if(action == "") return false; + + if( action.indexOf(";") != -1 || action.indexOf("}") != -1) + { + this.trace("Error: Action contains unsafe characters"); + return false; + } + + var tokens = action.split("("); + var func_name = tokens[0]; + if( typeof(this[func_name]) != "function") + { + this.trace("Error: Action not found on node: " + func_name); + return false; + } + + var code = action; + + try + { + var _foo = eval; + eval = null; + (new Function("with(this) { " + code + "}")).call(this); + eval = _foo; + } + catch (err) + { + this.trace("Error executing action {" + action + "} :" + err); + return false; + } + + return true; +} +*/ + +/* Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */ +LGraphNode.prototype.captureInput = function(v) +{ + if(!this.graph || !this.graph.list_of_graphcanvas) + return; + + var list = this.graph.list_of_graphcanvas; + + for(var i = 0; i < list.length; ++i) + { + var c = list[i]; + //releasing somebody elses capture?! + if(!v && c.node_capturing_input != this) + continue; + + //change + c.node_capturing_input = v ? this : null; + } +} + +/** +* Collapse the node to make it smaller on the canvas +* @method collapse +**/ +LGraphNode.prototype.collapse = function( force ) +{ + this.graph._version++; + if(this.constructor.collapsable === false && !force) + return; + if(!this.flags.collapsed) + this.flags.collapsed = true; + else + this.flags.collapsed = false; + this.setDirtyCanvas(true,true); +} + +/** +* Forces the node to do not move or realign on Z +* @method pin +**/ + +LGraphNode.prototype.pin = function(v) +{ + this.graph._version++; + if(v === undefined) + this.flags.pinned = !this.flags.pinned; + else + this.flags.pinned = v; +} + +LGraphNode.prototype.localToScreen = function(x,y, graphcanvas) +{ + return [(x + this.pos[0]) * graphcanvas.scale + graphcanvas.offset[0], + (y + this.pos[1]) * graphcanvas.scale + graphcanvas.offset[1]]; +} + + + + +function LGraphGroup( title ) +{ + this._ctor( title ); +} + +global.LGraphGroup = LiteGraph.LGraphGroup = LGraphGroup; + +LGraphGroup.prototype._ctor = function( title ) +{ + this.title = title || "Group"; + this._bounding = new Float32Array([10,10,140,80]); + this._pos = this._bounding.subarray(0,2); + this._size = this._bounding.subarray(2,4); + this._nodes = []; + this.color = LGraphCanvas.node_colors.pale_blue ? LGraphCanvas.node_colors.pale_blue.groupcolor : "#AAA"; + this.graph = null; + + Object.defineProperty( this, "pos", { + set: function(v) + { + if(!v || v.length < 2) + return; + this._pos[0] = v[0]; + this._pos[1] = v[1]; + }, + get: function() + { + return this._pos; + }, + enumerable: true + }); + + Object.defineProperty( this, "size", { + set: function(v) + { + if(!v || v.length < 2) + return; + this._size[0] = Math.max(140,v[0]); + this._size[1] = Math.max(80,v[1]); + }, + get: function() + { + return this._size; + }, + enumerable: true + }); +} + +LGraphGroup.prototype.configure = function(o) +{ + this.title = o.title; + this._bounding.set( o.bounding ); + this.color = o.color; +} + +LGraphGroup.prototype.serialize = function() +{ + var b = this._bounding; + return { + title: this.title, + bounding: [ b[0], b[1], b[2], b[3] ], + color: this.color + }; +} + +LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) +{ + this._pos[0] += deltax; + this._pos[1] += deltay; + if(ignore_nodes) + return; + for(var i = 0; i < this._nodes.length; ++i) + { + var node = this._nodes[i]; + node.pos[0] += deltax; + node.pos[1] += deltay; + } +} + +LGraphGroup.prototype.recomputeInsideNodes = function() +{ + this._nodes.length = 0; + var nodes = this.graph._nodes; + var node_bounding = new Float32Array(4); + + for(var i = 0; i < nodes.length; ++i) + { + var node = nodes[i]; + node.getBounding( node_bounding ); + if(!overlapBounding( this._bounding, node_bounding )) + continue; //out of the visible area + this._nodes.push( node ); + } +} + +LGraphGroup.prototype.isPointInside = LGraphNode.prototype.isPointInside; +LGraphGroup.prototype.setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas; + +//********************************************************************************* +// LGraphCanvas: LGraph renderer CLASS +//********************************************************************************* + +/** +* This class is in charge of rendering one graph inside a canvas. And provides all the interaction required. +* Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked +* +* @class LGraphCanvas +* @constructor +* @param {HTMLCanvas} canvas the canvas where you want to render (it accepts a selector in string format or the canvas element itself) +* @param {LGraph} graph [optional] +* @param {Object} options [optional] { skip_rendering, autoresize } +*/ +function LGraphCanvas( canvas, graph, options ) +{ + options = options || {}; + + //if(graph === undefined) + // throw ("No graph assigned"); + this.background_image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=' + + if(canvas && canvas.constructor === String ) + canvas = document.querySelector( canvas ); + + this.max_zoom = 10; + this.min_zoom = 0.1; + this.zoom_modify_alpha = true; //otherwise it generates ugly patterns when scaling down too much + + this.title_text_font = "bold "+LiteGraph.NODE_TEXT_SIZE+"px Arial"; + this.inner_text_font = "normal "+LiteGraph.NODE_SUBTEXT_SIZE+"px Arial"; + this.node_title_color = LiteGraph.NODE_TITLE_COLOR; + this.default_link_color = "#AAC"; + this.default_connection_color = { + input_off: "#AAB", + input_on: "#7F7", + output_off: "#AAB", + output_on: "#7F7" + }; + + this.highquality_render = true; + this.use_gradients = false; //set to true to render titlebar with gradients + this.editor_alpha = 1; //used for transition + this.pause_rendering = false; + this.render_shadows = true; + this.clear_background = true; + + this.render_only_selected = true; + this.live_mode = false; + this.show_info = true; + this.allow_dragcanvas = true; + this.allow_dragnodes = true; + this.allow_interaction = true; //allow to control widgets, buttons, collapse, etc + this.allow_searchbox = true; + this.drag_mode = false; + this.dragging_rectangle = null; + + this.filter = null; //allows to filter to only accept some type of nodes in a graph + + this.always_render_background = false; + this.render_canvas_border = true; + this.render_connections_shadows = false; //too much cpu + this.render_connections_border = true; + this.render_curved_connections = true; + this.render_connection_arrows = true; + + this.canvas_mouse = [0,0]; //mouse in canvas graph coordinates, where 0,0 is the top-left corner of the blue rectangle + + //to personalize the search box + this.onSearchBox = null; + this.onSearchBoxSelection = null; + + this.connections_width = 3; + this.round_radius = 8; + + this.current_node = null; + this.node_widget = null; //used for widgets + this.last_mouse_position = [0,0]; + + //link canvas and graph + if(graph) + graph.attachCanvas(this); + + this.setCanvas( canvas ); + this.clear(); + + if(!options.skip_render) + this.startRendering(); + + this.autoresize = options.autoresize; +} + +global.LGraphCanvas = LiteGraph.LGraphCanvas = LGraphCanvas; + +LGraphCanvas.link_type_colors = {"-1":"#F85",'number':"#AAC","node":"#DCA"}; +LGraphCanvas.gradients = {}; //cache of gradients + +/** +* clears all the data inside +* +* @method clear +*/ +LGraphCanvas.prototype.clear = function() +{ + this.frame = 0; + this.last_draw_time = 0; + this.render_time = 0; + this.fps = 0; + + this.scale = 1; + this.offset = [0,0]; + + this.dragging_rectangle = null; + + this.selected_nodes = {}; + this.selected_group = null; + + this.visible_nodes = []; + this.node_dragged = null; + this.node_over = null; + this.node_capturing_input = null; + this.connecting_node = null; + this.highlighted_links = {}; + + this.dirty_canvas = true; + this.dirty_bgcanvas = true; + this.dirty_area = null; + + this.node_in_panel = null; + this.node_widget = null; + + this.last_mouse = [0,0]; + this.last_mouseclick = 0; + + if(this.onClear) + this.onClear(); + //this.UIinit(); +} + +/** +* assigns a graph, you can reasign graphs to the same canvas +* +* @method setGraph +* @param {LGraph} graph +*/ +LGraphCanvas.prototype.setGraph = function( graph, skip_clear ) +{ + if(this.graph == graph) + return; + + if(!skip_clear) + this.clear(); + + if(!graph && this.graph) + { + this.graph.detachCanvas(this); + return; + } + + /* + if(this.graph) + this.graph.canvas = null; //remove old graph link to the canvas + this.graph = graph; + if(this.graph) + this.graph.canvas = this; + */ + graph.attachCanvas(this); + this.setDirty(true,true); +} + +/** +* opens a graph contained inside a node in the current graph +* +* @method openSubgraph +* @param {LGraph} graph +*/ +LGraphCanvas.prototype.openSubgraph = function(graph) +{ + if(!graph) + throw("graph cannot be null"); + + if(this.graph == graph) + throw("graph cannot be the same"); + + this.clear(); + + if(this.graph) + { + if(!this._graph_stack) + this._graph_stack = []; + this._graph_stack.push(this.graph); + } + + graph.attachCanvas(this); + this.setDirty(true,true); +} + +/** +* closes a subgraph contained inside a node +* +* @method closeSubgraph +* @param {LGraph} assigns a graph +*/ +LGraphCanvas.prototype.closeSubgraph = function() +{ + if(!this._graph_stack || this._graph_stack.length == 0) + return; + var graph = this._graph_stack.pop(); + this.selected_nodes = {}; + this.highlighted_links = {}; + graph.attachCanvas(this); + this.setDirty(true,true); +} + +/** +* assigns a canvas +* +* @method setCanvas +* @param {Canvas} assigns a canvas (also accepts the ID of the element (not a selector) +*/ +LGraphCanvas.prototype.setCanvas = function( canvas, skip_events ) +{ + var that = this; + + if(canvas) + { + if( canvas.constructor === String ) + { + canvas = document.getElementById(canvas); + if(!canvas) + throw("Error creating LiteGraph canvas: Canvas not found"); + } + } + + if(canvas === this.canvas) + return; + + if(!canvas && this.canvas) + { + //maybe detach events from old_canvas + if(!skip_events) + this.unbindEvents(); + } + + this.canvas = canvas; + + if(!canvas) + return; + + //this.canvas.tabindex = "1000"; + canvas.className += " lgraphcanvas"; + canvas.data = this; + + //bg canvas: used for non changing stuff + this.bgcanvas = null; + if(!this.bgcanvas) + { + this.bgcanvas = document.createElement("canvas"); + this.bgcanvas.width = this.canvas.width; + this.bgcanvas.height = this.canvas.height; + } + + if(canvas.getContext == null) + { + if( canvas.localName != "canvas" ) + throw("Element supplied for LGraphCanvas must be a element, you passed a " + canvas.localName ); + throw("This browser doesnt support Canvas"); + } + + var ctx = this.ctx = canvas.getContext("2d"); + if(ctx == null) + { + if(!canvas.webgl_enabled) + console.warn("This canvas seems to be WebGL, enabling WebGL renderer"); + this.enableWebGL(); + } + + //input: (move and up could be unbinded) + this._mousemove_callback = this.processMouseMove.bind(this); + this._mouseup_callback = this.processMouseUp.bind(this); + + if(!skip_events) + this.bindEvents(); +} + +//used in some events to capture them +LGraphCanvas.prototype._doNothing = function doNothing(e) { e.preventDefault(); return false; }; +LGraphCanvas.prototype._doReturnTrue = function doNothing(e) { e.preventDefault(); return true; }; + +/** +* binds mouse, keyboard, touch and drag events to the canvas +* @method bindEvents +**/ +LGraphCanvas.prototype.bindEvents = function() +{ + if( this._events_binded ) + { + console.warn("LGraphCanvas: events already binded"); + return; + } + + var canvas = this.canvas; + var ref_window = this.getCanvasWindow(); + var document = ref_window.document; //hack used when moving canvas between windows + + this._mousedown_callback = this.processMouseDown.bind(this); + this._mousewheel_callback = this.processMouseWheel.bind(this); + + canvas.addEventListener("mousedown", this._mousedown_callback, true ); //down do not need to store the binded + canvas.addEventListener("mousemove", this._mousemove_callback ); + canvas.addEventListener("mousewheel", this._mousewheel_callback, false); + + canvas.addEventListener("contextmenu", this._doNothing ); + canvas.addEventListener("DOMMouseScroll", this._mousewheel_callback, false); + + //touch events + //if( 'touchstart' in document.documentElement ) + { + canvas.addEventListener("touchstart", this.touchHandler, true); + canvas.addEventListener("touchmove", this.touchHandler, true); + canvas.addEventListener("touchend", this.touchHandler, true); + canvas.addEventListener("touchcancel", this.touchHandler, true); + } + + //Keyboard ****************** + this._key_callback = this.processKey.bind(this); + + canvas.addEventListener("keydown", this._key_callback, true ); + document.addEventListener("keyup", this._key_callback, true ); //in document, otherwise it doesnt fire keyup + + //Droping Stuff over nodes ************************************ + this._ondrop_callback = this.processDrop.bind(this); + + canvas.addEventListener("dragover", this._doNothing, false ); + canvas.addEventListener("dragend", this._doNothing, false ); + canvas.addEventListener("drop", this._ondrop_callback, false ); + canvas.addEventListener("dragenter", this._doReturnTrue, false ); + + this._events_binded = true; +} + +/** +* unbinds mouse events from the canvas +* @method unbindEvents +**/ +LGraphCanvas.prototype.unbindEvents = function() +{ + if( !this._events_binded ) + { + console.warn("LGraphCanvas: no events binded"); + return; + } + + var ref_window = this.getCanvasWindow(); + var document = ref_window.document; + + this.canvas.removeEventListener( "mousedown", this._mousedown_callback ); + this.canvas.removeEventListener( "mousewheel", this._mousewheel_callback ); + this.canvas.removeEventListener( "DOMMouseScroll", this._mousewheel_callback ); + this.canvas.removeEventListener( "keydown", this._key_callback ); + document.removeEventListener( "keyup", this._key_callback ); + this.canvas.removeEventListener( "contextmenu", this._doNothing ); + this.canvas.removeEventListener( "drop", this._ondrop_callback ); + this.canvas.removeEventListener( "dragenter", this._doReturnTrue ); + + this.canvas.removeEventListener("touchstart", this.touchHandler ); + this.canvas.removeEventListener("touchmove", this.touchHandler ); + this.canvas.removeEventListener("touchend", this.touchHandler ); + this.canvas.removeEventListener("touchcancel", this.touchHandler ); + + this._mousedown_callback = null; + this._mousewheel_callback = null; + this._key_callback = null; + this._ondrop_callback = null; + + this._events_binded = false; +} + +LGraphCanvas.getFileExtension = function (url) +{ + var question = url.indexOf("?"); + if(question != -1) + url = url.substr(0,question); + var point = url.lastIndexOf("."); + if(point == -1) + return ""; + return url.substr(point+1).toLowerCase(); +} + +/** +* this function allows to render the canvas using WebGL instead of Canvas2D +* this is useful if you plant to render 3D objects inside your nodes, it uses litegl.js for webgl and canvas2DtoWebGL to emulate the Canvas2D calls in webGL +* @method enableWebGL +**/ +LGraphCanvas.prototype.enableWebGL = function() +{ + if(typeof(GL) === undefined) + throw("litegl.js must be included to use a WebGL canvas"); + if(typeof(enableWebGLCanvas) === undefined) + throw("webglCanvas.js must be included to use this feature"); + + this.gl = this.ctx = enableWebGLCanvas(this.canvas); + this.ctx.webgl = true; + this.bgcanvas = this.canvas; + this.bgctx = this.gl; + this.canvas.webgl_enabled = true; + + /* + GL.create({ canvas: this.bgcanvas }); + this.bgctx = enableWebGLCanvas( this.bgcanvas ); + window.gl = this.gl; + */ +} + + +/** +* marks as dirty the canvas, this way it will be rendered again +* +* @class LGraphCanvas +* @method setDirty +* @param {bool} fgcanvas if the foreground canvas is dirty (the one containing the nodes) +* @param {bool} bgcanvas if the background canvas is dirty (the one containing the wires) +*/ +LGraphCanvas.prototype.setDirty = function( fgcanvas, bgcanvas ) +{ + if(fgcanvas) + this.dirty_canvas = true; + if(bgcanvas) + this.dirty_bgcanvas = true; +} + +/** +* Used to attach the canvas in a popup +* +* @method getCanvasWindow +* @return {window} returns the window where the canvas is attached (the DOM root node) +*/ +LGraphCanvas.prototype.getCanvasWindow = function() +{ + if(!this.canvas) + return window; + var doc = this.canvas.ownerDocument; + return doc.defaultView || doc.parentWindow; +} + +/** +* starts rendering the content of the canvas when needed +* +* @method startRendering +*/ +LGraphCanvas.prototype.startRendering = function() +{ + if(this.is_rendering) + return; //already rendering + + this.is_rendering = true; + renderFrame.call(this); + + function renderFrame() + { + if(!this.pause_rendering) + this.draw(); + + var window = this.getCanvasWindow(); + if(this.is_rendering) + window.requestAnimationFrame( renderFrame.bind(this) ); + } +} + +/** +* stops rendering the content of the canvas (to save resources) +* +* @method stopRendering +*/ +LGraphCanvas.prototype.stopRendering = function() +{ + this.is_rendering = false; + /* + if(this.rendering_timer_id) + { + clearInterval(this.rendering_timer_id); + this.rendering_timer_id = null; + } + */ +} + +/* LiteGraphCanvas input */ + +LGraphCanvas.prototype.processMouseDown = function(e) +{ + if(!this.graph) + return; + + this.adjustMouseEvent(e); + + var ref_window = this.getCanvasWindow(); + var document = ref_window.document; + LGraphCanvas.active_canvas = this; + var that = this; + + //move mouse move event to the window in case it drags outside of the canvas + this.canvas.removeEventListener("mousemove", this._mousemove_callback ); + ref_window.document.addEventListener("mousemove", this._mousemove_callback, true ); //catch for the entire window + ref_window.document.addEventListener("mouseup", this._mouseup_callback, true ); + + var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes ); + var skip_dragging = false; + var skip_action = false; + var now = LiteGraph.getTime(); + var is_double_click = (now - this.last_mouseclick) < 300; + + this.canvas_mouse[0] = e.canvasX; + this.canvas_mouse[1] = e.canvasY; + + LiteGraph.closeAllContextMenus( ref_window ); + + if(e.which == 1) //left button mouse + { + if( e.ctrlKey ) + { + this.dragging_rectangle = new Float32Array(4); + this.dragging_rectangle[0] = e.canvasX; + this.dragging_rectangle[1] = e.canvasY; + this.dragging_rectangle[2] = 1; + this.dragging_rectangle[3] = 1; + skip_action = true; + } + + var clicking_canvas_bg = false; + + //when clicked on top of a node + //and it is not interactive + if( node && this.allow_interaction && !skip_action ) + { + if( !this.live_mode && !node.flags.pinned ) + this.bringToFront( node ); //if it wasnt selected? + + //not dragging mouse to connect two slots + if(!this.connecting_node && !node.flags.collapsed && !this.live_mode) + { + //search for outputs + if(node.outputs) + for(var i = 0, l = node.outputs.length; i < l; ++i) + { + var output = node.outputs[i]; + var link_pos = node.getConnectionPos(false,i); + if( isInsideRectangle( e.canvasX, e.canvasY, link_pos[0] - 10, link_pos[1] - 5, 20,10) ) + { + this.connecting_node = node; + this.connecting_output = output; + this.connecting_pos = node.getConnectionPos(false,i); + this.connecting_slot = i; + + if (is_double_click) { + if (node.onOutputDblClick) + node.onOutputDblClick(i, e); + } else { + if (node.onOutputClick) + node.onOutputClick(i, e); + } + + skip_action = true; + break; + } + } + + //search for inputs + if(node.inputs) + for(var i = 0, l = node.inputs.length; i < l; ++i) + { + var input = node.inputs[i]; + var link_pos = node.getConnectionPos( true, i ); + if( isInsideRectangle(e.canvasX, e.canvasY, link_pos[0] - 10, link_pos[1] - 5, 20,10) ) + { + if (is_double_click) { + if (node.onInputDblClick) + node.onInputDblClick(i, e); + } else { + if (node.onInputClick) + node.onInputClick(i, e); + } + + if(input.link !== null) + { + node.disconnectInput(i); + this.dirty_bgcanvas = true; + skip_action = true; + } + } + } + + //Search for corner + if( !skip_action && node.flags.resizable !== false && isInsideRectangle(e.canvasX, e.canvasY, node.pos[0] + node.size[0] - 5, node.pos[1] + node.size[1] - 5 ,5,5 )) + { + this.resizing_node = node; + this.canvas.style.cursor = "se-resize"; + skip_action = true; + } + } + + //Search for corner + if( !skip_action && isInsideRectangle(e.canvasX, e.canvasY, node.pos[0], node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT, LiteGraph.NODE_TITLE_HEIGHT, LiteGraph.NODE_TITLE_HEIGHT )) + { + node.collapse(); + skip_action = true; + } + + //it wasnt clicked on the links boxes + if(!skip_action) + { + var block_drag_node = false; + + //widgets + var widget = this.processNodeWidgets( node, this.canvas_mouse, e ); + if(widget) + { + block_drag_node = true; + this.node_widget = [node, widget]; + } + + //double clicking + if (is_double_click && this.selected_nodes[ node.id ]) + { + //double click node + if( node.onDblClick) + node.onDblClick(e); + this.processNodeDblClicked( node ); + block_drag_node = true; + } + + //if do not capture mouse + if( node.onMouseDown && node.onMouseDown( e, [e.canvasX - node.pos[0], e.canvasY - node.pos[1]] ) ) + block_drag_node = true; + else if(this.live_mode) + { + clicking_canvas_bg = true; + block_drag_node = true; + } + + if(!block_drag_node) + { + if(this.allow_dragnodes) + this.node_dragged = node; + if(!this.selected_nodes[ node.id ]) + this.processNodeSelected( node, e ); + } + + this.dirty_canvas = true; + } + } + else //clicked outside of nodes + { + this.selected_group = this.graph.getGroupOnPos( e.canvasX, e.canvasY ); + this.selected_group_resizing = false; + if( this.selected_group ) + { + if( e.ctrlKey ) + this.dragging_rectangle = null; + + var dist = distance( [e.canvasX, e.canvasY], [ this.selected_group.pos[0] + this.selected_group.size[0], this.selected_group.pos[1] + this.selected_group.size[1] ] ); + if( (dist * this.scale) < 10 ) + this.selected_group_resizing = true; + else + this.selected_group.recomputeInsideNodes(); + } + + if( is_double_click ) + this.showSearchBox( e ); + + clicking_canvas_bg = true; + } + + if( !skip_action && clicking_canvas_bg && this.allow_dragcanvas ) + { + this.dragging_canvas = true; + } + } + else if (e.which == 2) //middle button + { + + } + else if (e.which == 3) //right button + { + this.processContextMenu( node, e ); + } + + //TODO + //if(this.node_selected != prev_selected) + // this.onNodeSelectionChange(this.node_selected); + + this.last_mouse[0] = e.localX; + this.last_mouse[1] = e.localY; + this.last_mouseclick = LiteGraph.getTime(); + + /* + if( (this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null) + this.draw(); + */ + + this.graph.change(); + + //this is to ensure to defocus(blur) if a text input element is on focus + if(!ref_window.document.activeElement || (ref_window.document.activeElement.nodeName.toLowerCase() != "input" && ref_window.document.activeElement.nodeName.toLowerCase() != "textarea")) + e.preventDefault(); + e.stopPropagation(); + + if(this.onMouseDown) + this.onMouseDown(e); + + return false; +} + +/** +* Called when a mouse move event has to be processed +* @method processMouseMove +**/ +LGraphCanvas.prototype.processMouseMove = function(e) +{ + if(this.autoresize) + this.resize(); + + if(!this.graph) + return; + + LGraphCanvas.active_canvas = this; + this.adjustMouseEvent(e); + var mouse = [e.localX, e.localY]; + var delta = [mouse[0] - this.last_mouse[0], mouse[1] - this.last_mouse[1]]; + this.last_mouse = mouse; + this.canvas_mouse[0] = e.canvasX; + this.canvas_mouse[1] = e.canvasY; + + if( this.node_widget ) + { + this.processNodeWidgets( this.node_widget[0], this.canvas_mouse, e, this.node_widget[1] ); + this.dirty_canvas = true; + } + + if( this.dragging_rectangle ) + { + this.dragging_rectangle[2] = e.canvasX - this.dragging_rectangle[0]; + this.dragging_rectangle[3] = e.canvasY - this.dragging_rectangle[1]; + this.dirty_canvas = true; + } + else if (this.selected_group) + { + if( this.selected_group_resizing ) + this.selected_group.size = [ e.canvasX - this.selected_group.pos[0], e.canvasY - this.selected_group.pos[1] ]; + else + { + var deltax = delta[0] / this.scale; + var deltay = delta[1] / this.scale; + this.selected_group.move( deltax, deltay, e.ctrlKey ); + if( this.selected_group._nodes.length) + this.dirty_canvas = true; + } + this.dirty_bgcanvas = true; + } + else if(this.dragging_canvas) + { + this.offset[0] += delta[0] / this.scale; + this.offset[1] += delta[1] / this.scale; + this.dirty_canvas = true; + this.dirty_bgcanvas = true; + } + else if(this.allow_interaction) + { + if(this.connecting_node) + this.dirty_canvas = true; + + //get node over + var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes ); + + //remove mouseover flag + for(var i = 0, l = this.graph._nodes.length; i < l; ++i) + { + if(this.graph._nodes[i].mouseOver && node != this.graph._nodes[i]) + { + //mouse leave + this.graph._nodes[i].mouseOver = false; + if(this.node_over && this.node_over.onMouseLeave) + this.node_over.onMouseLeave(e); + this.node_over = null; + this.dirty_canvas = true; + } + } + + //mouse over a node + if(node) + { + //this.canvas.style.cursor = "move"; + if(!node.mouseOver) + { + //mouse enter + node.mouseOver = true; + this.node_over = node; + this.dirty_canvas = true; + + if(node.onMouseEnter) node.onMouseEnter(e); + } + + //in case the node wants to do something + if(node.onMouseMove) + node.onMouseMove(e); + + //if dragging a link + if(this.connecting_node) + { + var pos = this._highlight_input || [0,0]; //to store the output of isOverNodeInput + + //on top of input + if( this.isOverNodeBox( node, e.canvasX, e.canvasY ) ) + { + //mouse on top of the corner box, dont know what to do + } + else + { + //check if I have a slot below de mouse + var slot = this.isOverNodeInput( node, e.canvasX, e.canvasY, pos ); + if(slot != -1 && node.inputs[slot] ) + { + var slot_type = node.inputs[slot].type; + if( LiteGraph.isValidConnection( this.connecting_output.type, slot_type ) ) + this._highlight_input = pos; + } + else + this._highlight_input = null; + } + } + + //Search for corner + if(this.canvas) + { + if( isInsideRectangle(e.canvasX, e.canvasY, node.pos[0] + node.size[0] - 5, node.pos[1] + node.size[1] - 5 ,5,5 )) + this.canvas.style.cursor = "se-resize"; + else + this.canvas.style.cursor = null; + } + } + else if(this.canvas) + this.canvas.style.cursor = null; + + if(this.node_capturing_input && this.node_capturing_input != node && this.node_capturing_input.onMouseMove) + { + this.node_capturing_input.onMouseMove(e); + } + + + if(this.node_dragged && !this.live_mode) + { + /* + this.node_dragged.pos[0] += delta[0] / this.scale; + this.node_dragged.pos[1] += delta[1] / this.scale; + this.node_dragged.pos[0] = Math.round(this.node_dragged.pos[0]); + this.node_dragged.pos[1] = Math.round(this.node_dragged.pos[1]); + */ + + for(var i in this.selected_nodes) + { + var n = this.selected_nodes[i]; + + n.pos[0] += delta[0] / this.scale; + n.pos[1] += delta[1] / this.scale; + //n.pos[0] = Math.round(n.pos[0]); + //n.pos[1] = Math.round(n.pos[1]); + } + + this.dirty_canvas = true; + this.dirty_bgcanvas = true; + } + + if(this.resizing_node && !this.live_mode) + { + //convert mouse to node space + this.resizing_node.size[0] = e.canvasX - this.resizing_node.pos[0]; + this.resizing_node.size[1] = e.canvasY - this.resizing_node.pos[1]; + + //constraint size + var max_slots = Math.max( this.resizing_node.inputs ? this.resizing_node.inputs.length : 0, this.resizing_node.outputs ? this.resizing_node.outputs.length : 0); + var min_height = max_slots * LiteGraph.NODE_SLOT_HEIGHT + ( this.resizing_node.widgets ? this.resizing_node.widgets.length : 0 ) * (LiteGraph.NODE_WIDGET_HEIGHT + 4 ) + 4; + if(this.resizing_node.size[1] < min_height ) + this.resizing_node.size[1] = min_height; + if(this.resizing_node.size[0] < LiteGraph.NODE_MIN_WIDTH) + this.resizing_node.size[0] = LiteGraph.NODE_MIN_WIDTH; + + this.canvas.style.cursor = "se-resize"; + this.dirty_canvas = true; + this.dirty_bgcanvas = true; + } + } + + /* + if((this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null) + this.draw(); + */ + + e.preventDefault(); + //e.stopPropagation(); + return false; + //this is not really optimal + //this.graph.change(); +} + +/** +* Called when a mouse up event has to be processed +* @method processMouseUp +**/ +LGraphCanvas.prototype.processMouseUp = function(e) +{ + if(!this.graph) + return; + + var window = this.getCanvasWindow(); + var document = window.document; + LGraphCanvas.active_canvas = this; + + //restore the mousemove event back to the canvas + document.removeEventListener("mousemove", this._mousemove_callback, true ); + this.canvas.addEventListener("mousemove", this._mousemove_callback, true); + document.removeEventListener("mouseup", this._mouseup_callback, true ); + + this.adjustMouseEvent(e); + var now = LiteGraph.getTime(); + e.click_time = (now - this.last_mouseclick); + + if (e.which == 1) //left button + { + this.node_widget = null; + this.selected_group = null; + this.selected_group_resizing = false; + + if( this.dragging_rectangle ) + { + if(this.graph) + { + var nodes = this.graph._nodes; + var node_bounding = new Float32Array(4); + this.deselectAllNodes(); + if( this.dragging_rectangle[2] < 0 ) //flip if negative width + this.dragging_rectangle[0] += this.dragging_rectangle[2]; + if( this.dragging_rectangle[3] < 0 ) //flip if negative height + this.dragging_rectangle[1] += this.dragging_rectangle[3]; + this.dragging_rectangle[2] = Math.abs( this.dragging_rectangle[2] * this.scale ); //abs to convert negative width + this.dragging_rectangle[3] = Math.abs( this.dragging_rectangle[3] * this.scale ); //abs to convert negative height + + for(var i = 0; i < nodes.length; ++i) + { + var node = nodes[i]; + node.getBounding( node_bounding ); + if(!overlapBounding( this.dragging_rectangle, node_bounding )) + continue; //out of the visible area + this.selectNode( node, true ); + } + } + this.dragging_rectangle = null; + } + else if(this.connecting_node) //dragging a connection + { + this.dirty_canvas = true; + this.dirty_bgcanvas = true; + + var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes ); + + //node below mouse + if(node) + { + if( this.connecting_output.type == LiteGraph.EVENT && this.isOverNodeBox( node, e.canvasX, e.canvasY ) ) + { + this.connecting_node.connect( this.connecting_slot, node, LiteGraph.EVENT ); + } + else + { + //slot below mouse? connect + var slot = this.isOverNodeInput(node, e.canvasX, e.canvasY); + if(slot != -1) + { + this.connecting_node.connect(this.connecting_slot, node, slot); + } + else + { //not on top of an input + var input = node.getInputInfo(0); + //auto connect + if(this.connecting_output.type == LiteGraph.EVENT) + this.connecting_node.connect( this.connecting_slot, node, LiteGraph.EVENT ); + else + if(input && !input.link && LiteGraph.isValidConnection( input.type && this.connecting_output.type ) ) + this.connecting_node.connect( this.connecting_slot, node, 0 ); + } + } + } + + this.connecting_output = null; + this.connecting_pos = null; + this.connecting_node = null; + this.connecting_slot = -1; + + }//not dragging connection + else if(this.resizing_node) + { + this.dirty_canvas = true; + this.dirty_bgcanvas = true; + this.resizing_node = null; + } + else if(this.node_dragged) //node being dragged? + { + this.dirty_canvas = true; + this.dirty_bgcanvas = true; + this.node_dragged.pos[0] = Math.round(this.node_dragged.pos[0]); + this.node_dragged.pos[1] = Math.round(this.node_dragged.pos[1]); + if(this.graph.config.align_to_grid) + this.node_dragged.alignToGrid(); + this.node_dragged = null; + } + else //no node being dragged + { + //get node over + var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes ); + if ( !node && e.click_time < 300 ) + this.deselectAllNodes(); + + this.dirty_canvas = true; + this.dragging_canvas = false; + + if( this.node_over && this.node_over.onMouseUp ) + this.node_over.onMouseUp(e, [e.canvasX - this.node_over.pos[0], e.canvasY - this.node_over.pos[1]] ); + if( this.node_capturing_input && this.node_capturing_input.onMouseUp ) + this.node_capturing_input.onMouseUp(e, [e.canvasX - this.node_capturing_input.pos[0], e.canvasY - this.node_capturing_input.pos[1]] ); + } + } + else if (e.which == 2) //middle button + { + //trace("middle"); + this.dirty_canvas = true; + this.dragging_canvas = false; + } + else if (e.which == 3) //right button + { + //trace("right"); + this.dirty_canvas = true; + this.dragging_canvas = false; + } + + /* + if((this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null) + this.draw(); + */ + + this.graph.change(); + + e.stopPropagation(); + e.preventDefault(); + return false; +} + +/** +* Called when a mouse wheel event has to be processed +* @method processMouseWheel +**/ +LGraphCanvas.prototype.processMouseWheel = function(e) +{ + if(!this.graph || !this.allow_dragcanvas) + return; + + var delta = (e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60); + + this.adjustMouseEvent(e); + + var zoom = this.scale; + + if (delta > 0) + zoom *= 1.1; + else if (delta < 0) + zoom *= 1/(1.1); + + this.setZoom( zoom, [ e.localX, e.localY ] ); + + /* + if(this.rendering_timer_id == null) + this.draw(); + */ + + this.graph.change(); + + e.preventDefault(); + return false; // prevent default +} + +/** +* retuns true if a position (in graph space) is on top of a node little corner box +* @method isOverNodeBox +**/ +LGraphCanvas.prototype.isOverNodeBox = function( node, canvasx, canvasy ) +{ + var title_height = LiteGraph.NODE_TITLE_HEIGHT; + if( isInsideRectangle( canvasx, canvasy, node.pos[0] + 2, node.pos[1] + 2 - title_height, title_height - 4,title_height - 4) ) + return true; + return false; +} + +/** +* retuns true if a position (in graph space) is on top of a node input slot +* @method isOverNodeInput +**/ +LGraphCanvas.prototype.isOverNodeInput = function(node, canvasx, canvasy, slot_pos ) +{ + if(node.inputs) + for(var i = 0, l = node.inputs.length; i < l; ++i) + { + var input = node.inputs[i]; + var link_pos = node.getConnectionPos(true,i); + if( isInsideRectangle(canvasx, canvasy, link_pos[0] - 10, link_pos[1] - 5, 20,10) ) + { + if(slot_pos) + { + slot_pos[0] = link_pos[0]; + slot_pos[1] = link_pos[1]; + } + return i; + } + } + return -1; +} + +/** +* process a key event +* @method processKey +**/ +LGraphCanvas.prototype.processKey = function(e) +{ + if(!this.graph) + return; + + var block_default = false; + //console.log(e); //debug + + if(e.target.localName == "input") + return; + + if(e.type == "keydown") + { + if(e.keyCode == 32) + { + this.dragging_canvas = true; + block_default = true; + } + + //select all Control A + if(e.keyCode == 65 && e.ctrlKey) + { + this.selectNodes(); + block_default = true; + } + + if(e.code == "KeyC" && (e.metaKey || e.ctrlKey) && !e.shiftKey ) //copy + { + if(this.selected_nodes) + { + this.copyToClipboard(); + block_default = true; + } + } + + if(e.code == "KeyV" && (e.metaKey || e.ctrlKey) && !e.shiftKey ) //paste + { + this.pasteFromClipboard(); + } + + //delete or backspace + if(e.keyCode == 46 || e.keyCode == 8) + { + this.deleteSelectedNodes(); + block_default = true; + } + + //collapse + //... + + //TODO + if(this.selected_nodes) + for (var i in this.selected_nodes) + if(this.selected_nodes[i].onKeyDown) + this.selected_nodes[i].onKeyDown(e); + } + else if( e.type == "keyup" ) + { + if(e.keyCode == 32) + this.dragging_canvas = false; + + if(this.selected_nodes) + for (var i in this.selected_nodes) + if(this.selected_nodes[i].onKeyUp) + this.selected_nodes[i].onKeyUp(e); + } + + this.graph.change(); + + if(block_default) + { + e.preventDefault(); + return false; + } +} + +LGraphCanvas.prototype.copyToClipboard = function() +{ + var clipboard_info = { + nodes: [], + links: [] + }; + var index = 0; + var selected_nodes_array = []; + for(var i in this.selected_nodes) + { + var node = this.selected_nodes[i]; + node._relative_id = index; + selected_nodes_array.push( node ); + index += 1; + } + + for(var i = 0; i < selected_nodes_array.length; ++i) + { + var node = selected_nodes_array[i]; + clipboard_info.nodes.push( node.clone().serialize() ); + if(node.inputs && node.inputs.length) + for(var j = 0; j < node.inputs.length; ++j) + { + var input = node.inputs[j]; + if(!input || input.link == null) + continue; + var link_info = this.graph.links[ input.link ]; + if(!link_info) + continue; + var target_node = this.graph.getNodeById( link_info.origin_id ); + if(!target_node || !this.selected_nodes[ target_node.id ] ) //improve this by allowing connections to non-selected nodes + continue; //not selected + clipboard_info.links.push([ target_node._relative_id, j, node._relative_id, link_info.target_slot ]); + } + } + localStorage.setItem( "litegrapheditor_clipboard", JSON.stringify( clipboard_info ) ); +} + +LGraphCanvas.prototype.pasteFromClipboard = function() +{ + var data = localStorage.getItem( "litegrapheditor_clipboard" ); + if(!data) + return; + + //create nodes + var clipboard_info = JSON.parse(data); + var nodes = []; + for(var i = 0; i < clipboard_info.nodes.length; ++i) + { + var node_data = clipboard_info.nodes[i]; + var node = LiteGraph.createNode( node_data.type ); + if(node) + { + node.configure(node_data); + node.pos[0] += 5; + node.pos[1] += 5; + this.graph.add( node ); + nodes.push( node ); + } + } + + //create links + for(var i = 0; i < clipboard_info.links.length; ++i) + { + var link_info = clipboard_info.links[i]; + var origin_node = nodes[ link_info[0] ]; + var target_node = nodes[ link_info[2] ]; + origin_node.connect( link_info[1], target_node, link_info[3] ); + } + + this.selectNodes( nodes ); +} + +/** +* process a item drop event on top the canvas +* @method processDrop +**/ +LGraphCanvas.prototype.processDrop = function(e) +{ + e.preventDefault(); + this.adjustMouseEvent(e); + + + var pos = [e.canvasX,e.canvasY]; + var node = this.graph.getNodeOnPos(pos[0],pos[1]); + + if(!node) + { + var r = null; + if(this.onDropItem) + r = this.onDropItem( event ); + if(!r) + this.checkDropItem(e); + return; + } + + if( node.onDropFile || node.onDropData ) + { + var files = e.dataTransfer.files; + if(files && files.length) + { + for(var i=0; i < files.length; i++) + { + var file = e.dataTransfer.files[0]; + var filename = file.name; + var ext = LGraphCanvas.getFileExtension( filename ); + //console.log(file); + + if(node.onDropFile) + node.onDropFile(file); + + if(node.onDropData) + { + //prepare reader + var reader = new FileReader(); + reader.onload = function (event) { + //console.log(event.target); + var data = event.target.result; + node.onDropData( data, filename, file ); + }; + + //read data + var type = file.type.split("/")[0]; + if(type == "text" || type == "") + reader.readAsText(file); + else if (type == "image") + reader.readAsDataURL(file); + else + reader.readAsArrayBuffer(file); + } + } + } + } + + if(node.onDropItem) + { + if( node.onDropItem( event ) ) + return true; + } + + if(this.onDropItem) + return this.onDropItem( event ); + + return false; +} + +//called if the graph doesnt have a default drop item behaviour +LGraphCanvas.prototype.checkDropItem = function(e) +{ + if(e.dataTransfer.files.length) + { + var file = e.dataTransfer.files[0]; + var ext = LGraphCanvas.getFileExtension( file.name ).toLowerCase(); + var nodetype = LiteGraph.node_types_by_file_extension[ext]; + if(nodetype) + { + var node = LiteGraph.createNode( nodetype.type ); + node.pos = [e.canvasX, e.canvasY]; + this.graph.add( node ); + if( node.onDropFile ) + node.onDropFile( file ); + } + } +} + + +LGraphCanvas.prototype.processNodeDblClicked = function(n) +{ + if(this.onShowNodePanel) + this.onShowNodePanel(n); + + if(this.onNodeDblClicked) + this.onNodeDblClicked(n); + + this.setDirty(true); +} + +LGraphCanvas.prototype.processNodeSelected = function(node,e) +{ + this.selectNode( node, e && e.shiftKey ); + if(this.onNodeSelected) + this.onNodeSelected(node); +} + +LGraphCanvas.prototype.processNodeDeselected = function(node) +{ + this.deselectNode(node); + if(this.onNodeDeselected) + this.onNodeDeselected(node); +} + +/** +* selects a given node (or adds it to the current selection) +* @method selectNode +**/ +LGraphCanvas.prototype.selectNode = function( node, add_to_current_selection ) +{ + if(node == null) + this.deselectAllNodes(); + else + this.selectNodes([node], add_to_current_selection ); +} + +/** +* selects several nodes (or adds them to the current selection) +* @method selectNodes +**/ +LGraphCanvas.prototype.selectNodes = function( nodes, add_to_current_selection ) +{ + if(!add_to_current_selection) + this.deselectAllNodes(); + + nodes = nodes || this.graph._nodes; + for(var i = 0; i < nodes.length; ++i) + { + var node = nodes[i]; + if(node.selected) + continue; + + if( !node.selected && node.onSelected ) + node.onSelected(); + node.selected = true; + this.selected_nodes[ node.id ] = node; + + if(node.inputs) + for(var i = 0; i < node.inputs.length; ++i) + this.highlighted_links[ node.inputs[i].link ] = true; + if(node.outputs) + for(var i = 0; i < node.outputs.length; ++i) + { + var out = node.outputs[i]; + if( out.links ) + for(var j = 0; j < out.links.length; ++j) + this.highlighted_links[ out.links[j] ] = true; + } + + } + + this.setDirty(true); +} + +/** +* removes a node from the current selection +* @method deselectNode +**/ +LGraphCanvas.prototype.deselectNode = function( node ) +{ + if(!node.selected) + return; + if(node.onDeselected) + node.onDeselected(); + node.selected = false; + + //remove highlighted + if(node.inputs) + for(var i = 0; i < node.inputs.length; ++i) + delete this.highlighted_links[ node.inputs[i].link ]; + if(node.outputs) + for(var i = 0; i < node.outputs.length; ++i) + { + var out = node.outputs[i]; + if( out.links ) + for(var j = 0; j < out.links.length; ++j) + delete this.highlighted_links[ out.links[j] ]; + } +} + +/** +* removes all nodes from the current selection +* @method deselectAllNodes +**/ +LGraphCanvas.prototype.deselectAllNodes = function() +{ + if(!this.graph) + return; + var nodes = this.graph._nodes; + for(var i = 0, l = nodes.length; i < l; ++i) + { + var node = nodes[i]; + if(!node.selected) + continue; + if(node.onDeselected) + node.onDeselected(); + node.selected = false; + } + this.selected_nodes = {}; + this.highlighted_links = {}; + this.setDirty(true); +} + +/** +* deletes all nodes in the current selection from the graph +* @method deleteSelectedNodes +**/ +LGraphCanvas.prototype.deleteSelectedNodes = function() +{ + for(var i in this.selected_nodes) + { + var m = this.selected_nodes[i]; + //if(m == this.node_in_panel) this.showNodePanel(null); + this.graph.remove(m); + } + this.selected_nodes = {}; + this.highlighted_links = {}; + this.setDirty(true); +} + +/** +* centers the camera on a given node +* @method centerOnNode +**/ +LGraphCanvas.prototype.centerOnNode = function(node) +{ + this.offset[0] = -node.pos[0] - node.size[0] * 0.5 + (this.canvas.width * 0.5 / this.scale); + this.offset[1] = -node.pos[1] - node.size[1] * 0.5 + (this.canvas.height * 0.5 / this.scale); + this.setDirty(true,true); +} + +/** +* adds some useful properties to a mouse event, like the position in graph coordinates +* @method adjustMouseEvent +**/ +LGraphCanvas.prototype.adjustMouseEvent = function(e) +{ + if(this.canvas) + { + var b = this.canvas.getBoundingClientRect(); + e.localX = e.pageX - b.left; + e.localY = e.pageY - b.top; + } + else + { + e.localX = e.pageX; + e.localY = e.pageY; + } + + e.deltaX = e.localX - this.last_mouse_position[0]; + e.deltaY = e.localY - this.last_mouse_position[1]; + + this.last_mouse_position[0] = e.localX; + this.last_mouse_position[1] = e.localY; + + e.canvasX = e.localX / this.scale - this.offset[0]; + e.canvasY = e.localY / this.scale - this.offset[1]; +} + +/** +* changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom +* @method setZoom +**/ +LGraphCanvas.prototype.setZoom = function(value, zooming_center) +{ + if(!zooming_center && this.canvas) + zooming_center = [this.canvas.width * 0.5,this.canvas.height * 0.5]; + + var center = this.convertOffsetToCanvas( zooming_center ); + + this.scale = value; + + if(this.scale > this.max_zoom) + this.scale = this.max_zoom; + else if(this.scale < this.min_zoom) + this.scale = this.min_zoom; + + var new_center = this.convertOffsetToCanvas( zooming_center ); + var delta_offset = [new_center[0] - center[0], new_center[1] - center[1]]; + + this.offset[0] += delta_offset[0]; + this.offset[1] += delta_offset[1]; + + this.dirty_canvas = true; + this.dirty_bgcanvas = true; +} + +/** +* converts a coordinate in canvas2D space to graphcanvas space (NAME IS CONFUSION, SHOULD BE THE OTHER WAY AROUND) +* @method convertOffsetToCanvas +**/ +LGraphCanvas.prototype.convertOffsetToCanvas = function( pos, out ) +{ + out = out || []; + out[0] = pos[0] / this.scale - this.offset[0]; + out[1] = pos[1] / this.scale - this.offset[1]; + return out; +} + +/** +* converts a coordinate in graphcanvas space to canvas2D space (NAME IS CONFUSION, SHOULD BE THE OTHER WAY AROUND) +* @method convertCanvasToOffset +**/ +LGraphCanvas.prototype.convertCanvasToOffset = function( pos, out ) +{ + out = out || []; + out[0] = (pos[0] + this.offset[0]) * this.scale; + out[1] = (pos[1] + this.offset[1]) * this.scale; + return out; +} + +LGraphCanvas.prototype.convertEventToCanvas = function(e) +{ + var rect = this.canvas.getBoundingClientRect(); + return this.convertOffsetToCanvas([e.pageX - rect.left,e.pageY - rect.top]); +} + +/** +* brings a node to front (above all other nodes) +* @method bringToFront +**/ +LGraphCanvas.prototype.bringToFront = function(node) +{ + var i = this.graph._nodes.indexOf(node); + if(i == -1) return; + + this.graph._nodes.splice(i,1); + this.graph._nodes.push(node); +} + +/** +* sends a node to the back (below all other nodes) +* @method sendToBack +**/ +LGraphCanvas.prototype.sendToBack = function(node) +{ + var i = this.graph._nodes.indexOf(node); + if(i == -1) return; + + this.graph._nodes.splice(i,1); + this.graph._nodes.unshift(node); +} + +/* Interaction */ + + + +/* LGraphCanvas render */ +var temp = new Float32Array(4); + +/** +* checks which nodes are visible (inside the camera area) +* @method computeVisibleNodes +**/ +LGraphCanvas.prototype.computeVisibleNodes = function( nodes, out ) +{ + var visible_nodes = out || []; + visible_nodes.length = 0; + nodes = nodes || this.graph._nodes; + for(var i = 0, l = nodes.length; i < l; ++i) + { + var n = nodes[i]; + + //skip rendering nodes in live mode + if(this.live_mode && !n.onDrawBackground && !n.onDrawForeground) + continue; + + if(!overlapBounding( this.visible_area, n.getBounding( temp ) )) + continue; //out of the visible area + + visible_nodes.push(n); + } + return visible_nodes; +} + +/** +* renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes) +* @method draw +**/ +LGraphCanvas.prototype.draw = function(force_canvas, force_bgcanvas) +{ + if(!this.canvas) + return; + + //fps counting + var now = LiteGraph.getTime(); + this.render_time = (now - this.last_draw_time)*0.001; + this.last_draw_time = now; + + if(this.graph) + { + var start = [-this.offset[0], -this.offset[1] ]; + var end = [start[0] + this.canvas.width / this.scale, start[1] + this.canvas.height / this.scale]; + this.visible_area = new Float32Array([ start[0], start[1], end[0] - start[0], end[1] - start[1] ]); + } + + if(this.dirty_bgcanvas || force_bgcanvas || this.always_render_background || (this.graph && this.graph._last_trigger_time && (now - this.graph._last_trigger_time) < 1000) ) + this.drawBackCanvas(); + + if(this.dirty_canvas || force_canvas) + this.drawFrontCanvas(); + + this.fps = this.render_time ? (1.0 / this.render_time) : 0; + this.frame += 1; +} + +/** +* draws the front canvas (the one containing all the nodes) +* @method drawFrontCanvas +**/ +LGraphCanvas.prototype.drawFrontCanvas = function() +{ + this.dirty_canvas = false; + + if(!this.ctx) + this.ctx = this.bgcanvas.getContext("2d"); + var ctx = this.ctx; + if(!ctx) //maybe is using webgl... + return; + + if(ctx.start2D) + ctx.start2D(); + + var canvas = this.canvas; + + //reset in case of error + ctx.restore(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + + //clip dirty area if there is one, otherwise work in full canvas + if(this.dirty_area) + { + ctx.save(); + ctx.beginPath(); + ctx.rect(this.dirty_area[0],this.dirty_area[1],this.dirty_area[2],this.dirty_area[3]); + ctx.clip(); + } + + //clear + //canvas.width = canvas.width; + if(this.clear_background) + ctx.clearRect(0,0,canvas.width, canvas.height); + + //draw bg canvas + if(this.bgcanvas == this.canvas) + this.drawBackCanvas(); + else + ctx.drawImage(this.bgcanvas,0,0); + + //rendering + if(this.onRender) + this.onRender(canvas, ctx); + + //info widget + if(this.show_info) + this.renderInfo(ctx); + + if(this.graph) + { + //apply transformations + ctx.save(); + ctx.scale(this.scale,this.scale); + ctx.translate( this.offset[0],this.offset[1] ); + + //draw nodes + var drawn_nodes = 0; + var visible_nodes = this.computeVisibleNodes( null, this.visible_nodes ); + + for (var i = 0; i < visible_nodes.length; ++i) + { + var node = visible_nodes[i]; + + //transform coords system + ctx.save(); + ctx.translate( node.pos[0], node.pos[1] ); + + //Draw + this.drawNode( node, ctx ); + drawn_nodes += 1; + + //Restore + ctx.restore(); + } + + //connections ontop? + if(this.graph.config.links_ontop) + if(!this.live_mode) + this.drawConnections(ctx); + + //current connection + if(this.connecting_pos != null) + { + ctx.lineWidth = this.connections_width; + var link_color = null; + switch( this.connecting_output.type ) + { + case LiteGraph.EVENT: link_color = "#F85"; break; + default: + link_color = "#AFA"; + } + //the connection being dragged by the mouse + this.renderLink( ctx, this.connecting_pos, [this.canvas_mouse[0],this.canvas_mouse[1]], null, false, null, link_color ); + + ctx.beginPath(); + if( this.connecting_output.type === LiteGraph.EVENT || this.connecting_output.shape === LiteGraph.BOX_SHAPE ) + ctx.rect( (this.connecting_pos[0] - 6) + 0.5, (this.connecting_pos[1] - 5) + 0.5,14,10); + else + ctx.arc( this.connecting_pos[0], this.connecting_pos[1],4,0,Math.PI*2); + ctx.fill(); + + ctx.fillStyle = "#ffcc00"; + if(this._highlight_input) + { + ctx.beginPath(); + ctx.arc( this._highlight_input[0], this._highlight_input[1],6,0,Math.PI*2); + ctx.fill(); + } + } + + if( this.dragging_rectangle ) + { + ctx.strokeStyle = "#FFF"; + ctx.strokeRect( this.dragging_rectangle[0], this.dragging_rectangle[1], this.dragging_rectangle[2], this.dragging_rectangle[3] ); + } + + + ctx.restore(); + } + + if(this.dirty_area) + { + ctx.restore(); + //this.dirty_area = null; + } + + if(ctx.finish2D) //this is a function I use in webgl renderer + ctx.finish2D(); +} + +/** +* draws some useful stats in the corner of the canvas +* @method renderInfo +**/ +LGraphCanvas.prototype.renderInfo = function( ctx, x, y ) +{ + x = x || 0; + y = y || 0; + + ctx.save(); + ctx.translate( x, y ); + + ctx.font = "10px Arial"; + ctx.fillStyle = "#888"; + if(this.graph) + { + ctx.fillText( "T: " + this.graph.globaltime.toFixed(2)+"s",5,13*1 ); + ctx.fillText( "I: " + this.graph.iteration,5,13*2 ); + ctx.fillText( "V: " + this.graph._version,5,13*3 ); + ctx.fillText( "FPS:" + this.fps.toFixed(2),5,13*4 ); + } + else + ctx.fillText( "No graph selected",5,13*1 ); + ctx.restore(); +} + +/** +* draws the back canvas (the one containing the background and the connections) +* @method drawBackCanvas +**/ +LGraphCanvas.prototype.drawBackCanvas = function() +{ + var canvas = this.bgcanvas; + if(canvas.width != this.canvas.width || + canvas.height != this.canvas.height) + { + canvas.width = this.canvas.width; + canvas.height = this.canvas.height; + } + + if(!this.bgctx) + this.bgctx = this.bgcanvas.getContext("2d"); + var ctx = this.bgctx; + if(ctx.start) + ctx.start(); + + //clear + if(this.clear_background) + ctx.clearRect(0,0,canvas.width, canvas.height); + + if(this._graph_stack && this._graph_stack.length) + { + ctx.strokeStyle = this._graph_stack[ this._graph_stack.length - 1].bgcolor; + ctx.lineWidth = 10; + ctx.strokeRect(1,1,canvas.width-2,canvas.height-2); + ctx.lineWidth = 1; + } + + var bg_already_painted = false; + if(this.onRenderBackground) + bg_already_painted = this.onRenderBackground( canvas, ctx ); + + //reset in case of error + ctx.restore(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + + if(this.graph) + { + //apply transformations + ctx.save(); + ctx.scale(this.scale,this.scale); + ctx.translate(this.offset[0],this.offset[1]); + + //render BG + if(this.background_image && this.scale > 0.5 && !bg_already_painted) + { + if (this.zoom_modify_alpha) + ctx.globalAlpha = (1.0 - 0.5 / this.scale) * this.editor_alpha; + else + ctx.globalAlpha = this.editor_alpha; + ctx.imageSmoothingEnabled = ctx.mozImageSmoothingEnabled = ctx.imageSmoothingEnabled = false; + if(!this._bg_img || this._bg_img.name != this.background_image) + { + this._bg_img = new Image(); + this._bg_img.name = this.background_image; + this._bg_img.src = this.background_image; + var that = this; + this._bg_img.onload = function() { + that.draw(true,true); + } + } + + var pattern = null; + if(this._pattern == null && this._bg_img.width > 0) + { + pattern = ctx.createPattern( this._bg_img, 'repeat' ); + this._pattern_img = this._bg_img; + this._pattern = pattern; + } + else + pattern = this._pattern; + if(pattern) + { + ctx.fillStyle = pattern; + ctx.fillRect(this.visible_area[0],this.visible_area[1],this.visible_area[2],this.visible_area[3]); + ctx.fillStyle = "transparent"; + } + + ctx.globalAlpha = 1.0; + ctx.imageSmoothingEnabled = ctx.mozImageSmoothingEnabled = ctx.imageSmoothingEnabled = true; + } + + //groups + if(this.graph._groups.length && !this.live_mode) + this.drawGroups(canvas, ctx); + + if(this.onBackgroundRender) + this.onBackgroundRender(canvas, ctx); + + //DEBUG: show clipping area + //ctx.fillStyle = "red"; + //ctx.fillRect( this.visible_area[0] + 10, this.visible_area[1] + 10, this.visible_area[2] - 20, this.visible_area[3] - 20); + + //bg + if (this.render_canvas_border) { + ctx.strokeStyle = "#235"; + ctx.strokeRect(0,0,canvas.width,canvas.height); + } + + if(this.render_connections_shadows) + { + ctx.shadowColor = "#000"; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + ctx.shadowBlur = 6; + } + else + ctx.shadowColor = "rgba(0,0,0,0)"; + + //draw connections + if(!this.live_mode) + this.drawConnections(ctx); + + ctx.shadowColor = "rgba(0,0,0,0)"; + + //restore state + ctx.restore(); + } + + if(ctx.finish) + ctx.finish(); + + this.dirty_bgcanvas = false; + this.dirty_canvas = true; //to force to repaint the front canvas with the bgcanvas +} + +var temp_vec2 = new Float32Array(2); + +/** +* draws the given node inside the canvas +* @method drawNode +**/ +LGraphCanvas.prototype.drawNode = function(node, ctx ) +{ + var glow = false; + this.current_node = node; + + var color = node.color || node.constructor.color || LiteGraph.NODE_DEFAULT_COLOR; + var bgcolor = node.bgcolor || node.constructor.bgcolor || LiteGraph.NODE_DEFAULT_BGCOLOR; + + //shadow and glow + if (node.mouseOver) + glow = true; + + if(node.selected) + { + /* + ctx.shadowColor = "#EEEEFF";//glow ? "#AAF" : "#000"; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + ctx.shadowBlur = 1; + */ + } + else if(this.render_shadows) + { + ctx.shadowColor = "rgba(0,0,0,0.5)"; + ctx.shadowOffsetX = 2; + ctx.shadowOffsetY = 2; + ctx.shadowBlur = 3; + } + else + ctx.shadowColor = "transparent"; + + //only render if it forces it to do it + if(this.live_mode) + { + if(!node.flags.collapsed) + { + ctx.shadowColor = "transparent"; + if(node.onDrawForeground) + node.onDrawForeground(ctx, this); + } + + return; + } + + //custom draw collapsed method + if(node.flags.collapsed && node.onDrawCollaped && node.onDrawCollapsed(ctx, this) == true) + return; + + var editor_alpha = this.editor_alpha; + ctx.globalAlpha = editor_alpha; + + //clip if required (mask) + var shape = node._shape || LiteGraph.BOX_SHAPE; + var size = temp_vec2; + temp_vec2.set( node.size ); + if( node.flags.collapsed ) + { + ctx.font = this.inner_text_font; + var title = node.getTitle ? node.getTitle() : node.title; + node._collapsed_width = Math.min( node.size[0], ctx.measureText(title).width + 40 );//LiteGraph.NODE_COLLAPSED_WIDTH; + size[0] = node._collapsed_width; + size[1] = 0; + } + + if( node.flags.clip_area ) //Start clipping + { + ctx.save(); + ctx.beginPath(); + if(shape == LiteGraph.BOX_SHAPE) + ctx.rect(0,0,size[0], size[1]); + else if (shape == LiteGraph.ROUND_SHAPE) + ctx.roundRect(0,0,size[0], size[1],10); + else if (shape == LiteGraph.CIRCLE_SHAPE) + ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5, 0, Math.PI*2); + ctx.clip(); + } + + //draw shape + this.drawNodeShape( node, ctx, size, color, bgcolor, node.selected, node.mouseOver ); + ctx.shadowColor = "transparent"; + + //connection slots + ctx.textAlign = "left"; + ctx.font = this.inner_text_font; + + var render_text = this.scale > 0.6; + + var out_slot = this.connecting_output; + ctx.lineWidth = 1; + + var max_y = 0; + + //render inputs and outputs + if(!node.flags.collapsed) + { + //input connection slots + if(node.inputs) + for(var i = 0; i < node.inputs.length; i++) + { + var slot = node.inputs[i]; + + ctx.globalAlpha = editor_alpha; + //change opacity of incompatible slots when dragging a connection + if ( this.connecting_node && LiteGraph.isValidConnection( slot.type && out_slot.type ) ) + ctx.globalAlpha = 0.4 * editor_alpha; + + ctx.fillStyle = slot.link != null ? (slot.colorOn || this.default_connection_color.input_on) : (slot.colorOff || this.default_connection_color.input_off); + + var pos = node.getConnectionPos( true, i ); + pos[0] -= node.pos[0]; + pos[1] -= node.pos[1]; + if( max_y < pos[1] + LiteGraph.NODE_SLOT_HEIGHT*0.5 ) + max_y = pos[1] + LiteGraph.NODE_SLOT_HEIGHT*0.5; + + ctx.beginPath(); + + if (slot.type === LiteGraph.EVENT || slot.shape === LiteGraph.BOX_SHAPE) { + ctx.rect((pos[0] - 6) + 0.5, (pos[1] - 5) + 0.5, 14, 10); + } else if (slot.shape === LiteGraph.ARROW_SHAPE) { + ctx.moveTo(pos[0] + 8, pos[1] + 0.5); + ctx.lineTo(pos[0] - 4, (pos[1] + 6) + 0.5); + ctx.lineTo(pos[0] - 4, (pos[1] - 6) + 0.5); + ctx.closePath(); + } else { + ctx.arc(pos[0], pos[1], 4, 0, Math.PI * 2); + } + + ctx.fill(); + + //render name + if(render_text) + { + var text = slot.label != null ? slot.label : slot.name; + if(text) + { + ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR; + ctx.fillText(text,pos[0] + 10,pos[1] + 5); + } + } + } + + //output connection slots + if(this.connecting_node) + ctx.globalAlpha = 0.4 * editor_alpha; + + ctx.textAlign = "right"; + ctx.strokeStyle = "black"; + if(node.outputs) + for(var i = 0; i < node.outputs.length; i++) + { + var slot = node.outputs[i]; + + var pos = node.getConnectionPos(false,i); + pos[0] -= node.pos[0]; + pos[1] -= node.pos[1]; + if( max_y < pos[1] + LiteGraph.NODE_SLOT_HEIGHT*0.5) + max_y = pos[1] + LiteGraph.NODE_SLOT_HEIGHT*0.5; + + ctx.fillStyle = slot.links && slot.links.length ? (slot.colorOn || this.default_connection_color.output_on) : (slot.colorOff || this.default_connection_color.output_off); + ctx.beginPath(); + //ctx.rect( node.size[0] - 14,i*14,10,10); + + if (slot.type === LiteGraph.EVENT || slot.shape === LiteGraph.BOX_SHAPE) { + ctx.rect((pos[0] - 6) + 0.5,(pos[1] - 5) + 0.5,14,10); + } else if (slot.shape === LiteGraph.ARROW_SHAPE) { + ctx.moveTo(pos[0] + 8, pos[1] + 0.5); + ctx.lineTo(pos[0] - 4, (pos[1] + 6) + 0.5); + ctx.lineTo(pos[0] - 4, (pos[1] - 6) + 0.5); + ctx.closePath(); + } else { + ctx.arc(pos[0], pos[1], 4, 0, Math.PI * 2); + } + + //trigger + //if(slot.node_id != null && slot.slot == -1) + // ctx.fillStyle = "#F85"; + + //if(slot.links != null && slot.links.length) + ctx.fill(); + ctx.stroke(); + + //render output name + if(render_text) + { + var text = slot.label != null ? slot.label : slot.name; + if(text) + { + ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR; + ctx.fillText(text, pos[0] - 10,pos[1] + 5); + } + } + } + + ctx.textAlign = "left"; + ctx.globalAlpha = 1; + + if(node.widgets) + this.drawNodeWidgets( node, max_y, ctx, (this.node_widget && this.node_widget[0] == node) ? this.node_widget[1] : null ); + + //draw foreground + if(node.onDrawForeground) + { + //immediate gui stuff + if( node.gui_rects ) + node.gui_rects.length = 0; + node.onDrawForeground( ctx, this ); + } + } + else //if collapsed + { + if(node.inputs) + { + for(var i = 0; i < node.inputs.length; i++) + { + var slot = node.inputs[i]; + if( slot.link == null ) + continue; + ctx.fillStyle = slot.colorOn || this.default_connection_color.input_on; + ctx.beginPath(); + if ( slot.type === LiteGraph.EVENT || slot.shape === LiteGraph.BOX_SHAPE) { + ctx.rect(0.5, 4 - LiteGraph.NODE_TITLE_HEIGHT + 0.5,14,LiteGraph.NODE_TITLE_HEIGHT - 8); + } else if (slot.shape === LiteGraph.ARROW_SHAPE) { + ctx.moveTo(8, LiteGraph.NODE_TITLE_HEIGHT * -0.5); + ctx.lineTo(-4, LiteGraph.NODE_TITLE_HEIGHT * -0.8); + ctx.lineTo(-4, LiteGraph.NODE_TITLE_HEIGHT * -0.2); + ctx.closePath(); + } else { + ctx.arc(0, LiteGraph.NODE_TITLE_HEIGHT * -0.5, 4, 0, Math.PI * 2); + } + ctx.fill(); + break; + } + } + + if(node.outputs) + { + for(var i = 0; i < node.outputs.length; i++) + { + var slot = node.outputs[i]; + if(!slot.links || !slot.links.length) + continue; + ctx.fillStyle = slot.colorOn || this.default_connection_color.output_on; + ctx.strokeStyle = "black"; + ctx.beginPath(); + if (slot.type === LiteGraph.EVENT || slot.shape === LiteGraph.BOX_SHAPE) { + ctx.rect( node._collapsed_width - 4 + 0.5, 4 - LiteGraph.NODE_TITLE_HEIGHT + 0.5,14,LiteGraph.NODE_TITLE_HEIGHT - 8); + } else if (slot.shape === LiteGraph.ARROW_SHAPE) { + ctx.moveTo(node._collapsed_width + 6, LiteGraph.NODE_TITLE_HEIGHT * -0.5); + ctx.lineTo(node._collapsed_width - 6, LiteGraph.NODE_TITLE_HEIGHT * -0.8); + ctx.lineTo(node._collapsed_width - 6, LiteGraph.NODE_TITLE_HEIGHT * -0.2); + ctx.closePath(); + } else { + ctx.arc(node._collapsed_width, LiteGraph.NODE_TITLE_HEIGHT * -0.5, 4, 0, Math.PI * 2); + } + ctx.fill(); + ctx.stroke(); + } + } + + } + + if(node.flags.clip_area) + ctx.restore(); + + ctx.globalAlpha = 1.0; +} + +/** +* draws the shape of the given node in the canvas +* @method drawNodeShape +**/ +LGraphCanvas.prototype.drawNodeShape = function( node, ctx, size, fgcolor, bgcolor, selected, mouse_over ) +{ + //bg rect + ctx.strokeStyle = fgcolor; + ctx.fillStyle = bgcolor; + + var title_height = LiteGraph.NODE_TITLE_HEIGHT; + + //render node area depending on shape + var shape = node._shape || node.constructor.shape || LiteGraph.BOX_SHAPE; + var title_mode = node.constructor.title_mode; + + var render_title = true; + if( title_mode == LiteGraph.TRANSPARENT_TITLE ) + render_title = false; + else if( title_mode == LiteGraph.AUTOHIDE_TITLE && mouse_over) + render_title = true; + + var areax = 0; + var areay = render_title ? -title_height : 0; + var areaw = size[0]+1; + var areah = render_title ? size[1] + title_height : size[1]; + + if(!node.flags.collapsed) + { + if(shape == LiteGraph.BOX_SHAPE || this.scale < 0.5) + { + ctx.beginPath(); + ctx.rect( areax, areay, areaw, areah ); + ctx.fill(); + } + else if (shape == LiteGraph.ROUND_SHAPE || shape == LiteGraph.CARD_SHAPE) + { + ctx.beginPath(); + ctx.roundRect( areax, areay, areaw, areah, this.round_radius, shape == LiteGraph.CARD_SHAPE ? 0 : this.round_radius); + ctx.fill(); + } + else if (shape == LiteGraph.CIRCLE_SHAPE) + { + ctx.beginPath(); + ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5, 0, Math.PI*2); + ctx.fill(); + } + } + ctx.shadowColor = "transparent"; + + //image + if (node.bgImage && node.bgImage.width) + ctx.drawImage( node.bgImage, (size[0] - node.bgImage.width) * 0.5 , (size[1] - node.bgImage.height) * 0.5); + + if(node.bgImageUrl && !node.bgImage) + node.bgImage = node.loadImage(node.bgImageUrl); + + if( node.onDrawBackground ) + { + //immediate gui stuff + if( node.gui_rects ) + node.gui_rects.length = 0; + node.onDrawBackground( ctx, this ); + } + + //title bg (remember, it is rendered ABOVE the node) + if(render_title || title_mode == LiteGraph.TRANSPARENT_TITLE ) + { + //title bar + if(title_mode != LiteGraph.TRANSPARENT_TITLE) //!node.flags.collapsed) + { + //* gradient test + if(this.use_gradients) + { + var grad = LGraphCanvas.gradients[ fgcolor ]; + if(!grad) + { + grad = LGraphCanvas.gradients[ fgcolor ] = ctx.createLinearGradient(0,0,400,0); + grad.addColorStop(0, fgcolor); + grad.addColorStop(1, "#000"); + } + ctx.fillStyle = grad; + } + else + ctx.fillStyle = fgcolor; + + var old_alpha = ctx.globalAlpha; + //ctx.globalAlpha = 0.5 * old_alpha; + ctx.beginPath(); + if(shape == LiteGraph.BOX_SHAPE || this.scale < 0.5) + { + ctx.rect(0, -title_height, size[0]+1, title_height); + ctx.fill() + //ctx.stroke(); + } + else if (shape == LiteGraph.ROUND_SHAPE || shape == LiteGraph.CARD_SHAPE) + { + ctx.roundRect(0,-title_height,size[0]+1, title_height, this.round_radius, node.flags.collapsed ? this.round_radius : 0); + ctx.fill(); + } + + /* + else if (shape == LiteGraph.CIRCLE_SHAPE) + { + ctx.beginPath(); + ctx.arc(title_height *0.5, title_height * -0.5, (title_height - 6) *0.5,0,Math.PI*2); + ctx.fill(); + } + */ + } + + //title box + ctx.fillStyle = node.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR; + ctx.beginPath(); + if (shape == LiteGraph.ROUND_SHAPE || shape == LiteGraph.CIRCLE_SHAPE || shape == LiteGraph.CARD_SHAPE) + ctx.arc(title_height *0.5, title_height * -0.5, (title_height - 8) *0.5,0,Math.PI*2); + else + ctx.rect(4,-title_height + 4,title_height - 8,title_height - 8); + ctx.fill(); + ctx.globalAlpha = old_alpha; + + //title text + if( this.scale > 0.5 ) + { + ctx.font = this.title_text_font; + var title = node.getTitle(); + if(title) + { + if(selected) + ctx.fillStyle = "white"; + else + ctx.fillStyle = node.constructor.title_text_color || this.node_title_color; + if( node.flags.collapsed ) + { + ctx.textAlign = "center"; + var measure = ctx.measureText(title); + ctx.fillText( title, title_height + measure.width * 0.5, -title_height * 0.2 ); + ctx.textAlign = "left"; + } + else + { + ctx.textAlign = "left"; + ctx.fillText( title, title_height, -title_height * 0.2 ); + } + } + } + } + + //render selection marker + if(selected) + { + if( title_mode == LiteGraph.TRANSPARENT_TITLE ) + { + areay -= title_height; + areah += title_height; + } + ctx.lineWidth = 1; + ctx.beginPath(); + if(shape == LiteGraph.BOX_SHAPE) + ctx.rect(-6 + areax,-6 + areay, 12 + areaw, 12 + areah ); + else if (shape == LiteGraph.ROUND_SHAPE || (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed) ) + ctx.roundRect(-6 + areax,-6 + areay, 12 + areaw, 12 + areah , this.round_radius * 2); + else if (shape == LiteGraph.CARD_SHAPE) + ctx.roundRect(-6 + areax,-6 + areay, 12 + areaw, 12 + areah , this.round_radius * 2, 2); + else if (shape == LiteGraph.CIRCLE_SHAPE) + ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI*2); + ctx.strokeStyle = "#DDD"; + ctx.stroke(); + ctx.strokeStyle = fgcolor; + } +} + +/** +* draws every connection visible in the canvas +* OPTIMIZE THIS: precatch connections position instead of recomputing them every time +* @method drawConnections +**/ +LGraphCanvas.prototype.drawConnections = function(ctx) +{ + var now = LiteGraph.getTime(); + + //draw connections + ctx.lineWidth = this.connections_width; + + ctx.fillStyle = "#AAA"; + ctx.strokeStyle = "#AAA"; + ctx.globalAlpha = this.editor_alpha; + //for every node + for (var n = 0, l = this.graph._nodes.length; n < l; ++n) + { + var node = this.graph._nodes[n]; + //for every input (we render just inputs because it is easier as every slot can only have one input) + if(node.inputs && node.inputs.length) + for(var i = 0; i < node.inputs.length; ++i) + { + var input = node.inputs[i]; + if(!input || input.link == null) + continue; + var link_id = input.link; + var link = this.graph.links[ link_id ]; + if(!link) + continue; + + var start_node = this.graph.getNodeById( link.origin_id ); + if(start_node == null) continue; + var start_node_slot = link.origin_slot; + var start_node_slotpos = null; + + if(start_node_slot == -1) + start_node_slotpos = [start_node.pos[0] + 10, start_node.pos[1] + 10]; + else + start_node_slotpos = start_node.getConnectionPos(false, start_node_slot); + + this.renderLink( ctx, start_node_slotpos, node.getConnectionPos(true,i), link ); + + //event triggered rendered on top + if(link && link._last_time && (now - link._last_time) < 1000 ) + { + var f = 2.0 - (now - link._last_time) * 0.002; + var color = "rgba(255,255,255, " + f.toFixed(2) + ")"; + this.renderLink( ctx, start_node_slotpos, node.getConnectionPos(true,i), link, true, f, color ); + } + } + } + ctx.globalAlpha = 1; +} + +/** +* draws a link between two points +* @method renderLink +**/ +LGraphCanvas.prototype.renderLink = function( ctx, a, b, link, skip_border, flow, color ) +{ + if(!this.highquality_render) + { + ctx.beginPath(); + ctx.moveTo(a[0],a[1]); + ctx.lineTo(b[0],b[1]); + ctx.stroke(); + return; + } + + var dist = distance(a,b); + + if(this.render_connections_border && this.scale > 0.6) + ctx.lineWidth = this.connections_width + 4; + + //choose color + if( !color && link ) + color = LGraphCanvas.link_type_colors[ link.type ]; + if(!color) + color = this.default_link_color; + + if( link != null && this.highlighted_links[ link.id ] ) + color = "#FFF"; + + //begin line shape + ctx.beginPath(); + + if(this.render_curved_connections) //splines + { + ctx.moveTo(a[0],a[1]); + ctx.bezierCurveTo(a[0] + dist*0.25, a[1], + b[0] - dist*0.25 , b[1], + b[0] ,b[1] ); + } + else //lines + { + ctx.moveTo(a[0]+10,a[1]); + ctx.lineTo(((a[0]+10) + (b[0]-10))*0.5,a[1]); + ctx.lineTo(((a[0]+10) + (b[0]-10))*0.5,b[1]); + ctx.lineTo(b[0]-10,b[1]); + } + + //rendering the outline of the connection can be a little bit slow + if(this.render_connections_border && this.scale > 0.6 && !skip_border) + { + ctx.strokeStyle = "rgba(0,0,0,0.5)"; + ctx.stroke(); + } + + ctx.lineWidth = this.connections_width; + ctx.fillStyle = ctx.strokeStyle = color; + ctx.stroke(); + //end line shape + + //render arrow in the middle + if( this.render_connection_arrows && this.scale >= 0.6 ) + { + //render arrow + if(this.render_connection_arrows && this.scale > 0.6) + { + //compute two points in the connection + var pos = this.computeConnectionPoint(a,b,0.5); + var pos2 = this.computeConnectionPoint(a,b,0.51); + + //compute the angle between them so the arrow points in the right direction + var angle = 0; + if(this.render_curved_connections) + angle = -Math.atan2( pos2[0] - pos[0], pos2[1] - pos[1]); + else + angle = b[1] > a[1] ? 0 : Math.PI; + + //render arrow + ctx.save(); + ctx.translate(pos[0],pos[1]); + ctx.rotate(angle); + ctx.beginPath(); + ctx.moveTo(-5,-5); + ctx.lineTo(0,+5); + ctx.lineTo(+5,-5); + ctx.fill(); + ctx.restore(); + } + } + + //render flowing points + if(flow) + { + for(var i = 0; i < 5; ++i) + { + var f = (LiteGraph.getTime() * 0.001 + (i * 0.2)) % 1; + var pos = this.computeConnectionPoint(a,b,f); + ctx.beginPath(); + ctx.arc(pos[0],pos[1],5,0,2*Math.PI); + ctx.fill(); + } + } +} + +LGraphCanvas.prototype.computeConnectionPoint = function(a,b,t) +{ + var dist = distance(a,b); + var p0 = a; + var p1 = [ a[0] + dist*0.25, a[1] ]; + var p2 = [ b[0] - dist*0.25, b[1] ]; + var p3 = b; + + var c1 = (1-t)*(1-t)*(1-t); + var c2 = 3*((1-t)*(1-t))*t; + var c3 = 3*(1-t)*(t*t); + var c4 = t*t*t; + + var x = c1*p0[0] + c2*p1[0] + c3*p2[0] + c4*p3[0]; + var y = c1*p0[1] + c2*p1[1] + c3*p2[1] + c4*p3[1]; + return [x,y]; +} + +/** +* draws the widgets stored inside a node +* @method drawNodeWidgets +**/ +LGraphCanvas.prototype.drawNodeWidgets = function( node, posY, ctx, active_widget ) +{ + if(!node.widgets || !node.widgets.length) + return 0; + var width = node.size[0]; + var widgets = node.widgets; + posY += 2; + var H = LiteGraph.NODE_WIDGET_HEIGHT; + var show_text = this.scale > 0.5; + + for(var i = 0; i < widgets.length; ++i) + { + var w = widgets[i]; + var y = posY; + if(w.y) + y = w.y; + w.last_y = y; + ctx.strokeStyle = "#AAA"; + ctx.fillStyle = "#222"; + ctx.textAlign = "left"; + + switch( w.type ) + { + case "button": + if(w.clicked) + { + ctx.fillStyle = "#AAA"; + w.clicked = false; + this.dirty_canvas = true; + } + ctx.fillRect(10,y,width-20,H); + ctx.strokeRect(10,y,width-20,H); + if(show_text) + { + ctx.textAlign = "center"; + ctx.fillStyle = "#AAA"; + ctx.fillText( w.name, width*0.5, y + H*0.7 ); + } + break; + case "slider": + ctx.fillStyle = "#111"; + ctx.fillRect(10,y,width-20,H); + var range = w.options.max - w.options.min; + var nvalue = (w.value - w.options.min) / range; + ctx.fillStyle = active_widget == w ? "#89A" : "#678"; + ctx.fillRect(10,y,nvalue*(width-20),H); + ctx.strokeRect(10,y,width-20,H); + if(show_text) + { + ctx.textAlign = "center"; + ctx.fillStyle = "#DDD"; + ctx.fillText( w.name + " " + Number(w.value).toFixed(3), width*0.5, y + H*0.7 ); + } + break; + case "number": + case "combo": + ctx.textAlign = "left"; + ctx.strokeStyle = "#AAA"; + ctx.fillStyle = "#111"; + ctx.beginPath(); + ctx.roundRect( 10, posY, width - 20, H,H*0.5 ); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = "#AAA"; + ctx.beginPath(); + ctx.moveTo( 26, posY + 5 ); + ctx.lineTo( 16, posY + H*0.5 ); + ctx.lineTo( 26, posY + H - 5 ); + ctx.moveTo( width - 26, posY + 5 ); + ctx.lineTo( width - 16, posY + H*0.5 ); + ctx.lineTo( width - 26, posY + H - 5 ); + ctx.fill(); + if(show_text) + { + ctx.fillStyle = "#999"; + ctx.fillText( w.name, 30, y + H*0.7 ); + ctx.fillStyle = "#DDD"; + ctx.textAlign = "right"; + if(w.type == "number") + ctx.fillText( Number(w.value).toFixed( w.options.precision !== undefined ? w.options.precision : 3), width - 40, y + H*0.7 ); + else + ctx.fillText( w.value, width - 40, y + H*0.7 ); + } + break; + default: + break; + } + posY += H + 4; + } + ctx.textAlign = "left"; +} + +/** +* process an event on widgets +* @method processNodeWidgets +**/ +LGraphCanvas.prototype.processNodeWidgets = function( node, pos, event, active_widget ) +{ + if(!node.widgets || !node.widgets.length) + return null; + + var x = pos[0] - node.pos[0]; + var y = pos[1] - node.pos[1]; + var width = node.size[0]; + var that = this; + + for(var i = 0; i < node.widgets.length; ++i) + { + var w = node.widgets[i]; + if( w == active_widget || (x > 6 && x < (width - 12) && y > w.last_y && y < (w.last_y + LiteGraph.NODE_WIDGET_HEIGHT)) ) + { + //inside widget + switch( w.type ) + { + case "button": + if(w.callback) + setTimeout( function(){ w.callback( w, that, node, pos ); }, 20 ); + w.clicked = true; + this.dirty_canvas = true; + break; + case "slider": + var range = w.options.max - w.options.min; + var nvalue = Math.clamp( (x - 10) / (width - 20), 0, 1); + w.value = w.options.min + (w.options.max - w.options.min) * nvalue; + if(w.callback) + setTimeout( function(){ w.callback( w.value, that, node, pos ); }, 20 ); + this.dirty_canvas = true; + break; + case "number": + case "combo": + if(event.type == "mousemove" && w.type == "number") + { + w.value += (event.deltaX * 0.1) * (w.options.step || 1); + if(w.options.min != null && w.value < w.options.min) + w.value = w.options.min; + if(w.options.max != null && w.value > w.options.max) + w.value = w.options.max; + } + else if( event.type == "mousedown" ) + { + var delta = ( x < 40 ? -1 : ( x > width - 40 ? 1 : 0) ); + if (w.type == "number") + { + w.value += delta * 0.1 * (w.options.step || 1); + if(w.options.min != null && w.value < w.options.min) + w.value = w.options.min; + if(w.options.max != null && w.value > w.options.max) + w.value = w.options.max; + } + else if(delta) + { + var index = w.options.values.indexOf( w.value ) + delta; + if( index >= w.options.values.length ) + index = 0; + if( index < 0 ) + index = w.options.values.length - 1; + w.value = w.options.values[ index ]; + } + } + if(w.callback) + setTimeout( function(){ w.callback( w.value, that, node, pos ); }, 20 ); + this.dirty_canvas = true; + break; + } + + return w; + } + } + return null; +} + +/** +* draws every group area in the background +* @method drawGroups +**/ +LGraphCanvas.prototype.drawGroups = function(canvas, ctx) +{ + if(!this.graph) + return; + + var groups = this.graph._groups; + + ctx.save(); + ctx.globalAlpha = 0.5; + ctx.font = "24px Arial"; + + for(var i = 0; i < groups.length; ++i) + { + var group = groups[i]; + + if(!overlapBounding( this.visible_area, group._bounding )) + continue; //out of the visible area + + ctx.fillStyle = group.color || "#335"; + ctx.strokeStyle = group.color || "#335"; + var pos = group._pos; + var size = group._size; + ctx.globalAlpha = 0.25; + ctx.beginPath(); + ctx.rect( pos[0] + 0.5, pos[1] + 0.5, size[0], size[1] ); + ctx.fill(); + ctx.globalAlpha = 1; + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo( pos[0] + size[0], pos[1] + size[1] ); + ctx.lineTo( pos[0] + size[0] - 10, pos[1] + size[1] ); + ctx.lineTo( pos[0] + size[0], pos[1] + size[1] - 10 ); + ctx.fill(); + + ctx.fillText( group.title, pos[0] + 4, pos[1] + 24 ); + } + + ctx.restore(); +} + +/** +* resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode +* @method resize +**/ +LGraphCanvas.prototype.resize = function(width, height) +{ + if(!width && !height) + { + var parent = this.canvas.parentNode; + width = parent.offsetWidth; + height = parent.offsetHeight; + } + + if(this.canvas.width == width && this.canvas.height == height) + return; + + this.canvas.width = width; + this.canvas.height = height; + this.bgcanvas.width = this.canvas.width; + this.bgcanvas.height = this.canvas.height; + this.setDirty(true,true); +} + +/** +* switches to live mode (node shapes are not rendered, only the content) +* this feature was designed when graphs where meant to create user interfaces +* @method switchLiveMode +**/ +LGraphCanvas.prototype.switchLiveMode = function(transition) +{ + if(!transition) + { + this.live_mode = !this.live_mode; + this.dirty_canvas = true; + this.dirty_bgcanvas = true; + return; + } + + var self = this; + var delta = this.live_mode ? 1.1 : 0.9; + if(this.live_mode) + { + this.live_mode = false; + this.editor_alpha = 0.1; + } + + var t = setInterval(function() { + self.editor_alpha *= delta; + self.dirty_canvas = true; + self.dirty_bgcanvas = true; + + if(delta < 1 && self.editor_alpha < 0.01) + { + clearInterval(t); + if(delta < 1) + self.live_mode = true; + } + if(delta > 1 && self.editor_alpha > 0.99) + { + clearInterval(t); + self.editor_alpha = 1; + } + },1); +} + +LGraphCanvas.prototype.onNodeSelectionChange = function(node) +{ + return; //disabled +} + +LGraphCanvas.prototype.touchHandler = function(event) +{ + //alert("foo"); + var touches = event.changedTouches, + first = touches[0], + type = ""; + + switch(event.type) + { + case "touchstart": type = "mousedown"; break; + case "touchmove": type = "mousemove"; break; + case "touchend": type = "mouseup"; break; + default: return; + } + + //initMouseEvent(type, canBubble, cancelable, view, clickCount, + // screenX, screenY, clientX, clientY, ctrlKey, + // altKey, shiftKey, metaKey, button, relatedTarget); + + var window = this.getCanvasWindow(); + var document = window.document; + + var simulatedEvent = document.createEvent("MouseEvent"); + simulatedEvent.initMouseEvent(type, true, true, window, 1, + first.screenX, first.screenY, + first.clientX, first.clientY, false, + false, false, false, 0/*left*/, null); + first.target.dispatchEvent(simulatedEvent); + event.preventDefault(); +} + +/* CONTEXT MENU ********************/ + +LGraphCanvas.onGroupAdd = function(info,entry,mouse_event) +{ + var canvas = LGraphCanvas.active_canvas; + var ref_window = canvas.getCanvasWindow(); + + var group = new LiteGraph.LGraphGroup(); + group.pos = canvas.convertEventToCanvas( mouse_event ); + canvas.graph.add( group ); +} + +LGraphCanvas.onMenuAdd = function( node, options, e, prev_menu ) +{ + var canvas = LGraphCanvas.active_canvas; + var ref_window = canvas.getCanvasWindow(); + + var values = LiteGraph.getNodeTypesCategories(); + var entries = []; + for(var i in values) + if(values[i]) + entries.push({ value: values[i], content: values[i], has_submenu: true }); + + var menu = new LiteGraph.ContextMenu( entries, { event: e, callback: inner_clicked, parentMenu: prev_menu }, ref_window); + + function inner_clicked( v, option, e ) + { + var category = v.value; + var node_types = LiteGraph.getNodeTypesInCategory( category, canvas.filter ); + var values = []; + for(var i in node_types) + values.push( { content: node_types[i].title, value: node_types[i].type }); + + new LiteGraph.ContextMenu( values, {event: e, callback: inner_create, parentMenu: menu }, ref_window); + return false; + } + + function inner_create( v, e ) + { + var first_event = prev_menu.getFirstEvent(); + var node = LiteGraph.createNode( v.value ); + if(node) + { + node.pos = canvas.convertEventToCanvas( first_event ); + canvas.graph.add( node ); + } + } + + return false; +} + +LGraphCanvas.onMenuCollapseAll = function() +{ + +} + + +LGraphCanvas.onMenuNodeEdit = function() +{ + +} + +LGraphCanvas.showMenuNodeOptionalInputs = function( v, options, e, prev_menu, node ) +{ + if(!node) + return; + + var that = this; + var canvas = LGraphCanvas.active_canvas; + var ref_window = canvas.getCanvasWindow(); + + var options = node.optional_inputs; + if(node.onGetInputs) + options = node.onGetInputs(); + + var entries = []; + if(options) + for (var i in options) + { + var entry = options[i]; + if(!entry) + { + entries.push(null); + continue; + } + var label = entry[0]; + if(entry[2] && entry[2].label) + label = entry[2].label; + var data = {content: label, value: entry}; + if(entry[1] == LiteGraph.ACTION) + data.className = "event"; + entries.push(data); + } + + if(this.onMenuNodeInputs) + entries = this.onMenuNodeInputs( entries ); + + if(!entries.length) + return; + + var menu = new LiteGraph.ContextMenu(entries, { event: e, callback: inner_clicked, parentMenu: prev_menu, node: node }, ref_window); + + function inner_clicked(v, e, prev) + { + if(!node) + return; + + if(v.callback) + v.callback.call( that, node, v, e, prev ); + + if(v.value) + { + node.addInput(v.value[0],v.value[1], v.value[2]); + node.setDirtyCanvas(true,true); + } + } + + return false; +} + +LGraphCanvas.showMenuNodeOptionalOutputs = function( v, options, e, prev_menu, node ) +{ + if(!node) + return; + + var that = this; + var canvas = LGraphCanvas.active_canvas; + var ref_window = canvas.getCanvasWindow(); + + var options = node.optional_outputs; + if(node.onGetOutputs) + options = node.onGetOutputs(); + + var entries = []; + if(options) + for (var i in options) + { + var entry = options[i]; + if(!entry) //separator? + { + entries.push(null); + continue; + } + + if(node.flags && node.flags.skip_repeated_outputs && node.findOutputSlot(entry[0]) != -1) + continue; //skip the ones already on + var label = entry[0]; + if(entry[2] && entry[2].label) + label = entry[2].label; + var data = {content: label, value: entry}; + if(entry[1] == LiteGraph.EVENT) + data.className = "event"; + entries.push(data); + } + + if(this.onMenuNodeOutputs) + entries = this.onMenuNodeOutputs( entries ); + + if(!entries.length) + return; + + var menu = new LiteGraph.ContextMenu(entries, {event: e, callback: inner_clicked, parentMenu: prev_menu, node: node }, ref_window); + + function inner_clicked( v, e, prev ) + { + if(!node) + return; + + if(v.callback) + v.callback.call( that, node, v, e, prev ); + + if(!v.value) + return; + + var value = v.value[1]; + + if(value && (value.constructor === Object || value.constructor === Array)) //submenu why? + { + var entries = []; + for(var i in value) + entries.push({ content: i, value: value[i]}); + new LiteGraph.ContextMenu( entries, { event: e, callback: inner_clicked, parentMenu: prev_menu, node: node }); + return false; + } + else + { + node.addOutput( v.value[0], v.value[1], v.value[2]); + node.setDirtyCanvas(true,true); + } + + } + + return false; +} + +LGraphCanvas.onShowMenuNodeProperties = function( value, options, e, prev_menu, node ) +{ + if(!node || !node.properties) + return; + + var that = this; + var canvas = LGraphCanvas.active_canvas; + var ref_window = canvas.getCanvasWindow(); + + var entries = []; + for (var i in node.properties) + { + var value = node.properties[i] !== undefined ? node.properties[i] : " "; + //value could contain invalid html characters, clean that + value = LGraphCanvas.decodeHTML(value); + entries.push({content: "" + i + "" + "" + value + "", value: i}); + } + if(!entries.length) + return; + + var menu = new LiteGraph.ContextMenu(entries, {event: e, callback: inner_clicked, parentMenu: prev_menu, allow_html: true, node: node },ref_window); + + function inner_clicked( v, options, e, prev ) + { + if(!node) + return; + var rect = this.getBoundingClientRect(); + canvas.showEditPropertyValue( node, v.value, { position: [rect.left, rect.top] }); + } + + return false; +} + +LGraphCanvas.decodeHTML = function( str ) +{ + var e = document.createElement("div"); + e.innerText = str; + return e.innerHTML; +} + +LGraphCanvas.onResizeNode = function( value, options, e, menu, node ) +{ + if(!node) + return; + node.size = node.computeSize(); + node.setDirtyCanvas(true,true); +} + + +LGraphCanvas.onShowTitleEditor = function( value, options, e, menu, node ) +{ + var input_html = ""; + + var dialog = document.createElement("div"); + dialog.className = "graphdialog"; + dialog.innerHTML = "Title"; + var input = dialog.querySelector("input"); + if(input) + { + input.value = node.title; + input.addEventListener("keydown", function(e){ + if(e.keyCode != 13) + return; + inner(); + e.preventDefault(); + e.stopPropagation(); + }); + } + + var graphcanvas = LGraphCanvas.active_canvas; + var canvas = graphcanvas.canvas; + + var rect = canvas.getBoundingClientRect(); + var offsetx = -20; + var offsety = -20; + if(rect) + { + offsetx -= rect.left; + offsety -= rect.top; + } + + if( event ) + { + dialog.style.left = (event.pageX + offsetx) + "px"; + dialog.style.top = (event.pageY + offsety)+ "px"; + } + else + { + dialog.style.left = (canvas.width * 0.5 + offsetx) + "px"; + dialog.style.top = (canvas.height * 0.5 + offsety) + "px"; + } + + var button = dialog.querySelector("button"); + button.addEventListener("click", inner ); + canvas.parentNode.appendChild( dialog ); + + function inner() + { + setValue( input.value ); + } + + function setValue(value) + { + node.title = value; + dialog.parentNode.removeChild( dialog ); + node.setDirtyCanvas(true,true); + } +} + +LGraphCanvas.prototype.showSearchBox = function(event) +{ + var that = this; + var input_html = ""; + + var dialog = document.createElement("div"); + dialog.className = "graphdialog"; + dialog.innerHTML = "Search
"; + dialog.close = function() + { + that.search_box = null; + dialog.parentNode.removeChild( dialog ); + } + + dialog.addEventListener("mouseleave",function(e){ + dialog.close(); + }); + + if(that.search_box) + that.search_box.close(); + that.search_box = dialog; + + var helper = dialog.querySelector(".helper"); + + var first = null; + var timeout = null; + var selected = null; + + var input = dialog.querySelector("input"); + if(input) + { + input.addEventListener("keydown", function(e){ + + if(e.keyCode == 38) //UP + changeSelection(false); + else if(e.keyCode == 40) //DOWN + changeSelection(true); + else if(e.keyCode == 27) //ESC + dialog.close(); + else if(e.keyCode == 13) + { + if(selected) + select( selected.innerHTML ) + else if(first) + select(first); + else + dialog.close(); + } + else + { + if(timeout) + clearInterval(timeout); + timeout = setTimeout( refreshHelper, 10 ); + return; + } + e.preventDefault(); + e.stopPropagation(); + }); + } + + var graphcanvas = LGraphCanvas.active_canvas; + var canvas = graphcanvas.canvas; + + var rect = canvas.getBoundingClientRect(); + var offsetx = -20; + var offsety = -20; + if(rect) + { + offsetx -= rect.left; + offsety -= rect.top; + } + + if( event ) + { + dialog.style.left = (event.pageX + offsetx) + "px"; + dialog.style.top = (event.pageY + offsety)+ "px"; + } + else + { + dialog.style.left = (canvas.width * 0.5 + offsetx) + "px"; + dialog.style.top = (canvas.height * 0.5 + offsety) + "px"; + } + + canvas.parentNode.appendChild( dialog ); + input.focus(); + + function select( name ) + { + if(name) + { + if( that.onSearchBoxSelection ) + that.onSearchBoxSelection( name, event, graphcanvas ); + else + { + var node = LiteGraph.createNode( name ); + if(node) + { + node.pos = graphcanvas.convertEventToCanvas( event ); + graphcanvas.graph.add( node ); + } + } + } + + dialog.close(); + } + + function changeSelection( forward ) + { + var prev = selected; + if(selected) + selected.classList.remove("selected"); + if(!selected) + selected = forward ? helper.childNodes[0] : helper.childNodes[ helper.childNodes.length ]; + else + { + selected = forward ? selected.nextSibling : selected.previousSibling; + if(!selected) + selected = prev; + } + if(!selected) + return; + selected.classList.add("selected"); + selected.scrollIntoView(); + } + + function refreshHelper() + { + timeout = null; + var str = input.value; + first = null; + helper.innerHTML = ""; + if(!str) + return; + + if( that.onSearchBox ) + that.onSearchBox( help, str, graphcanvas ); + else + for( var i in LiteGraph.registered_node_types ) + if(i.indexOf(str) != -1) + { + var help = document.createElement("div"); + if(!first) first = i; + help.innerText = i; + help.className = "help-item"; + help.addEventListener("click", function(e){ + select( this.innerText ); + }); + helper.appendChild(help); + } + } + + return dialog; +} + +LGraphCanvas.prototype.showEditPropertyValue = function( node, property, options ) +{ + if(!node || node.properties[ property ] === undefined ) + return; + + options = options || {}; + var that = this; + + var type = "string"; + + if(node.properties[ property ] !== null) + type = typeof(node.properties[ property ]); + + //for arrays + if(type == "object") + { + if( node.properties[ property ].length ) + type = "array"; + } + + var info = null; + if(node.getPropertyInfo) + info = node.getPropertyInfo(property); + if(node.properties_info) + { + for(var i = 0; i < node.properties_info.length; ++i) + { + if( node.properties_info[i].name == property ) + { + info = node.properties_info[i]; + break; + } + } + } + + if(info !== undefined && info !== null && info.type ) + type = info.type; + + var input_html = ""; + + if(type == "string" || type == "number" || type == "array") + input_html = ""; + else if(type == "enum" && info.values) + { + input_html = ""; + } + else if(type == "boolean") + { + input_html = ""; + } + else + { + console.warn("unknown type: " + type ); + return; + } + + var dialog = this.createDialog( "" + property + ""+input_html+"" , options ); + + if(type == "enum" && info.values) + { + var input = dialog.querySelector("select"); + input.addEventListener("change", function(e){ + setValue( e.target.value ); + //var index = e.target.value; + //setValue( e.options[e.selectedIndex].value ); + }); + } + else if(type == "boolean") + { + var input = dialog.querySelector("input"); + if(input) + { + input.addEventListener("click", function(e){ + setValue( !!input.checked ); + }); + } + } + else + { + var input = dialog.querySelector("input"); + if(input) + { + input.value = node.properties[ property ] !== undefined ? node.properties[ property ] : ""; + input.addEventListener("keydown", function(e){ + if(e.keyCode != 13) + return; + inner(); + e.preventDefault(); + e.stopPropagation(); + }); + } + } + + var button = dialog.querySelector("button"); + button.addEventListener("click", inner ); + + function inner() + { + setValue( input.value ); + } + + function setValue(value) + { + if(typeof( node.properties[ property ] ) == "number") + value = Number(value); + if(type == "array") + value = value.split(",").map(Number); + node.properties[ property ] = value; + if(node._graph) + node._graph._version++; + if(node.onPropertyChanged) + node.onPropertyChanged( property, value ); + dialog.close(); + node.setDirtyCanvas(true,true); + } +} + +LGraphCanvas.prototype.createDialog = function( html, options ) +{ + options = options || {}; + + var dialog = document.createElement("div"); + dialog.className = "graphdialog"; + dialog.innerHTML = html; + + var rect = this.canvas.getBoundingClientRect(); + var offsetx = -20; + var offsety = -20; + if(rect) + { + offsetx -= rect.left; + offsety -= rect.top; + } + + if( options.position ) + { + offsetx += options.position[0]; + offsety += options.position[1]; + } + else if( options.event ) + { + offsetx += options.event.pageX; + offsety += options.event.pageY; + } + else //centered + { + offsetx += this.canvas.width * 0.5; + offsety += this.canvas.height * 0.5; + } + + dialog.style.left = offsetx + "px"; + dialog.style.top = offsety + "px"; + + this.canvas.parentNode.appendChild( dialog ); + + dialog.close = function() + { + if(this.parentNode) + this.parentNode.removeChild( this ); + } + + return dialog; +} + +LGraphCanvas.onMenuNodeCollapse = function( value, options, e, menu, node ) +{ + node.collapse(); +} + +LGraphCanvas.onMenuNodePin = function( value, options, e, menu, node ) +{ + node.pin(); +} + +LGraphCanvas.onMenuNodeMode = function( value, options, e, menu, node ) +{ + new LiteGraph.ContextMenu(["Always","On Event","On Trigger","Never"], {event: e, callback: inner_clicked, parentMenu: menu, node: node }); + + function inner_clicked(v) + { + if(!node) + return; + switch(v) + { + case "On Event": node.mode = LiteGraph.ON_EVENT; break; + case "On Trigger": node.mode = LiteGraph.ON_TRIGGER; break; + case "Never": node.mode = LiteGraph.NEVER; break; + case "Always": + default: + node.mode = LiteGraph.ALWAYS; break; + } + } + + return false; +} + +LGraphCanvas.onMenuNodeColors = function( value, options, e, menu, node ) +{ + if(!node) + throw("no node for color"); + + var values = []; + values.push({ value:null, content:"No color" }); + + for(var i in LGraphCanvas.node_colors) + { + var color = LGraphCanvas.node_colors[i]; + var value = { value:i, content:""+i+"" }; + values.push(value); + } + new LiteGraph.ContextMenu( values, { event: e, callback: inner_clicked, parentMenu: menu, node: node }); + + function inner_clicked(v) + { + if(!node) + return; + + var color = v.value ? LGraphCanvas.node_colors[ v.value ] : null; + if(color) + { + if(node.constructor === LiteGraph.LGraphGroup) + node.color = color.groupcolor; + else + { + node.color = color.color; + node.bgcolor = color.bgcolor; + } + } + else + { + delete node.color; + delete node.bgcolor; + } + node.setDirtyCanvas(true,true); + } + + return false; +} + +LGraphCanvas.onMenuNodeShapes = function( value, options, e, menu, node ) +{ + if(!node) + throw("no node passed"); + + new LiteGraph.ContextMenu( LiteGraph.VALID_SHAPES, { event: e, callback: inner_clicked, parentMenu: menu, node: node }); + + function inner_clicked(v) + { + if(!node) + return; + node.shape = v; + node.setDirtyCanvas(true); + } + + return false; +} + +LGraphCanvas.onMenuNodeRemove = function( value, options, e, menu, node ) +{ + if(!node) + throw("no node passed"); + + if(node.removable === false) + return; + + node.graph.remove(node); + node.setDirtyCanvas(true,true); +} + +LGraphCanvas.onMenuNodeClone = function( value, options, e, menu, node ) +{ + if(node.clonable == false) return; + var newnode = node.clone(); + if(!newnode) + return; + newnode.pos = [node.pos[0]+5,node.pos[1]+5]; + node.graph.add(newnode); + node.setDirtyCanvas(true,true); +} + +LGraphCanvas.node_colors = { + "red": { color:"#322", bgcolor:"#533", groupcolor: "#A88" }, + "brown": { color:"#332922", bgcolor:"#593930", groupcolor: "#b06634" }, + "green": { color:"#232", bgcolor:"#353", groupcolor: "#8A8" }, + "blue": { color:"#223", bgcolor:"#335", groupcolor: "#88A" }, + "pale_blue": { color:"#2a363b", bgcolor:"#3f5159", groupcolor: "#3f789e" }, + "cyan": { color:"#233", bgcolor:"#355", groupcolor: "#8AA" }, + "purple": { color:"#323", bgcolor:"#535", groupcolor: "#a1309b" }, + "yellow": { color:"#432", bgcolor:"#653", groupcolor: "#b58b2a" }, + "black": { color:"#222", bgcolor:"#000", groupcolor: "#444" } +}; + +LGraphCanvas.prototype.getCanvasMenuOptions = function() +{ + var options = null; + if(this.getMenuOptions) + options = this.getMenuOptions(); + else + { + options = [ + { content:"Add Node", has_submenu: true, callback: LGraphCanvas.onMenuAdd }, + { content:"Add Group", callback: LGraphCanvas.onGroupAdd } + //{content:"Collapse All", callback: LGraphCanvas.onMenuCollapseAll } + ]; + + if(this._graph_stack && this._graph_stack.length > 0) + options = [{content:"Close subgraph", callback: this.closeSubgraph.bind(this) },null].concat(options); + } + + if(this.getExtraMenuOptions) + { + var extra = this.getExtraMenuOptions(this,options); + if(extra) + options = options.concat( extra ); + } + + return options; +} + +//called by processContextMenu to extract the menu list +LGraphCanvas.prototype.getNodeMenuOptions = function( node ) +{ + var options = null; + + if(node.getMenuOptions) + options = node.getMenuOptions(this); + else + options = [ + {content:"Inputs", has_submenu: true, disabled:true, callback: LGraphCanvas.showMenuNodeOptionalInputs }, + {content:"Outputs", has_submenu: true, disabled:true, callback: LGraphCanvas.showMenuNodeOptionalOutputs }, + null, + {content:"Properties", has_submenu: true, callback: LGraphCanvas.onShowMenuNodeProperties }, + null, + {content:"Title", callback: LGraphCanvas.onShowTitleEditor }, + {content:"Mode", has_submenu: true, callback: LGraphCanvas.onMenuNodeMode }, + {content:"Resize", callback: LGraphCanvas.onResizeNode }, + {content:"Collapse", callback: LGraphCanvas.onMenuNodeCollapse }, + {content:"Pin", callback: LGraphCanvas.onMenuNodePin }, + {content:"Colors", has_submenu: true, callback: LGraphCanvas.onMenuNodeColors }, + {content:"Shapes", has_submenu: true, callback: LGraphCanvas.onMenuNodeShapes }, + null + ]; + + if(node.getExtraMenuOptions) + { + var extra = node.getExtraMenuOptions(this); + if(extra) + { + extra.push(null); + options = extra.concat( options ); + } + } + + if( node.clonable !== false ) + options.push({content:"Clone", callback: LGraphCanvas.onMenuNodeClone }); + if( node.removable !== false ) + options.push(null,{content:"Remove", callback: LGraphCanvas.onMenuNodeRemove }); + + if(node.onGetInputs) + { + var inputs = node.onGetInputs(); + if(inputs && inputs.length) + options[0].disabled = false; + } + + if(node.onGetOutputs) + { + var outputs = node.onGetOutputs(); + if(outputs && outputs.length ) + options[1].disabled = false; + } + + if(node.graph && node.graph.onGetNodeMenuOptions ) + node.graph.onGetNodeMenuOptions( options, node ); + + return options; +} + +LGraphCanvas.prototype.getGroupMenuOptions = function( node ) +{ + var o = [ + {content:"Title", callback: LGraphCanvas.onShowTitleEditor }, + {content:"Color", has_submenu: true, callback: LGraphCanvas.onMenuNodeColors }, + null, + {content:"Remove", callback: LGraphCanvas.onMenuNodeRemove } + ]; + + return o; +} + +LGraphCanvas.prototype.processContextMenu = function( node, event ) +{ + var that = this; + var canvas = LGraphCanvas.active_canvas; + var ref_window = canvas.getCanvasWindow(); + + var menu_info = null; + var options = { event: event, callback: inner_option_clicked, node: node }; + + //check if mouse is in input + var slot = null; + if(node) + { + slot = node.getSlotInPosition( event.canvasX, event.canvasY ); + LGraphCanvas.active_node = node; + } + + if(slot) //on slot + { + menu_info = []; + menu_info.push( slot.locked ? "Cannot remove" : { content: "Remove Slot", slot: slot } ); + menu_info.push( { content: "Rename Slot", slot: slot } ); + options.title = (slot.input ? slot.input.type : slot.output.type) || "*"; + if(slot.input && slot.input.type == LiteGraph.ACTION) + options.title = "Action"; + if(slot.output && slot.output.type == LiteGraph.EVENT) + options.title = "Event"; + } + else + { + if( node ) //on node + menu_info = this.getNodeMenuOptions(node); + else + { + var group = this.graph.getGroupOnPos( event.canvasX, event.canvasY ); + if( group ) //on group + { + options.node = group; + menu_info = this.getGroupMenuOptions( group ); + } + else + menu_info = this.getCanvasMenuOptions(); + } + } + + //show menu + if(!menu_info) + return; + + var menu = new LiteGraph.ContextMenu( menu_info, options, ref_window ); + + function inner_option_clicked( v, options, e ) + { + if(!v) + return; + + if(v.content == "Remove Slot") + { + var info = v.slot; + if(info.input) + node.removeInput( info.slot ); + else if(info.output) + node.removeOutput( info.slot ); + return; + } + else if( v.content == "Rename Slot") + { + var info = v.slot; + var dialog = that.createDialog( "Name" , options ); + var input = dialog.querySelector("input"); + dialog.querySelector("button").addEventListener("click",function(e){ + if(input.value) + { + var slot_info = info.input ? node.getInputInfo( info.slot ) : node.getOutputInfo( info.slot ); + if( slot_info ) + slot_info.label = input.value; + that.setDirty(true); + } + dialog.close(); + }); + } + + //if(v.callback) + // return v.callback.call(that, node, options, e, menu, that, event ); + } +} + + + + + + +//API ************************************************* +//like rect but rounded corners +if(this.CanvasRenderingContext2D) +CanvasRenderingContext2D.prototype.roundRect = function (x, y, width, height, radius, radius_low) { + if ( radius === undefined ) { + radius = 5; + } + + if(radius_low === undefined) + radius_low = radius; + + this.moveTo(x + radius, y); + this.lineTo(x + width - radius, y); + this.quadraticCurveTo(x + width, y, x + width, y + radius); + + this.lineTo(x + width, y + height - radius_low); + this.quadraticCurveTo(x + width, y + height, x + width - radius_low, y + height); + this.lineTo(x + radius_low, y + height); + this.quadraticCurveTo(x, y + height, x, y + height - radius_low); + this.lineTo(x, y + radius); + this.quadraticCurveTo(x, y, x + radius, y); +} + +function compareObjects(a,b) +{ + for(var i in a) + if(a[i] != b[i]) + return false; + return true; +} +LiteGraph.compareObjects = compareObjects; + +function distance(a,b) +{ + return Math.sqrt( (b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]) ); +} +LiteGraph.distance = distance; + +function colorToString(c) +{ + return "rgba(" + Math.round(c[0] * 255).toFixed() + "," + Math.round(c[1] * 255).toFixed() + "," + Math.round(c[2] * 255).toFixed() + "," + (c.length == 4 ? c[3].toFixed(2) : "1.0") + ")"; +} +LiteGraph.colorToString = colorToString; + +function isInsideRectangle( x,y, left, top, width, height) +{ + if (left < x && (left + width) > x && + top < y && (top + height) > y) + return true; + return false; +} +LiteGraph.isInsideRectangle = isInsideRectangle; + +//[minx,miny,maxx,maxy] +function growBounding( bounding, x,y) +{ + if(x < bounding[0]) + bounding[0] = x; + else if(x > bounding[2]) + bounding[2] = x; + + if(y < bounding[1]) + bounding[1] = y; + else if(y > bounding[3]) + bounding[3] = y; +} +LiteGraph.growBounding = growBounding; + +//point inside boundin box +function isInsideBounding(p,bb) +{ + if (p[0] < bb[0][0] || + p[1] < bb[0][1] || + p[0] > bb[1][0] || + p[1] > bb[1][1]) + return false; + return true; +} +LiteGraph.isInsideBounding = isInsideBounding; + +//boundings overlap, format: [ startx, starty, width, height ] +function overlapBounding(a,b) +{ + var A_end_x = a[0] + a[2]; + var A_end_y = a[1] + a[3]; + var B_end_x = b[0] + b[2]; + var B_end_y = b[1] + b[3]; + + if ( a[0] > B_end_x || + a[1] > B_end_y || + A_end_x < b[0] || + A_end_y < b[1]) + return false; + return true; +} +LiteGraph.overlapBounding = overlapBounding; + +//Convert a hex value to its decimal value - the inputted hex must be in the +// format of a hex triplet - the kind we use for HTML colours. The function +// will return an array with three values. +function hex2num(hex) { + if(hex.charAt(0) == "#") hex = hex.slice(1); //Remove the '#' char - if there is one. + hex = hex.toUpperCase(); + var hex_alphabets = "0123456789ABCDEF"; + var value = new Array(3); + var k = 0; + var int1,int2; + for(var i=0;i<6;i+=2) { + int1 = hex_alphabets.indexOf(hex.charAt(i)); + int2 = hex_alphabets.indexOf(hex.charAt(i+1)); + value[k] = (int1 * 16) + int2; + k++; + } + return(value); +} + +LiteGraph.hex2num = hex2num; + +//Give a array with three values as the argument and the function will return +// the corresponding hex triplet. +function num2hex(triplet) { + var hex_alphabets = "0123456789ABCDEF"; + var hex = "#"; + var int1,int2; + for(var i=0;i<3;i++) { + int1 = triplet[i] / 16; + int2 = triplet[i] % 16; + + hex += hex_alphabets.charAt(int1) + hex_alphabets.charAt(int2); + } + return(hex); +} + +LiteGraph.num2hex = num2hex; + +/* LiteGraph GUI elements used for canvas editing *************************************/ + +/** +* ContextMenu from LiteGUI +* +* @class ContextMenu +* @constructor +* @param {Array} values (allows object { title: "Nice text", callback: function ... }) +* @param {Object} options [optional] Some options:\ +* - title: title to show on top of the menu +* - callback: function to call when an option is clicked, it receives the item information +* - ignore_item_callbacks: ignores the callback inside the item, it just calls the options.callback +* - event: you can pass a MouseEvent, this way the ContextMenu appears in that position +*/ +function ContextMenu( values, options ) +{ + options = options || {}; + this.options = options; + var that = this; + + //to link a menu with its parent + if(options.parentMenu) + { + if( options.parentMenu.constructor !== this.constructor ) + { + console.error("parentMenu must be of class ContextMenu, ignoring it"); + options.parentMenu = null; + } + else + { + this.parentMenu = options.parentMenu; + this.parentMenu.lock = true; + this.parentMenu.current_submenu = this; + } + } + + if(options.event && options.event.constructor !== MouseEvent && options.event.constructor !== CustomEvent) + { + console.error("Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it."); + options.event = null; + } + + var root = document.createElement("div"); + root.className = "litegraph litecontextmenu litemenubar-panel"; + root.style.minWidth = 100; + root.style.minHeight = 100; + root.style.pointerEvents = "none"; + setTimeout( function() { root.style.pointerEvents = "auto"; },100); //delay so the mouse up event is not caugh by this element + + //this prevents the default context browser menu to open in case this menu was created when pressing right button + root.addEventListener("mouseup", function(e){ + e.preventDefault(); return true; + }, true); + root.addEventListener("contextmenu", function(e) { + if(e.button != 2) //right button + return false; + e.preventDefault(); + return false; + },true); + + root.addEventListener("mousedown", function(e){ + if(e.button == 2) + { + that.close(); + e.preventDefault(); return true; + } + }, true); + + function on_mouse_wheel(e) + { + var pos = parseInt( root.style.top ); + root.style.top = (pos + e.deltaY * 0.1).toFixed() + "px"; + e.preventDefault(); + return true; + } + + root.addEventListener("wheel", on_mouse_wheel, true); + root.addEventListener("mousewheel", on_mouse_wheel, true); + + + this.root = root; + + //title + if(options.title) + { + var element = document.createElement("div"); + element.className = "litemenu-title"; + element.innerHTML = options.title; + root.appendChild(element); + } + + //entries + var num = 0; + for(var i in values) + { + var name = values.constructor == Array ? values[i] : i; + if( name != null && name.constructor !== String ) + name = name.content === undefined ? String(name) : name.content; + var value = values[i]; + this.addItem( name, value, options ); + num++; + } + + //close on leave + root.addEventListener("mouseleave", function(e) { + if(that.lock) + return; + that.close(e); + }); + + //insert before checking position + var root_document = document; + if(options.event) + root_document = options.event.target.ownerDocument; + + if(!root_document) + root_document = document; + root_document.body.appendChild(root); + + //compute best position + var left = options.left || 0; + var top = options.top || 0; + if(options.event) + { + left = (options.event.pageX - 10); + top = (options.event.pageY - 10); + if(options.title) + top -= 20; + + if(options.parentMenu) + { + var rect = options.parentMenu.root.getBoundingClientRect(); + left = rect.left + rect.width; + } + + var body_rect = document.body.getBoundingClientRect(); + var root_rect = root.getBoundingClientRect(); + + if(left > (body_rect.width - root_rect.width - 10)) + left = (body_rect.width - root_rect.width - 10); + if(top > (body_rect.height - root_rect.height - 10)) + top = (body_rect.height - root_rect.height - 10); + } + + root.style.left = left + "px"; + root.style.top = top + "px"; +} + +ContextMenu.prototype.addItem = function( name, value, options ) +{ + var that = this; + options = options || {}; + + var element = document.createElement("div"); + element.className = "litemenu-entry submenu"; + + var disabled = false; + + if(value === null) + { + element.classList.add("separator"); + //element.innerHTML = "
" + //continue; + } + else + { + element.innerHTML = value && value.title ? value.title : name; + element.value = value; + + if(value) + { + if(value.disabled) + { + disabled = true; + element.classList.add("disabled"); + } + if(value.submenu || value.has_submenu) + element.classList.add("has_submenu"); + } + + if(typeof(value) == "function") + { + element.dataset["value"] = name; + element.onclick_callback = value; + } + else + element.dataset["value"] = value; + + if(value.className) + element.className += " " + value.className; + } + + this.root.appendChild(element); + if(!disabled) + element.addEventListener("click", inner_onclick); + if(options.autoopen) + element.addEventListener("mouseenter", inner_over); + + function inner_over(e) + { + var value = this.value; + if(!value || !value.has_submenu) + return; + inner_onclick.call(this,e); + } + + //menu option clicked + function inner_onclick(e) { + var value = this.value; + var close_parent = true; + + if(that.current_submenu) + that.current_submenu.close(e); + + //global callback + if(options.callback) + { + var r = options.callback.call( this, value, options, e, that, options.node ); + if(r === true) + close_parent = false; + } + + //special cases + if(value) + { + if (value.callback && !options.ignore_item_callbacks && value.disabled !== true ) //item callback + { + var r = value.callback.call( this, value, options, e, that, options.node ); + if(r === true) + close_parent = false; + } + if(value.submenu) + { + if(!value.submenu.options) + throw("ContextMenu submenu needs options"); + var submenu = new that.constructor( value.submenu.options, { + callback: value.submenu.callback, + event: e, + parentMenu: that, + ignore_item_callbacks: value.submenu.ignore_item_callbacks, + title: value.submenu.title, + autoopen: options.autoopen + }); + close_parent = false; + } + } + + if(close_parent && !that.lock) + that.close(); + } + + return element; +} + +ContextMenu.prototype.close = function(e, ignore_parent_menu) +{ + if(this.root.parentNode) + this.root.parentNode.removeChild( this.root ); + if(this.parentMenu && !ignore_parent_menu) + { + this.parentMenu.lock = false; + this.parentMenu.current_submenu = null; + if( e === undefined ) + this.parentMenu.close(); + else if( e && !ContextMenu.isCursorOverElement( e, this.parentMenu.root) ) + { + ContextMenu.trigger( this.parentMenu.root, "mouseleave", e ); + } + } + if(this.current_submenu) + this.current_submenu.close(e, true); +} + +//this code is used to trigger events easily (used in the context menu mouseleave +ContextMenu.trigger = function(element, event_name, params, origin) +{ + var evt = document.createEvent( 'CustomEvent' ); + evt.initCustomEvent( event_name, true,true, params ); //canBubble, cancelable, detail + evt.srcElement = origin; + if( element.dispatchEvent ) + element.dispatchEvent( evt ); + else if( element.__events ) + element.__events.dispatchEvent( evt ); + //else nothing seems binded here so nothing to do + return evt; +} + +//returns the top most menu +ContextMenu.prototype.getTopMenu = function() +{ + if( this.options.parentMenu ) + return this.options.parentMenu.getTopMenu(); + return this; +} + +ContextMenu.prototype.getFirstEvent = function() +{ + if( this.options.parentMenu ) + return this.options.parentMenu.getFirstEvent(); + return this.options.event; +} + + + +ContextMenu.isCursorOverElement = function( event, element ) +{ + var left = event.pageX; + var top = event.pageY; + var rect = element.getBoundingClientRect(); + if(!rect) + return false; + if(top > rect.top && top < (rect.top + rect.height) && + left > rect.left && left < (rect.left + rect.width) ) + return true; + return false; +} + + + +LiteGraph.ContextMenu = ContextMenu; + +LiteGraph.closeAllContextMenus = function( ref_window ) +{ + ref_window = ref_window || window; + + var elements = ref_window.document.querySelectorAll(".litecontextmenu"); + if(!elements.length) + return; + + var result = []; + for(var i = 0; i < elements.length; i++) + result.push(elements[i]); + + for(var i in result) + { + if(result[i].close) + result[i].close(); + else if(result[i].parentNode) + result[i].parentNode.removeChild( result[i] ); + } +} + +LiteGraph.extendClass = function ( target, origin ) +{ + for(var i in origin) //copy class properties + { + if(target.hasOwnProperty(i)) + continue; + target[i] = origin[i]; + } + + if(origin.prototype) //copy prototype properties + for(var i in origin.prototype) //only enumerables + { + if(!origin.prototype.hasOwnProperty(i)) + continue; + + if(target.prototype.hasOwnProperty(i)) //avoid overwritting existing ones + continue; + + //copy getters + if(origin.prototype.__lookupGetter__(i)) + target.prototype.__defineGetter__(i, origin.prototype.__lookupGetter__(i)); + else + target.prototype[i] = origin.prototype[i]; + + //and setters + if(origin.prototype.__lookupSetter__(i)) + target.prototype.__defineSetter__(i, origin.prototype.__lookupSetter__(i)); + } +} + +//used to create nodes from wrapping functions +LiteGraph.getParameterNames = function(func) { + return (func + '') + .replace(/[/][/].*$/mg,'') // strip single-line comments + .replace(/\s+/g, '') // strip white space + .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments /**/ + .split('){', 1)[0].replace(/^[^(]*[(]/, '') // extract the parameters + .replace(/=[^,]+/g, '') // strip any ES6 defaults + .split(',').filter(Boolean); // split & filter [""] +} + +Math.clamp = function(v,a,b) { return (a > v ? a : (b < v ? b : v)); } + +if( typeof(window) != "undefined" && !window["requestAnimationFrame"] ) +{ + window.requestAnimationFrame = window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + (function( callback ){ + window.setTimeout(callback, 1000 / 60); + }); +} + +})(this); + +if(typeof(exports) != "undefined") + exports.LiteGraph = this.LiteGraph; //basic nodes (function(global){ @@ -9761,7 +9913,7 @@ function MathOperation() this.addOutput("=","number"); this.addProperty( "A", 1 ); this.addProperty( "B", 1 ); - this.addProperty( "OP", "+", "string", { values: MathOperation.values } ); + this.addProperty( "OP", "+", "enum", { values: MathOperation.values } ); } MathOperation.values = ["+","-","*","/","%","^"]; @@ -9770,6 +9922,10 @@ MathOperation.title = "Operation"; MathOperation.desc = "Easy math operators"; MathOperation["@OP"] = { type:"enum", title: "operation", values: MathOperation.values }; +MathOperation.prototype.getTitle = function() +{ + return "A " + this.properties.OP + " B"; +} MathOperation.prototype.setValue = function(v) { @@ -9814,9 +9970,9 @@ MathOperation.prototype.onDrawBackground = function(ctx) return; ctx.font = "40px Arial"; - ctx.fillStyle = "black"; + ctx.fillStyle = "#CCC"; ctx.textAlign = "center"; - ctx.fillText(this.properties.OP, this.size[0] * 0.5, this.size[1] * 0.5 + LiteGraph.NODE_TITLE_HEIGHT ); + ctx.fillText(this.properties.OP, this.size[0] * 0.5, this.size[1] * 0.35 + LiteGraph.NODE_TITLE_HEIGHT ); ctx.textAlign = "left"; } diff --git a/build/litegraph.min.js b/build/litegraph.min.js index d427c281a..1f975a572 100755 --- a/build/litegraph.min.js +++ b/build/litegraph.min.js @@ -1,232 +1,235 @@ -(function(t){function h(a){l.debug&&console.log("Graph created");this.list_of_graphcanvas=null;this.clear();a&&this.configure(a)}function e(a){this._ctor(a)}function m(a){this._ctor(a)}function d(a,c,g){g=g||{};this.background_image="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII="; +(function(s){function h(a){l.debug&&console.log("Graph created");this.list_of_graphcanvas=null;this.clear();a&&this.configure(a)}function e(a){this._ctor(a)}function n(a){this._ctor(a)}function d(a,b,g){g=g||{};this.background_image="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII="; a&&a.constructor===String&&(a=document.querySelector(a));this.max_zoom=10;this.min_zoom=0.1;this.zoom_modify_alpha=!0;this.title_text_font="bold "+l.NODE_TEXT_SIZE+"px Arial";this.inner_text_font="normal "+l.NODE_SUBTEXT_SIZE+"px Arial";this.node_title_color=l.NODE_TITLE_COLOR;this.default_link_color="#AAC";this.default_connection_color={input_off:"#AAB",input_on:"#7F7",output_off:"#AAB",output_on:"#7F7"};this.highquality_render=!0;this.use_gradients=!1;this.editor_alpha=1;this.pause_rendering=!1; this.render_only_selected=this.clear_background=this.render_shadows=!0;this.live_mode=!1;this.allow_searchbox=this.allow_interaction=this.allow_dragnodes=this.allow_dragcanvas=this.show_info=!0;this.drag_mode=!1;this.filter=this.dragging_rectangle=null;this.always_render_background=!1;this.render_canvas_border=!0;this.render_connections_shadows=!1;this.render_connection_arrows=this.render_curved_connections=this.render_connections_border=!0;this.canvas_mouse=[0,0];this.onSearchBoxSelection=this.onSearchBox= -null;this.connections_width=3;this.round_radius=8;this.node_widget=this.current_node=null;this.last_mouse_position=[0,0];c&&c.attachCanvas(this);this.setCanvas(a);this.clear();g.skip_render||this.startRendering();this.autoresize=g.autoresize}function r(a,c){return Math.sqrt((c[0]-a[0])*(c[0]-a[0])+(c[1]-a[1])*(c[1]-a[1]))}function q(a,c,g,b,f,k){return ga&&bc?!0:!1}function s(a,c){var g=a[0]+a[2],b=a[1]+a[3],f=c[1]+c[3];return a[0]>c[0]+c[2]||a[1]>f||gd.width-l.width-10&&(k=d.width-l.width-10);p>d.height-l.height-10&&(p=d.height-l.height-10)}f.style.left=k+"px";f.style.top=p+"px"}var l=t.LiteGraph={NODE_TITLE_HEIGHT:20,NODE_SLOT_HEIGHT:15,NODE_WIDGET_HEIGHT:20,NODE_WIDTH:140,NODE_MIN_WIDTH:50,NODE_COLLAPSED_RADIUS:10, -NODE_COLLAPSED_WIDTH:80,CANVAS_GRID_SIZE:10,NODE_TITLE_COLOR:"#999",NODE_TEXT_SIZE:14,NODE_TEXT_COLOR:"#AAA",NODE_SUBTEXT_SIZE:12,NODE_DEFAULT_COLOR:"#333",NODE_DEFAULT_BGCOLOR:"#444",NODE_DEFAULT_BOXCOLOR:"#888",NODE_DEFAULT_SHAPE:"box",MAX_NUMBER_OF_NODES:1E3,DEFAULT_POSITION:[100,100],node_images_path:"",VALID_SHAPES:["default","box","round","card"],BOX_SHAPE:1,ROUND_SHAPE:2,CIRCLE_SHAPE:3,CARD_SHAPE:4,INPUT:1,OUTPUT:2,EVENT:-1,ACTION:-1,ALWAYS:0,ON_EVENT:1,NEVER:2,ON_TRIGGER:3,NORMAL_TITLE:0, -NO_TITLE:1,TRANSPARENT_TITLE:2,AUTOHIDE_TITLE:3,proxy:null,debug:!1,throw_errors:!0,allow_scripts:!0,registered_node_types:{},node_types_by_file_extension:{},Nodes:{},registerNodeType:function(a,c){if(!c.prototype)throw"Cannot register a simple object, it must be a class with a prototype";c.type=a;l.debug&&console.log("Node registered: "+a);a.split("/");var g=c.name,b=a.lastIndexOf("/");c.category=a.substr(0,b);c.title||(c.title=g);if(c.prototype)for(var f in e.prototype)c.prototype[f]||(c.prototype[f]= -e.prototype[f]);Object.defineProperty(c.prototype,"shape",{set:function(a){switch(a){case "default":delete this._shape;break;case "box":this._shape=l.BOX_SHAPE;break;case "round":this._shape=l.ROUND_SHAPE;break;case "circle":this._shape=l.CIRCLE_SHAPE;break;case "card":this._shape=l.CARD_SHAPE;break;default:this._shape=a}},get:function(a){return this._shape},enumerable:!0});this.registered_node_types[a]=c;c.constructor.name&&(this.Nodes[g]=c);c.prototype.onPropertyChange&&console.warn("LiteGraph node class "+ -a+" has onPropertyChange method, it must be called onPropertyChanged with d at the end");if(c.supported_extensions)for(f in c.supported_extensions)this.node_types_by_file_extension[c.supported_extensions[f].toLowerCase()]=c},wrapFunctionAsNode:function(a,c,g,b){for(var f=Array(c.length),k="",p=l.getParameterNames(c),d=0;dp&&(p=f.size[0]),d+=f.size[1]+a;c+=p+a}this.setDirtyCanvas(!0,!0)};h.prototype.getTime=function(){return this.globaltime}; -h.prototype.getFixedTime=function(){return this.fixedtime};h.prototype.getElapsedTime=function(){return this.elapsed_time};h.prototype.sendEventToAllNodes=function(a,c,g){g=g||l.ALWAYS;var b=this._nodes_in_order?this._nodes_in_order:this._nodes;if(b)for(var f=0,k=b.length;f=l.MAX_NUMBER_OF_NODES)throw"LiteGraph: max number of nodes in a graph reached";null==a.id||-1==a.id?a.id=++this.last_node_id:this.last_node_id< -a.id&&(this.last_node_id=a.id);a.graph=this;this._version++;this._nodes.push(a);this._nodes_by_id[a.id]=a;if(a.onAdded)a.onAdded(this);this.config.align_to_grid&&a.alignToGrid();c||this.updateExecutionOrder();if(this.onNodeAdded)this.onNodeAdded(a);this.setDirtyCanvas(!0);this.change();return a}};h.prototype.remove=function(a){if(a.constructor===l.LGraphGroup){var c=this._groups.indexOf(a);-1!=c&&this._groups.splice(c,1);a.graph=null;this._version++;this.setDirtyCanvas(!0,!0);this.change()}else if(null!= -this._nodes_by_id[a.id]&&!a.ignore_remove){if(a.inputs)for(c=0;ca.length||(this._pos[0]=a[0],this._pos[1]=a[1])},get:function(){return this._pos},enumerable:!0});this.id=-1;this.type=null;this.inputs=[];this.outputs=[];this.connections=[];this.properties={};this.properties_info=[];this.data=null;this.flags={}};e.prototype.configure=function(a){this.graph&&this.graph._version++;for(var c in a)if("console"!=c)if("properties"==c)for(var g in a.properties){if(this.properties[g]=a.properties[g], -this.onPropertyChanged)this.onPropertyChanged(g,a.properties[g])}else null!=a[c]&&("object"==typeof a[c]?this[c]&&this[c].configure?this[c].configure(a[c]):this[c]=l.cloneObject(a[c],this[c]):this[c]=a[c]);a.title||(this.title=this.constructor.title);if(this.onConnectionsChange){if(this.inputs)for(var b=0;b=this.outputs.length)){var g=this.outputs[a];if(g&&(g._data=c,this.outputs[a].links))for(g=0;g=this.inputs.length||null==this.inputs[a].link)){var g=this.graph.links[this.inputs[a].link];if(!g)return null;if(!c)return g.data; -var b=this.graph.getNodeById(g.origin_id);if(!b)return g.data;if(b.updateOutputData)b.updateOutputData(g.origin_slot);else if(b.onExecute)b.onExecute();return g.data}};e.prototype.getInputDataByName=function(a,c){var g=this.findInputSlot(a);return-1==g?null:this.getInputData(g,c)};e.prototype.isInputConnected=function(a){return this.inputs?a=this.inputs.length)return null;a=this.inputs[a];return a&&a.link?(a=this.graph.links[a.link])?this.graph.getNodeById(a.origin_id):null:null};e.prototype.getInputOrProperty=function(a){if(!this.inputs||!this.inputs.length)return this.properties?this.properties[a]:null;for(var c=0,g=this.inputs.length;c=this.outputs.length?null:this.outputs[a]._data};e.prototype.getOutputInfo=function(a){return this.outputs?a=this.outputs.length)return null;a=this.outputs[a];if(!a.links||0==a.links.length)return null;for(var c=[],g=0;ga&&this.pos[1]-b-gc)return!0;return!1};e.prototype.getSlotInPosition=function(a,c){if(this.inputs)for(var g=0,b=this.inputs.length;g=this.outputs.length)return l.debug&&console.log("Connect: Error, slot number not found"),!1;c&&c.constructor===Number&&(c=this.graph.getNodeById(c));if(!c)throw"Node not found";if(c==this)return!1;if(g.constructor===String){if(g=c.findInputSlot(g),-1==g)return l.debug&&console.log("Connect: Error, no slot of name "+g),!1}else{if(g===l.EVENT)return!1;if(!c.inputs||g>=c.inputs.length)return l.debug&&console.log("Connect: Error, slot number not found"),!1}null!=c.inputs[g].link&& -c.disconnectInput(g);this.setDirtyCanvas(!1,!0);this.graph.connectionChange(this);var b=this.outputs[a];if(c.onConnectInput&&!1===c.onConnectInput(g,b.type,b))return!1;var f=c.inputs[g];if(l.isValidConnection(b.type,f.type)){var k={id:this.graph.last_link_id++,type:f.type,origin_id:this.id,origin_slot:a,target_id:c.id,target_slot:g};this.graph.links[k.id]=k;null==b.links&&(b.links=[]);b.links.push(k.id);c.inputs[g].link=k.id;this.graph&&this.graph._version++;if(this.onConnectionsChange)this.onConnectionsChange(l.OUTPUT, -a,!0,k,b);if(c.onConnectionsChange)c.onConnectionsChange(l.INPUT,g,!0,k,f)}this.setDirtyCanvas(!1,!0);this.graph.connectionChange(this);return!0};e.prototype.disconnectOutput=function(a,c){if(a.constructor===String){if(a=this.findOutputSlot(a),-1==a)return l.debug&&console.log("Connect: Error, no slot of name "+a),!1}else if(!this.outputs||a>=this.outputs.length)return l.debug&&console.log("Connect: Error, slot number not found"),!1;var g=this.outputs[a];if(!g.links||0==g.links.length)return!1;if(c){c.constructor=== -Number&&(c=this.graph.getNodeById(c));if(!c)throw"Target Node not found";for(var b=0,f=g.links.length;b=this.inputs.length)return l.debug&&console.log("Connect: Error, slot number not found"),!1;var c=this.inputs[a];if(!c)return!1;var g=this.inputs[a].link;this.inputs[a].link=null;var b=this.graph.links[g];if(b){var f=this.graph.getNodeById(b.origin_id);if(!f)return!1;var k=f.outputs[b.origin_slot];if(!k||!k.links||0==k.links.length)return!1;for(var d=0,n=k.links.length;dc&&this.inputs[c].pos?[this.pos[0]+this.inputs[c].pos[0],this.pos[1]+this.inputs[c].pos[1]]:!a&&this.outputs.length>c&&this.outputs[c].pos?[this.pos[0]+this.outputs[c].pos[0],this.pos[1]+this.outputs[c].pos[1]]:a?[this.pos[0],this.pos[1]+10+c*l.NODE_SLOT_HEIGHT+(this.constructor.slot_start_y||0)]:[this.pos[0]+this.size[0]+1,this.pos[1]+10+c*l.NODE_SLOT_HEIGHT+(this.constructor.slot_start_y||0)]};e.prototype.alignToGrid=function(){this.pos[0]=l.CANVAS_GRID_SIZE*Math.round(this.pos[0]/ -l.CANVAS_GRID_SIZE);this.pos[1]=l.CANVAS_GRID_SIZE*Math.round(this.pos[1]/l.CANVAS_GRID_SIZE)};e.prototype.trace=function(a){this.console||(this.console=[]);this.console.push(a);this.console.length>e.MAX_CONSOLE&&this.console.shift();this.graph.onNodeTrace(this,a)};e.prototype.setDirtyCanvas=function(a,c){this.graph&&this.graph.sendActionToCanvas("setDirty",[a,c])};e.prototype.loadImage=function(a){var c=new Image;c.src=l.node_images_path+a;c.ready=!1;var g=this;c.onload=function(){this.ready=!0; -g.setDirtyCanvas(!0)};return c};e.prototype.captureInput=function(a){if(this.graph&&this.graph.list_of_graphcanvas)for(var c=this.graph.list_of_graphcanvas,g=0;ga.length||(this._pos[0]=a[0],this._pos[1]=a[1])},get:function(){return this._pos},enumerable:!0});Object.defineProperty(this,"size",{set:function(a){!a||2>a.length||(this._size[0]=Math.max(140,a[0]),this._size[1]=Math.max(80,a[1]))},get:function(){return this._size},enumerable:!0})};m.prototype.configure=function(a){this.title=a.title;this._bounding.set(a.bounding);this.color=a.color};m.prototype.serialize=function(){var a=this._bounding;return{title:this.title,bounding:[a[0],a[1],a[2],a[3]],color:this.color}}; -m.prototype.move=function(a,c,g){this._pos[0]+=a;this._pos[1]+=c;if(!g)for(g=0;g element, you passed a "+a.localName;throw"This browser doesnt support Canvas";}null==(this.ctx=a.getContext("2d"))&&(a.webgl_enabled||console.warn("This canvas seems to be WebGL, enabling WebGL renderer"),this.enableWebGL());this._mousemove_callback=this.processMouseMove.bind(this); -this._mouseup_callback=this.processMouseUp.bind(this);c||this.bindEvents()}};d.prototype._doNothing=function(a){a.preventDefault();return!1};d.prototype._doReturnTrue=function(a){a.preventDefault();return!0};d.prototype.bindEvents=function(){if(this._events_binded)console.warn("LGraphCanvas: events already binded");else{var a=this.canvas,c=this.getCanvasWindow().document;this._mousedown_callback=this.processMouseDown.bind(this);this._mousewheel_callback=this.processMouseWheel.bind(this);a.addEventListener("mousedown", -this._mousedown_callback,!0);a.addEventListener("mousemove",this._mousemove_callback);a.addEventListener("mousewheel",this._mousewheel_callback,!1);a.addEventListener("contextmenu",this._doNothing);a.addEventListener("DOMMouseScroll",this._mousewheel_callback,!1);a.addEventListener("touchstart",this.touchHandler,!0);a.addEventListener("touchmove",this.touchHandler,!0);a.addEventListener("touchend",this.touchHandler,!0);a.addEventListener("touchcancel",this.touchHandler,!0);this._key_callback=this.processKey.bind(this); -a.addEventListener("keydown",this._key_callback,!0);c.addEventListener("keyup",this._key_callback,!0);this._ondrop_callback=this.processDrop.bind(this);a.addEventListener("dragover",this._doNothing,!1);a.addEventListener("dragend",this._doNothing,!1);a.addEventListener("drop",this._ondrop_callback,!1);a.addEventListener("dragenter",this._doReturnTrue,!1);this._events_binded=!0}};d.prototype.unbindEvents=function(){if(this._events_binded){var a=this.getCanvasWindow().document;this.canvas.removeEventListener("mousedown", -this._mousedown_callback);this.canvas.removeEventListener("mousewheel",this._mousewheel_callback);this.canvas.removeEventListener("DOMMouseScroll",this._mousewheel_callback);this.canvas.removeEventListener("keydown",this._key_callback);a.removeEventListener("keyup",this._key_callback);this.canvas.removeEventListener("contextmenu",this._doNothing);this.canvas.removeEventListener("drop",this._ondrop_callback);this.canvas.removeEventListener("dragenter",this._doReturnTrue);this.canvas.removeEventListener("touchstart", -this.touchHandler);this.canvas.removeEventListener("touchmove",this.touchHandler);this.canvas.removeEventListener("touchend",this.touchHandler);this.canvas.removeEventListener("touchcancel",this.touchHandler);this._ondrop_callback=this._key_callback=this._mousewheel_callback=this._mousedown_callback=null;this._events_binded=!1}else console.warn("LGraphCanvas: no events binded")};d.getFileExtension=function(a){var c=a.indexOf("?");-1!=c&&(a=a.substr(0,c));c=a.lastIndexOf(".");return-1==c?"":a.substr(c+ -1).toLowerCase()};d.prototype.enableWebGL=function(){if(void 0===typeof GL)throw"litegl.js must be included to use a WebGL canvas";if(void 0===typeof enableWebGLCanvas)throw"webglCanvas.js must be included to use this feature";this.gl=this.ctx=enableWebGLCanvas(this.canvas);this.ctx.webgl=!0;this.bgcanvas=this.canvas;this.bgctx=this.gl;this.canvas.webgl_enabled=!0};d.prototype.setDirty=function(a,c){a&&(this.dirty_canvas=!0);c&&(this.dirty_bgcanvas=!0)};d.prototype.getCanvasWindow=function(){if(!this.canvas)return window; -var a=this.canvas.ownerDocument;return a.defaultView||a.parentWindow};d.prototype.startRendering=function(){function a(){this.pause_rendering||this.draw();var c=this.getCanvasWindow();this.is_rendering&&c.requestAnimationFrame(a.bind(this))}this.is_rendering||(this.is_rendering=!0,a.call(this))};d.prototype.stopRendering=function(){this.is_rendering=!1};d.prototype.processMouseDown=function(a){if(this.graph){this.adjustMouseEvent(a);var c=this.getCanvasWindow();d.active_canvas=this;this.canvas.removeEventListener("mousemove", -this._mousemove_callback);c.document.addEventListener("mousemove",this._mousemove_callback,!0);c.document.addEventListener("mouseup",this._mouseup_callback,!0);var g=this.graph.getNodeOnPos(a.canvasX,a.canvasY,this.visible_nodes),b=!1,f=l.getTime();this.canvas_mouse[0]=a.canvasX;this.canvas_mouse[1]=a.canvasY;l.closeAllContextMenus(c);if(1==a.which){a.ctrlKey&&(this.dragging_rectangle=new Float32Array(4),this.dragging_rectangle[0]=a.canvasX,this.dragging_rectangle[1]=a.canvasY,this.dragging_rectangle[2]= -1,this.dragging_rectangle[3]=1,b=!0);var k=!1;if(g&&this.allow_interaction&&!b){this.live_mode||g.flags.pinned||this.bringToFront(g);if(!this.connecting_node&&!g.flags.collapsed&&!this.live_mode){if(g.outputs)for(var p=0,n=g.outputs.length;pf-this.last_mouseclick&&this.selected_nodes[g.id]){if(g.onDblClick)g.onDblClick(a);this.processNodeDblClicked(g);p=!0}g.onMouseDown&&g.onMouseDown(a,[a.canvasX-g.pos[0],a.canvasY-g.pos[1]])?p=!0:this.live_mode&&(p=k=!0);p||(this.allow_dragnodes&&(this.node_dragged=g),this.selected_nodes[g.id]||this.processNodeSelected(g,a));this.dirty_canvas=!0}}else this.selected_group=this.graph.getGroupOnPos(a.canvasX,a.canvasY),this.selected_group_resizing=!1,this.selected_group&& -(a.ctrlKey&&(this.dragging_rectangle=null),10>r([a.canvasX,a.canvasY],[this.selected_group.pos[0]+this.selected_group.size[0],this.selected_group.pos[1]+this.selected_group.size[1]])*this.scale?this.selected_group_resizing=!0:this.selected_group.recomputeInsideNodes()),k=!0;!b&&k&&this.allow_dragcanvas&&(this.dragging_canvas=!0)}else 2!=a.which&&3==a.which&&this.processContextMenu(g,a);this.last_mouse[0]=a.localX;this.last_mouse[1]=a.localY;this.last_mouseclick=l.getTime();this.graph.change();(!c.document.activeElement|| -"input"!=c.document.activeElement.nodeName.toLowerCase()&&"textarea"!=c.document.activeElement.nodeName.toLowerCase())&&a.preventDefault();a.stopPropagation();if(this.onMouseDown)this.onMouseDown(a);return!1}};d.prototype.processMouseMove=function(a){this.autoresize&&this.resize();if(this.graph){d.active_canvas=this;this.adjustMouseEvent(a);var c=[a.localX,a.localY],g=[c[0]-this.last_mouse[0],c[1]-this.last_mouse[1]];this.last_mouse=c;this.canvas_mouse[0]=a.canvasX;this.canvas_mouse[1]=a.canvasY; -this.node_widget&&(this.processNodeWidgets(this.node_widget[0],this.canvas_mouse,a,this.node_widget[1]),this.dirty_canvas=!0);if(this.dragging_rectangle)this.dragging_rectangle[2]=a.canvasX-this.dragging_rectangle[0],this.dragging_rectangle[3]=a.canvasY-this.dragging_rectangle[1],this.dirty_canvas=!0;else if(this.selected_group)this.selected_group_resizing?this.selected_group.size=[a.canvasX-this.selected_group.pos[0],a.canvasY-this.selected_group.pos[1]]:(this.selected_group.move(g[0]/this.scale, -g[1]/this.scale,a.ctrlKey),this.selected_group._nodes.length&&(this.dirty_canvas=!0)),this.dirty_bgcanvas=!0;else if(this.dragging_canvas)this.offset[0]+=g[0]/this.scale,this.offset[1]+=g[1]/this.scale,this.dirty_bgcanvas=this.dirty_canvas=!0;else if(this.allow_interaction){this.connecting_node&&(this.dirty_canvas=!0);for(var b=this.graph.getNodeOnPos(a.canvasX,a.canvasY,this.visible_nodes),c=0,f=this.graph._nodes.length;cthis.dragging_rectangle[2]&&(this.dragging_rectangle[0]+=this.dragging_rectangle[2]);0>this.dragging_rectangle[3]&&(this.dragging_rectangle[1]+=this.dragging_rectangle[3]);this.dragging_rectangle[2]= -Math.abs(this.dragging_rectangle[2]*this.scale);this.dragging_rectangle[3]=Math.abs(this.dragging_rectangle[3]*this.scale);for(var f=0;fa.click_time&&this.deselectAllNodes();this.dirty_canvas=!0;this.dragging_canvas=!1;if(this.node_over&& -this.node_over.onMouseUp)this.node_over.onMouseUp(a,[a.canvasX-this.node_over.pos[0],a.canvasY-this.node_over.pos[1]]);if(this.node_capturing_input&&this.node_capturing_input.onMouseUp)this.node_capturing_input.onMouseUp(a,[a.canvasX-this.node_capturing_input.pos[0],a.canvasY-this.node_capturing_input.pos[1]])}else 2==a.which?(this.dirty_canvas=!0,this.dragging_canvas=!1):3==a.which&&(this.dirty_canvas=!0,this.dragging_canvas=!1);this.graph.change();a.stopPropagation();a.preventDefault();return!1}}; -d.prototype.processMouseWheel=function(a){if(this.graph&&this.allow_dragcanvas){var c=null!=a.wheelDeltaY?a.wheelDeltaY:-60*a.detail;this.adjustMouseEvent(a);var g=this.scale;0c&&(g*=1/1.1);this.setZoom(g,[a.localX,a.localY]);this.graph.change();a.preventDefault();return!1}};d.prototype.isOverNodeBox=function(a,c,g){var b=l.NODE_TITLE_HEIGHT;return q(c,g,a.pos[0]+2,a.pos[1]+2-b,b-4,b-4)?!0:!1};d.prototype.isOverNodeInput=function(a,c,g,b){if(a.inputs)for(var f=0,k=a.inputs.length;fthis.max_zoom?this.scale=this.max_zoom:this.scaleb-this.graph._last_trigger_time)&& -this.drawBackCanvas();(this.dirty_canvas||a)&&this.drawFrontCanvas();this.fps=this.render_time?1/this.render_time:0;this.frame+=1}};d.prototype.drawFrontCanvas=function(){this.dirty_canvas=!1;this.ctx||(this.ctx=this.bgcanvas.getContext("2d"));var a=this.ctx;if(a){a.start2D&&a.start2D();var c=this.canvas;a.restore();a.setTransform(1,0,0,1,0,0);this.dirty_area&&(a.save(),a.beginPath(),a.rect(this.dirty_area[0],this.dirty_area[1],this.dirty_area[2],this.dirty_area[3]),a.clip());this.clear_background&& -a.clearRect(0,0,c.width,c.height);this.bgcanvas==this.canvas?this.drawBackCanvas():a.drawImage(this.bgcanvas,0,0);if(this.onRender)this.onRender(c,a);this.show_info&&this.renderInfo(a);if(this.graph){a.save();a.scale(this.scale,this.scale);a.translate(this.offset[0],this.offset[1]);for(var c=this.computeVisibleNodes(null,this.visible_nodes),b=0;bthis.scale?(c.beginPath(),c.rect(0,p,q,r),c.fill()):e==l.ROUND_SHAPE||e==l.CARD_SHAPE?(c.beginPath(),c.roundRect(0,p,q,r,this.round_radius,e==l.CARD_SHAPE?0:this.round_radius),c.fill()):e==l.CIRCLE_SHAPE&&(c.beginPath(),c.arc(0.5*b[0],0.5*b[1],0.5* -b[0],0,2*Math.PI),c.fill()));c.shadowColor="transparent";a.bgImage&&a.bgImage.width&&c.drawImage(a.bgImage,0.5*(b[0]-a.bgImage.width),0.5*(b[1]-a.bgImage.height));a.bgImageUrl&&!a.bgImage&&(a.bgImage=a.loadImage(a.bgImageUrl));a.onDrawBackground&&(a.gui_rects&&(a.gui_rects.length=0),a.onDrawBackground(c,this));if(m||h==l.TRANSPARENT_TITLE){if(h!=l.TRANSPARENT_TITLE){if(this.use_gradients){var s=d.gradients[n];s||(s=d.gradients[n]=c.createLinearGradient(0,0,400,0),s.addColorStop(0,n),s.addColorStop(1, -"#000"));c.fillStyle=s}else c.fillStyle=n;s=c.globalAlpha;c.beginPath();if(e==l.BOX_SHAPE||0.5>this.scale)c.rect(0,-f,b[0]+1,f),c.fill();else if(e==l.ROUND_SHAPE||e==l.CARD_SHAPE)c.roundRect(0,-f,b[0],f,this.round_radius,a.flags.collapsed?this.round_radius:0),c.fill()}c.fillStyle=a.boxcolor||l.NODE_DEFAULT_BOXCOLOR;c.beginPath();e==l.ROUND_SHAPE||e==l.CIRCLE_SHAPE||e==l.CARD_SHAPE?c.arc(0.5*f,-0.5*f,0.5*(f-8),0,2*Math.PI):c.rect(4,-f+4,f-8,f-8);c.fill();c.globalAlpha=s;0.5c-p._last_time&&(e=2-0.002*(c-p._last_time),n="rgba(255,255,255, "+e.toFixed(2)+")",this.renderLink(a,h,f.getConnectionPos(!0,k),p,!0,e,n))}}}}a.globalAlpha=1};d.prototype.renderLink=function(a,c,b,e,f,k,n){if(this.highquality_render){var h=r(c,b);this.render_connections_border&&0.6c[1]?0:Math.PI,a.save(),a.translate(e[0],e[1]),a.rotate(n),a.beginPath(),a.moveTo(-5,-5),a.lineTo(0,5),a.lineTo(5,-5),a.fill(),a.restore());if(k)for(k=0;5>k;++k)e=(0.001*l.getTime()+0.2*k)% -1,e=this.computeConnectionPoint(c,b,e),a.beginPath(),a.arc(e[0],e[1],5,0,2*Math.PI),a.fill()}else a.beginPath(),a.moveTo(c[0],c[1]),a.lineTo(b[0],b[1]),a.stroke()};d.prototype.computeConnectionPoint=function(a,c,b){var d=r(a,c),f=[a[0]+0.25*d,a[1]],d=[c[0]-0.25*d,c[1]],k=(1-b)*(1-b)*(1-b),e=3*(1-b)*(1-b)*b,n=3*(1-b)*b*b;b*=b*b;return[k*a[0]+e*f[0]+n*d[0]+b*c[0],k*a[1]+e*f[1]+n*d[1]+b*c[1]]};d.prototype.drawNodeWidgets=function(a,c,b,d){if(!a.widgets||!a.widgets.length)return 0;var f=a.size[0];a=a.widgets; -c+=2;for(var k=l.NODE_WIDGET_HEIGHT,e=0.5m.last_y&&km.options.max&&(m.value=m.options.max)):"mousedown"==b.type&&(b=40>f?-1:f>e-40?1:0,"number"==m.type?(m.value+=0.1*b*(m.options.step||1),null!=m.options.min&&m.valuem.options.max&&(m.value=m.options.max)):b&&(b=m.options.values.indexOf(m.value)+b,b>=m.options.values.length&&(b=0),0>b&&(b=m.options.values.length-1),m.value=m.options.values[b])),m.callback&& -setTimeout(function(){m.callback(m.value,n,a,c)},20),this.dirty_canvas=!0}return m}}return null};d.prototype.drawGroups=function(a,c){if(this.graph){var b=this.graph._groups;c.save();c.globalAlpha=0.5;c.font="24px Arial";for(var d=0;db&&0.01>c.editor_alpha&&(clearInterval(d),1>b&&(c.live_mode=!0));1"+m+""+a+"",value:m});if(h.length)return new l.ContextMenu(h,{event:b,callback:k,parentMenu:e, -allow_html:!0,node:f},c),!1}};d.decodeHTML=function(a){var c=document.createElement("div");c.innerText=a;return c.innerHTML};d.onResizeNode=function(a,c,b,d,f){f&&(f.size=f.computeSize(),f.setDirtyCanvas(!0,!0))};d.onShowTitleEditor=function(a,c,b,e,f){function k(){f.title=h.value;n.parentNode.removeChild(n);f.setDirtyCanvas(!0,!0)}var n=document.createElement("div");n.className="graphdialog";n.innerHTML="Title"; -var h=n.querySelector("input");h&&(h.value=f.title,h.addEventListener("keydown",function(a){13==a.keyCode&&(k(),a.preventDefault(),a.stopPropagation())}));a=d.active_canvas.canvas;c=a.getBoundingClientRect();e=b=-20;c&&(b-=c.left,e-=c.top);event?(n.style.left=event.pageX+b+"px",n.style.top=event.pageY+e+"px"):(n.style.left=0.5*a.width+b+"px",n.style.top=0.5*a.height+e+"px");n.querySelector("button").addEventListener("click",k);a.parentNode.appendChild(n)};d.prototype.showSearchBox=function(a){function c(c){if(c)if(f.onSearchBoxSelection)f.onSearchBoxSelection(c, -a,s);else if(c=l.createNode(c))c.pos=s.convertEventToCanvas(a),s.graph.add(c);k.close()}function b(a){var c=q;q&&q.classList.remove("selected");q?(q=a?q.nextSibling:q.previousSibling)||(q=c):q=a?n.childNodes[0]:n.childNodes[n.childNodes.length];q&&(q.classList.add("selected"),q.scrollIntoView())}function e(){m=null;var a=r.value;h=null;n.innerHTML="";if(a)if(f.onSearchBox)f.onSearchBox(g,a,s);else for(var b in l.registered_node_types)if(-1!=b.indexOf(a)){var g=document.createElement("div");h||(h= -b);g.innerText=b;g.className="help-item";g.addEventListener("click",function(a){c(this.innerText)});n.appendChild(g)}}var f=this,k=document.createElement("div");k.className="graphdialog";k.innerHTML="Search
";k.close=function(){f.search_box=null;k.parentNode.removeChild(k)};k.addEventListener("mouseleave",function(a){k.close()});f.search_box&&f.search_box.close();f.search_box=k;var n=k.querySelector(".helper"), -h=null,m=null,q=null,r=k.querySelector("input");r&&r.addEventListener("keydown",function(a){if(38==a.keyCode)b(!1);else if(40==a.keyCode)b(!0);else if(27==a.keyCode)k.close();else if(13==a.keyCode)q?c(q.innerHTML):h?c(h):k.close();else{m&&clearInterval(m);m=setTimeout(e,10);return}a.preventDefault();a.stopPropagation()});var s=d.active_canvas,u=s.canvas,t=u.getBoundingClientRect(),y=-20,E=-20;t&&(y-=t.left,E-=t.top);a?(k.style.left=a.pageX+y+"px",k.style.top=a.pageY+E+"px"):(k.style.left=0.5*u.width+ -y+"px",k.style.top=0.5*u.height+E+"px");u.parentNode.appendChild(k);r.focus();return k};d.prototype.showEditPropertyValue=function(a,c,b){function d(){f(q.value)}function f(b){"number"==typeof a.properties[c]&&(b=Number(b));"array"==k&&(b=b.split(",").map(Number));a.properties[c]=b;a._graph&&a._graph._version++;if(a.onPropertyChanged)a.onPropertyChanged(c,b);m.close();a.setDirtyCanvas(!0,!0)}if(a&&void 0!==a.properties[c]){b=b||{};var k="string";null!==a.properties[c]&&(k=typeof a.properties[c]); -"object"==k&&a.properties[c].length&&(k="array");var e=null;a.getPropertyInfo&&(e=a.getPropertyInfo(c));if(a.properties_info)for(var n=0;n";else if("enum"==k&&e.values){h=""}else if("boolean"==k)h="";else{console.warn("unknown type: "+k);return}var m=this.createDialog(""+c+""+h+"",b);if("enum"==k&&e.values){var q=m.querySelector("select");q.addEventListener("change",function(a){f(a.target.value)})}else if("boolean"==k)(q=m.querySelector("input"))&& -q.addEventListener("click",function(a){f(!!q.checked)});else if(q=m.querySelector("input"))q.value=void 0!==a.properties[c]?a.properties[c]:"",q.addEventListener("keydown",function(a){13==a.keyCode&&(d(),a.preventDefault(),a.stopPropagation())});m.querySelector("button").addEventListener("click",d)}};d.prototype.createDialog=function(a,c){c=c||{};var b=document.createElement("div");b.className="graphdialog";b.innerHTML=a;var d=this.canvas.getBoundingClientRect(),f=-20,k=-20;d&&(f-=d.left,k-=d.top); -c.position?(f+=c.position[0],k+=c.position[1]):c.event?(f+=c.event.pageX,k+=c.event.pageY):(f+=0.5*this.canvas.width,k+=0.5*this.canvas.height);b.style.left=f+"px";b.style.top=k+"px";this.canvas.parentNode.appendChild(b);b.close=function(){this.parentNode&&this.parentNode.removeChild(this)};return b};d.onMenuNodeCollapse=function(a,c,b,d,f){f.collapse()};d.onMenuNodePin=function(a,c,b,d,f){f.pin()};d.onMenuNodeMode=function(a,c,b,d,f){new l.ContextMenu(["Always","On Event","On Trigger","Never"],{event:b, -callback:function(a){if(f)switch(a){case "On Event":f.mode=l.ON_EVENT;break;case "On Trigger":f.mode=l.ON_TRIGGER;break;case "Never":f.mode=l.NEVER;break;default:f.mode=l.ALWAYS}},parentMenu:d,node:f});return!1};d.onMenuNodeColors=function(a,c,b,e,f){if(!f)throw"no node for color";c=[];c.push({value:null,content:"No color"});for(var k in d.node_colors)a=d.node_colors[k],a={value:k,content:""+k+""},c.push(a);new l.ContextMenu(c,{event:b,callback:function(a){f&&((a=a.value?d.node_colors[a.value]:null)?f.constructor===l.LGraphGroup?f.color=a.groupcolor:(f.color=a.color,f.bgcolor=a.bgcolor):(delete f.color,delete f.bgcolor),f.setDirtyCanvas(!0,!0))},parentMenu:e,node:f});return!1};d.onMenuNodeShapes=function(a,c,b,d,f){if(!f)throw"no node passed";new l.ContextMenu(l.VALID_SHAPES,{event:b,callback:function(a){f&&(f.shape=a,f.setDirtyCanvas(!0))}, -parentMenu:d,node:f});return!1};d.onMenuNodeRemove=function(a,c,b,d,f){if(!f)throw"no node passed";!1!==f.removable&&(f.graph.remove(f),f.setDirtyCanvas(!0,!0))};d.onMenuNodeClone=function(a,c,b,d,f){!1!=f.clonable&&(a=f.clone())&&(a.pos=[f.pos[0]+5,f.pos[1]+5],f.graph.add(a),f.setDirtyCanvas(!0,!0))};d.node_colors={red:{color:"#322",bgcolor:"#533",groupcolor:"#A88"},brown:{color:"#332922",bgcolor:"#593930",groupcolor:"#b06634"},green:{color:"#232",bgcolor:"#353",groupcolor:"#8A8"},blue:{color:"#223", +null;this.connections_width=3;this.round_radius=8;this.node_widget=this.current_node=null;this.last_mouse_position=[0,0];b&&b.attachCanvas(this);this.setCanvas(a);this.clear();g.skip_render||this.startRendering();this.autoresize=g.autoresize}function r(a,b){return Math.sqrt((b[0]-a[0])*(b[0]-a[0])+(b[1]-a[1])*(b[1]-a[1]))}function q(a,b,g,c,f,k){return ga&&cb?!0:!1}function t(a,b){var g=a[0]+a[2],c=a[1]+a[3],f=b[1]+b[3];return a[0]>b[0]+b[2]||a[1]>f||gd.width-l.width-10&&(k=d.width-l.width-10);p>d.height-l.height-10&&(p=d.height-l.height-10)}f.style.left=k+"px";f.style.top=p+"px"}var l=s.LiteGraph={NODE_TITLE_HEIGHT:20,NODE_SLOT_HEIGHT:15,NODE_WIDGET_HEIGHT:20,NODE_WIDTH:140,NODE_MIN_WIDTH:50,NODE_COLLAPSED_RADIUS:10, +NODE_COLLAPSED_WIDTH:80,CANVAS_GRID_SIZE:10,NODE_TITLE_COLOR:"#999",NODE_TEXT_SIZE:14,NODE_TEXT_COLOR:"#AAA",NODE_SUBTEXT_SIZE:12,NODE_DEFAULT_COLOR:"#333",NODE_DEFAULT_BGCOLOR:"#444",NODE_DEFAULT_BOXCOLOR:"#888",NODE_DEFAULT_SHAPE:"box",MAX_NUMBER_OF_NODES:1E3,DEFAULT_POSITION:[100,100],node_images_path:"",VALID_SHAPES:["default","box","round","card"],BOX_SHAPE:1,ROUND_SHAPE:2,CIRCLE_SHAPE:3,CARD_SHAPE:4,ARROW_SHAPE:5,INPUT:1,OUTPUT:2,EVENT:-1,ACTION:-1,ALWAYS:0,ON_EVENT:1,NEVER:2,ON_TRIGGER:3,NORMAL_TITLE:0, +NO_TITLE:1,TRANSPARENT_TITLE:2,AUTOHIDE_TITLE:3,proxy:null,debug:!1,throw_errors:!0,allow_scripts:!0,registered_node_types:{},node_types_by_file_extension:{},Nodes:{},registerNodeType:function(a,b){if(!b.prototype)throw"Cannot register a simple object, it must be a class with a prototype";b.type=a;l.debug&&console.log("Node registered: "+a);a.split("/");var g=b.name,c=a.lastIndexOf("/");b.category=a.substr(0,c);b.title||(b.title=g);if(b.prototype)for(var f in e.prototype)b.prototype[f]||(b.prototype[f]= +e.prototype[f]);Object.defineProperty(b.prototype,"shape",{set:function(a){switch(a){case "default":delete this._shape;break;case "box":this._shape=l.BOX_SHAPE;break;case "round":this._shape=l.ROUND_SHAPE;break;case "circle":this._shape=l.CIRCLE_SHAPE;break;case "card":this._shape=l.CARD_SHAPE;break;default:this._shape=a}},get:function(a){return this._shape},enumerable:!0});this.registered_node_types[a]=b;b.constructor.name&&(this.Nodes[g]=b);b.prototype.onPropertyChange&&console.warn("LiteGraph node class "+ +a+" has onPropertyChange method, it must be called onPropertyChanged with d at the end");if(b.supported_extensions)for(f in b.supported_extensions)this.node_types_by_file_extension[b.supported_extensions[f].toLowerCase()]=b},wrapFunctionAsNode:function(a,b,g,c){for(var f=Array(b.length),k="",p=l.getParameterNames(b),d=0;dp&&(p=f.size[0]),d+=f.size[1]+a;b+=p+a}this.setDirtyCanvas(!0,!0)};h.prototype.getTime=function(){return this.globaltime}; +h.prototype.getFixedTime=function(){return this.fixedtime};h.prototype.getElapsedTime=function(){return this.elapsed_time};h.prototype.sendEventToAllNodes=function(a,b,g){g=g||l.ALWAYS;var c=this._nodes_in_order?this._nodes_in_order:this._nodes;if(c)for(var f=0,k=c.length;f=l.MAX_NUMBER_OF_NODES)throw"LiteGraph: max number of nodes in a graph reached";null==a.id||-1==a.id?a.id=++this.last_node_id:this.last_node_id< +a.id&&(this.last_node_id=a.id);a.graph=this;this._version++;this._nodes.push(a);this._nodes_by_id[a.id]=a;if(a.onAdded)a.onAdded(this);this.config.align_to_grid&&a.alignToGrid();b||this.updateExecutionOrder();if(this.onNodeAdded)this.onNodeAdded(a);this.setDirtyCanvas(!0);this.change();return a}};h.prototype.remove=function(a){if(a.constructor===l.LGraphGroup){var b=this._groups.indexOf(a);-1!=b&&this._groups.splice(b,1);a.graph=null;this._version++;this.setDirtyCanvas(!0,!0);this.change()}else if(null!= +this._nodes_by_id[a.id]&&!a.ignore_remove){if(a.inputs)for(b=0;ba.length||(this._pos[0]=a[0],this._pos[1]=a[1])},get:function(){return this._pos},enumerable:!0});this.id=-1;this.type=null;this.inputs=[];this.outputs=[];this.connections=[];this.properties={};this.properties_info=[];this.data=null;this.flags={}};e.prototype.configure=function(a){this.graph&&this.graph._version++;for(var b in a)if("console"!= +b)if("properties"==b)for(var g in a.properties){if(this.properties[g]=a.properties[g],this.onPropertyChanged)this.onPropertyChanged(g,a.properties[g])}else null!=a[b]&&("object"==typeof a[b]?this[b]&&this[b].configure?this[b].configure(a[b]):this[b]=l.cloneObject(a[b],this[b]):this[b]=a[b]);a.title||(this.title=this.constructor.title);if(this.onConnectionsChange){if(this.inputs)for(var c=0;c=this.outputs.length)){var g=this.outputs[a];if(g&&(g._data=b,this.outputs[a].links))for(g=0;g=this.inputs.length|| +null==this.inputs[a].link)){var g=this.graph.links[this.inputs[a].link];if(!g)return null;if(!b)return g.data;var c=this.graph.getNodeById(g.origin_id);if(!c)return g.data;if(c.updateOutputData)c.updateOutputData(g.origin_slot);else if(c.onExecute)c.onExecute();return g.data}};e.prototype.getInputDataByName=function(a,b){var g=this.findInputSlot(a);return-1==g?null:this.getInputData(g,b)};e.prototype.isInputConnected=function(a){return this.inputs?a=this.inputs.length)return null;a=this.inputs[a];return a&&null!==a.link?(a=this.graph.links[a.link])?this.graph.getNodeById(a.origin_id):null:null};e.prototype.getInputOrProperty=function(a){if(!this.inputs||!this.inputs.length)return this.properties?this.properties[a]:null;for(var b=0,g=this.inputs.length;b=this.outputs.length?null:this.outputs[a]._data};e.prototype.getOutputInfo=function(a){return this.outputs?a=this.outputs.length)return null;a=this.outputs[a];if(!a.links||0==a.links.length)return null;for(var b=[],g=0;ga&&this.pos[1]-c-gb)return!0;return!1};e.prototype.getSlotInPosition=function(a,b){if(this.inputs)for(var g=0,c=this.inputs.length;g=this.outputs.length)return l.debug&&console.log("Connect: Error, slot number not found"),!1;b&&b.constructor===Number&&(b=this.graph.getNodeById(b)); +if(!b)throw"target node is null";if(b==this)return!1;if(g.constructor===String){if(g=b.findInputSlot(g),-1==g)return l.debug&&console.log("Connect: Error, no slot of name "+g),!1}else{if(g===l.EVENT)return!1;if(!b.inputs||g>=b.inputs.length)return l.debug&&console.log("Connect: Error, slot number not found"),!1}null!=b.inputs[g].link&&b.disconnectInput(g);var c=this.outputs[a];if(b.onConnectInput&&!1===b.onConnectInput(g,c.type,c))return!1;var f=b.inputs[g];if(l.isValidConnection(c.type,f.type)){var k= +{id:this.graph.last_link_id++,type:f.type,origin_id:this.id,origin_slot:a,target_id:b.id,target_slot:g};this.graph.links[k.id]=k;null==c.links&&(c.links=[]);c.links.push(k.id);b.inputs[g].link=k.id;this.graph&&this.graph._version++;if(this.onConnectionsChange)this.onConnectionsChange(l.OUTPUT,a,!0,k,c);if(b.onConnectionsChange)b.onConnectionsChange(l.INPUT,g,!0,k,f);if(this.graph&&this.graph.onNodeConnectionChange)this.graph.onNodeConnectionChange(l.OUTPUT,this,a,b,g)}this.setDirtyCanvas(!1,!0);this.graph.connectionChange(this); +return!0};e.prototype.disconnectOutput=function(a,b){if(a.constructor===String){if(a=this.findOutputSlot(a),-1==a)return l.debug&&console.log("Connect: Error, no slot of name "+a),!1}else if(!this.outputs||a>=this.outputs.length)return l.debug&&console.log("Connect: Error, slot number not found"),!1;var g=this.outputs[a];if(!g.links||0==g.links.length)return!1;if(b){b.constructor===Number&&(b=this.graph.getNodeById(b));if(!b)throw"Target Node not found";for(var c=0,f=g.links.length;c=this.inputs.length)return l.debug&&console.log("Connect: Error, slot number not found"),!1;var b=this.inputs[a];if(!b)return!1; +var g=this.inputs[a].link;this.inputs[a].link=null;var c=this.graph.links[g];if(c){var f=this.graph.getNodeById(c.origin_id);if(!f)return!1;var k=f.outputs[c.origin_slot];if(!k||!k.links||0==k.links.length)return!1;for(var d=0,m=k.links.length;db&&this.inputs[b].pos?[this.pos[0]+this.inputs[b].pos[0],this.pos[1]+this.inputs[b].pos[1]]:!a&&this.outputs.length>b&&this.outputs[b].pos?[this.pos[0]+this.outputs[b].pos[0], +this.pos[1]+this.outputs[b].pos[1]]:a?[this.pos[0],this.pos[1]+10+b*l.NODE_SLOT_HEIGHT+(this.constructor.slot_start_y||0)]:[this.pos[0]+this.size[0]+1,this.pos[1]+10+b*l.NODE_SLOT_HEIGHT+(this.constructor.slot_start_y||0)]};e.prototype.alignToGrid=function(){this.pos[0]=l.CANVAS_GRID_SIZE*Math.round(this.pos[0]/l.CANVAS_GRID_SIZE);this.pos[1]=l.CANVAS_GRID_SIZE*Math.round(this.pos[1]/l.CANVAS_GRID_SIZE)};e.prototype.trace=function(a){this.console||(this.console=[]);this.console.push(a);this.console.length> +e.MAX_CONSOLE&&this.console.shift();this.graph.onNodeTrace(this,a)};e.prototype.setDirtyCanvas=function(a,b){this.graph&&this.graph.sendActionToCanvas("setDirty",[a,b])};e.prototype.loadImage=function(a){var b=new Image;b.src=l.node_images_path+a;b.ready=!1;var g=this;b.onload=function(){this.ready=!0;g.setDirtyCanvas(!0)};return b};e.prototype.captureInput=function(a){if(this.graph&&this.graph.list_of_graphcanvas)for(var b=this.graph.list_of_graphcanvas,g=0;ga.length||(this._pos[0]=a[0],this._pos[1]=a[1])},get:function(){return this._pos},enumerable:!0});Object.defineProperty(this,"size",{set:function(a){!a||2>a.length||(this._size[0]=Math.max(140,a[0]), +this._size[1]=Math.max(80,a[1]))},get:function(){return this._size},enumerable:!0})};n.prototype.configure=function(a){this.title=a.title;this._bounding.set(a.bounding);this.color=a.color};n.prototype.serialize=function(){var a=this._bounding;return{title:this.title,bounding:[a[0],a[1],a[2],a[3]],color:this.color}};n.prototype.move=function(a,b,g){this._pos[0]+=a;this._pos[1]+=b;if(!g)for(g=0;g element, you passed a "+ +a.localName;throw"This browser doesnt support Canvas";}null==(this.ctx=a.getContext("2d"))&&(a.webgl_enabled||console.warn("This canvas seems to be WebGL, enabling WebGL renderer"),this.enableWebGL());this._mousemove_callback=this.processMouseMove.bind(this);this._mouseup_callback=this.processMouseUp.bind(this);b||this.bindEvents()}};d.prototype._doNothing=function(a){a.preventDefault();return!1};d.prototype._doReturnTrue=function(a){a.preventDefault();return!0};d.prototype.bindEvents=function(){if(this._events_binded)console.warn("LGraphCanvas: events already binded"); +else{var a=this.canvas,b=this.getCanvasWindow().document;this._mousedown_callback=this.processMouseDown.bind(this);this._mousewheel_callback=this.processMouseWheel.bind(this);a.addEventListener("mousedown",this._mousedown_callback,!0);a.addEventListener("mousemove",this._mousemove_callback);a.addEventListener("mousewheel",this._mousewheel_callback,!1);a.addEventListener("contextmenu",this._doNothing);a.addEventListener("DOMMouseScroll",this._mousewheel_callback,!1);a.addEventListener("touchstart", +this.touchHandler,!0);a.addEventListener("touchmove",this.touchHandler,!0);a.addEventListener("touchend",this.touchHandler,!0);a.addEventListener("touchcancel",this.touchHandler,!0);this._key_callback=this.processKey.bind(this);a.addEventListener("keydown",this._key_callback,!0);b.addEventListener("keyup",this._key_callback,!0);this._ondrop_callback=this.processDrop.bind(this);a.addEventListener("dragover",this._doNothing,!1);a.addEventListener("dragend",this._doNothing,!1);a.addEventListener("drop", +this._ondrop_callback,!1);a.addEventListener("dragenter",this._doReturnTrue,!1);this._events_binded=!0}};d.prototype.unbindEvents=function(){if(this._events_binded){var a=this.getCanvasWindow().document;this.canvas.removeEventListener("mousedown",this._mousedown_callback);this.canvas.removeEventListener("mousewheel",this._mousewheel_callback);this.canvas.removeEventListener("DOMMouseScroll",this._mousewheel_callback);this.canvas.removeEventListener("keydown",this._key_callback);a.removeEventListener("keyup", +this._key_callback);this.canvas.removeEventListener("contextmenu",this._doNothing);this.canvas.removeEventListener("drop",this._ondrop_callback);this.canvas.removeEventListener("dragenter",this._doReturnTrue);this.canvas.removeEventListener("touchstart",this.touchHandler);this.canvas.removeEventListener("touchmove",this.touchHandler);this.canvas.removeEventListener("touchend",this.touchHandler);this.canvas.removeEventListener("touchcancel",this.touchHandler);this._ondrop_callback=this._key_callback= +this._mousewheel_callback=this._mousedown_callback=null;this._events_binded=!1}else console.warn("LGraphCanvas: no events binded")};d.getFileExtension=function(a){var b=a.indexOf("?");-1!=b&&(a=a.substr(0,b));b=a.lastIndexOf(".");return-1==b?"":a.substr(b+1).toLowerCase()};d.prototype.enableWebGL=function(){if(void 0===typeof GL)throw"litegl.js must be included to use a WebGL canvas";if(void 0===typeof enableWebGLCanvas)throw"webglCanvas.js must be included to use this feature";this.gl=this.ctx=enableWebGLCanvas(this.canvas); +this.ctx.webgl=!0;this.bgcanvas=this.canvas;this.bgctx=this.gl;this.canvas.webgl_enabled=!0};d.prototype.setDirty=function(a,b){a&&(this.dirty_canvas=!0);b&&(this.dirty_bgcanvas=!0)};d.prototype.getCanvasWindow=function(){if(!this.canvas)return window;var a=this.canvas.ownerDocument;return a.defaultView||a.parentWindow};d.prototype.startRendering=function(){function a(){this.pause_rendering||this.draw();var b=this.getCanvasWindow();this.is_rendering&&b.requestAnimationFrame(a.bind(this))}this.is_rendering|| +(this.is_rendering=!0,a.call(this))};d.prototype.stopRendering=function(){this.is_rendering=!1};d.prototype.processMouseDown=function(a){if(this.graph){this.adjustMouseEvent(a);var b=this.getCanvasWindow();d.active_canvas=this;this.canvas.removeEventListener("mousemove",this._mousemove_callback);b.document.addEventListener("mousemove",this._mousemove_callback,!0);b.document.addEventListener("mouseup",this._mouseup_callback,!0);var g=this.graph.getNodeOnPos(a.canvasX,a.canvasY,this.visible_nodes), +c=!1,f=300>l.getTime()-this.last_mouseclick;this.canvas_mouse[0]=a.canvasX;this.canvas_mouse[1]=a.canvasY;l.closeAllContextMenus(b);if(1==a.which){a.ctrlKey&&(this.dragging_rectangle=new Float32Array(4),this.dragging_rectangle[0]=a.canvasX,this.dragging_rectangle[1]=a.canvasY,this.dragging_rectangle[2]=1,this.dragging_rectangle[3]=1,c=!0);var k=!1;if(g&&this.allow_interaction&&!c){this.live_mode||g.flags.pinned||this.bringToFront(g);if(!this.connecting_node&&!g.flags.collapsed&&!this.live_mode){if(g.outputs)for(var p= +0,m=g.outputs.length;pr([a.canvasX,a.canvasY],[this.selected_group.pos[0]+this.selected_group.size[0], +this.selected_group.pos[1]+this.selected_group.size[1]])*this.scale?this.selected_group_resizing=!0:this.selected_group.recomputeInsideNodes()),f&&this.showSearchBox(a),k=!0;!c&&k&&this.allow_dragcanvas&&(this.dragging_canvas=!0)}else 2!=a.which&&3==a.which&&this.processContextMenu(g,a);this.last_mouse[0]=a.localX;this.last_mouse[1]=a.localY;this.last_mouseclick=l.getTime();this.graph.change();(!b.document.activeElement||"input"!=b.document.activeElement.nodeName.toLowerCase()&&"textarea"!=b.document.activeElement.nodeName.toLowerCase())&& +a.preventDefault();a.stopPropagation();if(this.onMouseDown)this.onMouseDown(a);return!1}};d.prototype.processMouseMove=function(a){this.autoresize&&this.resize();if(this.graph){d.active_canvas=this;this.adjustMouseEvent(a);var b=[a.localX,a.localY],g=[b[0]-this.last_mouse[0],b[1]-this.last_mouse[1]];this.last_mouse=b;this.canvas_mouse[0]=a.canvasX;this.canvas_mouse[1]=a.canvasY;this.node_widget&&(this.processNodeWidgets(this.node_widget[0],this.canvas_mouse,a,this.node_widget[1]),this.dirty_canvas= +!0);if(this.dragging_rectangle)this.dragging_rectangle[2]=a.canvasX-this.dragging_rectangle[0],this.dragging_rectangle[3]=a.canvasY-this.dragging_rectangle[1],this.dirty_canvas=!0;else if(this.selected_group)this.selected_group_resizing?this.selected_group.size=[a.canvasX-this.selected_group.pos[0],a.canvasY-this.selected_group.pos[1]]:(this.selected_group.move(g[0]/this.scale,g[1]/this.scale,a.ctrlKey),this.selected_group._nodes.length&&(this.dirty_canvas=!0)),this.dirty_bgcanvas=!0;else if(this.dragging_canvas)this.offset[0]+= +g[0]/this.scale,this.offset[1]+=g[1]/this.scale,this.dirty_bgcanvas=this.dirty_canvas=!0;else if(this.allow_interaction){this.connecting_node&&(this.dirty_canvas=!0);for(var c=this.graph.getNodeOnPos(a.canvasX,a.canvasY,this.visible_nodes),b=0,f=this.graph._nodes.length;bthis.dragging_rectangle[2]&&(this.dragging_rectangle[0]+=this.dragging_rectangle[2]);0>this.dragging_rectangle[3]&&(this.dragging_rectangle[1]+=this.dragging_rectangle[3]);this.dragging_rectangle[2]=Math.abs(this.dragging_rectangle[2]*this.scale);this.dragging_rectangle[3]=Math.abs(this.dragging_rectangle[3]*this.scale);for(var f=0;fa.click_time&&this.deselectAllNodes();this.dirty_canvas=!0;this.dragging_canvas=!1;if(this.node_over&&this.node_over.onMouseUp)this.node_over.onMouseUp(a,[a.canvasX-this.node_over.pos[0],a.canvasY-this.node_over.pos[1]]);if(this.node_capturing_input&&this.node_capturing_input.onMouseUp)this.node_capturing_input.onMouseUp(a, +[a.canvasX-this.node_capturing_input.pos[0],a.canvasY-this.node_capturing_input.pos[1]])}else 2==a.which?(this.dirty_canvas=!0,this.dragging_canvas=!1):3==a.which&&(this.dirty_canvas=!0,this.dragging_canvas=!1);this.graph.change();a.stopPropagation();a.preventDefault();return!1}};d.prototype.processMouseWheel=function(a){if(this.graph&&this.allow_dragcanvas){var b=null!=a.wheelDeltaY?a.wheelDeltaY:-60*a.detail;this.adjustMouseEvent(a);var g=this.scale;0b&&(g*=1/1.1);this.setZoom(g,[a.localX, +a.localY]);this.graph.change();a.preventDefault();return!1}};d.prototype.isOverNodeBox=function(a,b,g){var c=l.NODE_TITLE_HEIGHT;return q(b,g,a.pos[0]+2,a.pos[1]+2-c,c-4,c-4)?!0:!1};d.prototype.isOverNodeInput=function(a,b,g,c){if(a.inputs)for(var f=0,k=a.inputs.length;fthis.max_zoom?this.scale=this.max_zoom:this.scalec-this.graph._last_trigger_time)&&this.drawBackCanvas();(this.dirty_canvas||a)&&this.drawFrontCanvas();this.fps=this.render_time?1/this.render_time:0;this.frame+=1}};d.prototype.drawFrontCanvas=function(){this.dirty_canvas=!1;this.ctx||(this.ctx=this.bgcanvas.getContext("2d"));var a=this.ctx;if(a){a.start2D&& +a.start2D();var b=this.canvas;a.restore();a.setTransform(1,0,0,1,0,0);this.dirty_area&&(a.save(),a.beginPath(),a.rect(this.dirty_area[0],this.dirty_area[1],this.dirty_area[2],this.dirty_area[3]),a.clip());this.clear_background&&a.clearRect(0,0,b.width,b.height);this.bgcanvas==this.canvas?this.drawBackCanvas():a.drawImage(this.bgcanvas,0,0);if(this.onRender)this.onRender(b,a);this.show_info&&this.renderInfo(a);if(this.graph){a.save();a.scale(this.scale,this.scale);a.translate(this.offset[0],this.offset[1]); +for(var b=this.computeVisibleNodes(null,this.visible_nodes),c=0;cthis.scale?(b.beginPath(),b.rect(0,p,n,q),b.fill()):e==l.ROUND_SHAPE||e==l.CARD_SHAPE?(b.beginPath(),b.roundRect(0,p,n,q,this.round_radius,e==l.CARD_SHAPE?0:this.round_radius),b.fill()):e==l.CIRCLE_SHAPE&&(b.beginPath(), +b.arc(0.5*c[0],0.5*c[1],0.5*c[0],0,2*Math.PI),b.fill()));b.shadowColor="transparent";a.bgImage&&a.bgImage.width&&b.drawImage(a.bgImage,0.5*(c[0]-a.bgImage.width),0.5*(c[1]-a.bgImage.height));a.bgImageUrl&&!a.bgImage&&(a.bgImage=a.loadImage(a.bgImageUrl));a.onDrawBackground&&(a.gui_rects&&(a.gui_rects.length=0),a.onDrawBackground(b,this));if(y||h==l.TRANSPARENT_TITLE){if(h!=l.TRANSPARENT_TITLE){if(this.use_gradients){var r=d.gradients[m];r||(r=d.gradients[m]=b.createLinearGradient(0,0,400,0),r.addColorStop(0, +m),r.addColorStop(1,"#000"));b.fillStyle=r}else b.fillStyle=m;r=b.globalAlpha;b.beginPath();if(e==l.BOX_SHAPE||0.5>this.scale)b.rect(0,-f,c[0]+1,f),b.fill();else if(e==l.ROUND_SHAPE||e==l.CARD_SHAPE)b.roundRect(0,-f,c[0]+1,f,this.round_radius,a.flags.collapsed?this.round_radius:0),b.fill()}b.fillStyle=a.boxcolor||l.NODE_DEFAULT_BOXCOLOR;b.beginPath();e==l.ROUND_SHAPE||e==l.CIRCLE_SHAPE||e==l.CARD_SHAPE?b.arc(0.5*f,-0.5*f,0.5*(f-8),0,2*Math.PI):b.rect(4,-f+4,f-8,f-8);b.fill();b.globalAlpha=r;0.5b-p._last_time&&(m=2-0.002*(b-p._last_time),e="rgba(255,255,255, "+m.toFixed(2)+")",this.renderLink(a,h,f.getConnectionPos(!0,k),p,!0,m,e))}}}}a.globalAlpha=1};d.prototype.renderLink=function(a,b,c,m,f,k,p){if(this.highquality_render){var e=r(b,c);this.render_connections_border&& +0.6b[1]?0:Math.PI,a.save(),a.translate(m[0],m[1]),a.rotate(p),a.beginPath(),a.moveTo(-5,-5),a.lineTo(0,5),a.lineTo(5,-5),a.fill(),a.restore());if(k)for(k=0;5>k;++k)m=(0.001* +l.getTime()+0.2*k)%1,m=this.computeConnectionPoint(b,c,m),a.beginPath(),a.arc(m[0],m[1],5,0,2*Math.PI),a.fill()}else a.beginPath(),a.moveTo(b[0],b[1]),a.lineTo(c[0],c[1]),a.stroke()};d.prototype.computeConnectionPoint=function(a,b,c){var d=r(a,b),f=[a[0]+0.25*d,a[1]],d=[b[0]-0.25*d,b[1]],k=(1-c)*(1-c)*(1-c),m=3*(1-c)*(1-c)*c,e=3*(1-c)*c*c;c*=c*c;return[k*a[0]+m*f[0]+e*d[0]+c*b[0],k*a[1]+m*f[1]+e*d[1]+c*b[1]]};d.prototype.drawNodeWidgets=function(a,b,c,d){if(!a.widgets||!a.widgets.length)return 0; +var f=a.size[0];a=a.widgets;b+=2;for(var k=l.NODE_WIDGET_HEIGHT,m=0.5n.last_y&&kn.options.max&&(n.value=n.options.max)):"mousedown"==c.type&&(c=40>f?-1:f>m-40?1:0,"number"==n.type?(n.value+=0.1*c*(n.options.step||1),null!=n.options.min&&n.valuen.options.max&&(n.value=n.options.max)):c&&(c=n.options.values.indexOf(n.value)+c,c>=n.options.values.length&&(c=0),0>c&&(c=n.options.values.length-1),n.value=n.options.values[c])),n.callback&& +setTimeout(function(){n.callback(n.value,e,a,b)},20),this.dirty_canvas=!0}return n}}return null};d.prototype.drawGroups=function(a,b){if(this.graph){var c=this.graph._groups;b.save();b.globalAlpha=0.5;b.font="24px Arial";for(var d=0;dc&&0.01>b.editor_alpha&&(clearInterval(d),1>c&&(b.live_mode=!0));1"+n+""+a+"",value:n});if(h.length)return new l.ContextMenu(h,{event:c,callback:k,parentMenu:m, +allow_html:!0,node:f},b),!1}};d.decodeHTML=function(a){var b=document.createElement("div");b.innerText=a;return b.innerHTML};d.onResizeNode=function(a,b,c,d,f){f&&(f.size=f.computeSize(),f.setDirtyCanvas(!0,!0))};d.onShowTitleEditor=function(a,b,c,m,f){function k(){f.title=h.value;e.parentNode.removeChild(e);f.setDirtyCanvas(!0,!0)}var e=document.createElement("div");e.className="graphdialog";e.innerHTML="Title"; +var h=e.querySelector("input");h&&(h.value=f.title,h.addEventListener("keydown",function(a){13==a.keyCode&&(k(),a.preventDefault(),a.stopPropagation())}));a=d.active_canvas.canvas;b=a.getBoundingClientRect();m=c=-20;b&&(c-=b.left,m-=b.top);event?(e.style.left=event.pageX+c+"px",e.style.top=event.pageY+m+"px"):(e.style.left=0.5*a.width+c+"px",e.style.top=0.5*a.height+m+"px");e.querySelector("button").addEventListener("click",k);a.parentNode.appendChild(e)};d.prototype.showSearchBox=function(a){function b(b){if(b)if(f.onSearchBoxSelection)f.onSearchBoxSelection(b, +a,t);else if(b=l.createNode(b))b.pos=t.convertEventToCanvas(a),t.graph.add(b);k.close()}function c(a){var b=q;q&&q.classList.remove("selected");q?(q=a?q.nextSibling:q.previousSibling)||(q=b):q=a?e.childNodes[0]:e.childNodes[e.childNodes.length];q&&(q.classList.add("selected"),q.scrollIntoView())}function m(){n=null;var a=r.value;h=null;e.innerHTML="";if(a)if(f.onSearchBox)f.onSearchBox(g,a,t);else for(var c in l.registered_node_types)if(-1!=c.indexOf(a)){var g=document.createElement("div");h||(h= +c);g.innerText=c;g.className="help-item";g.addEventListener("click",function(a){b(this.innerText)});e.appendChild(g)}}var f=this,k=document.createElement("div");k.className="graphdialog";k.innerHTML="Search
";k.close=function(){f.search_box=null;k.parentNode.removeChild(k)};k.addEventListener("mouseleave",function(a){k.close()});f.search_box&&f.search_box.close();f.search_box=k;var e=k.querySelector(".helper"), +h=null,n=null,q=null,r=k.querySelector("input");r&&r.addEventListener("keydown",function(a){if(38==a.keyCode)c(!1);else if(40==a.keyCode)c(!0);else if(27==a.keyCode)k.close();else if(13==a.keyCode)q?b(q.innerHTML):h?b(h):k.close();else{n&&clearInterval(n);n=setTimeout(m,10);return}a.preventDefault();a.stopPropagation()});var t=d.active_canvas,u=t.canvas,s=u.getBoundingClientRect(),z=-20,E=-20;s&&(z-=s.left,E-=s.top);a?(k.style.left=a.pageX+z+"px",k.style.top=a.pageY+E+"px"):(k.style.left=0.5*u.width+ +z+"px",k.style.top=0.5*u.height+E+"px");u.parentNode.appendChild(k);r.focus();return k};d.prototype.showEditPropertyValue=function(a,b,c){function d(){f(q.value)}function f(c){"number"==typeof a.properties[b]&&(c=Number(c));"array"==k&&(c=c.split(",").map(Number));a.properties[b]=c;a._graph&&a._graph._version++;if(a.onPropertyChanged)a.onPropertyChanged(b,c);n.close();a.setDirtyCanvas(!0,!0)}if(a&&void 0!==a.properties[b]){c=c||{};var k="string";null!==a.properties[b]&&(k=typeof a.properties[b]); +"object"==k&&a.properties[b].length&&(k="array");var m=null;a.getPropertyInfo&&(m=a.getPropertyInfo(b));if(a.properties_info)for(var e=0;e";else if("enum"==k&&m.values){h=""}else if("boolean"==k)h="";else{console.warn("unknown type: "+k);return}var n=this.createDialog(""+b+""+h+"",c);if("enum"==k&&m.values){var q=n.querySelector("select");q.addEventListener("change",function(a){f(a.target.value)})}else if("boolean"==k)(q=n.querySelector("input"))&& +q.addEventListener("click",function(a){f(!!q.checked)});else if(q=n.querySelector("input"))q.value=void 0!==a.properties[b]?a.properties[b]:"",q.addEventListener("keydown",function(a){13==a.keyCode&&(d(),a.preventDefault(),a.stopPropagation())});n.querySelector("button").addEventListener("click",d)}};d.prototype.createDialog=function(a,b){b=b||{};var c=document.createElement("div");c.className="graphdialog";c.innerHTML=a;var d=this.canvas.getBoundingClientRect(),f=-20,k=-20;d&&(f-=d.left,k-=d.top); +b.position?(f+=b.position[0],k+=b.position[1]):b.event?(f+=b.event.pageX,k+=b.event.pageY):(f+=0.5*this.canvas.width,k+=0.5*this.canvas.height);c.style.left=f+"px";c.style.top=k+"px";this.canvas.parentNode.appendChild(c);c.close=function(){this.parentNode&&this.parentNode.removeChild(this)};return c};d.onMenuNodeCollapse=function(a,b,c,d,f){f.collapse()};d.onMenuNodePin=function(a,b,c,d,f){f.pin()};d.onMenuNodeMode=function(a,b,c,d,f){new l.ContextMenu(["Always","On Event","On Trigger","Never"],{event:c, +callback:function(a){if(f)switch(a){case "On Event":f.mode=l.ON_EVENT;break;case "On Trigger":f.mode=l.ON_TRIGGER;break;case "Never":f.mode=l.NEVER;break;default:f.mode=l.ALWAYS}},parentMenu:d,node:f});return!1};d.onMenuNodeColors=function(a,b,c,m,f){if(!f)throw"no node for color";b=[];b.push({value:null,content:"No color"});for(var k in d.node_colors)a=d.node_colors[k],a={value:k,content:""+k+""},b.push(a);new l.ContextMenu(b,{event:c,callback:function(a){f&&((a=a.value?d.node_colors[a.value]:null)?f.constructor===l.LGraphGroup?f.color=a.groupcolor:(f.color=a.color,f.bgcolor=a.bgcolor):(delete f.color,delete f.bgcolor),f.setDirtyCanvas(!0,!0))},parentMenu:m,node:f});return!1};d.onMenuNodeShapes=function(a,b,c,d,f){if(!f)throw"no node passed";new l.ContextMenu(l.VALID_SHAPES,{event:c,callback:function(a){f&&(f.shape=a,f.setDirtyCanvas(!0))}, +parentMenu:d,node:f});return!1};d.onMenuNodeRemove=function(a,b,c,d,f){if(!f)throw"no node passed";!1!==f.removable&&(f.graph.remove(f),f.setDirtyCanvas(!0,!0))};d.onMenuNodeClone=function(a,b,c,d,f){!1!=f.clonable&&(a=f.clone())&&(a.pos=[f.pos[0]+5,f.pos[1]+5],f.graph.add(a),f.setDirtyCanvas(!0,!0))};d.node_colors={red:{color:"#322",bgcolor:"#533",groupcolor:"#A88"},brown:{color:"#332922",bgcolor:"#593930",groupcolor:"#b06634"},green:{color:"#232",bgcolor:"#353",groupcolor:"#8A8"},blue:{color:"#223", bgcolor:"#335",groupcolor:"#88A"},pale_blue:{color:"#2a363b",bgcolor:"#3f5159",groupcolor:"#3f789e"},cyan:{color:"#233",bgcolor:"#355",groupcolor:"#8AA"},purple:{color:"#323",bgcolor:"#535",groupcolor:"#a1309b"},yellow:{color:"#432",bgcolor:"#653",groupcolor:"#b58b2a"},black:{color:"#222",bgcolor:"#000",groupcolor:"#444"}};d.prototype.getCanvasMenuOptions=function(){var a=null;this.getMenuOptions?a=this.getMenuOptions():(a=[{content:"Add Node",has_submenu:!0,callback:d.onMenuAdd},{content:"Add Group", -callback:d.onGroupAdd}],this._graph_stack&&0Name",f),n=e.querySelector("input"); -e.querySelector("button").addEventListener("click",function(c){if(n.value){if(c=k.input?a.getInputInfo(k.slot):a.getOutputInfo(k.slot))c.label=n.value;b.setDirty(!0)}e.close()})}},node:a},n=null;a&&(n=a.getSlotInPosition(c.canvasX,c.canvasY),d.active_node=a);n?(f=[],f.push(n.locked?"Cannot remove":{content:"Remove Slot",slot:n}),f.push({content:"Rename Slot",slot:n}),k.title=(n.input?n.input.type:n.output.type)||"*",n.input&&n.input.type==l.ACTION&&(k.title="Action"),n.output&&n.output.type==l.EVENT&& -(k.title="Event")):a?f=this.getNodeMenuOptions(a):(f=this.graph.getGroupOnPos(c.canvasX,c.canvasY))?(k.node=f,f=this.getGroupMenuOptions(f)):f=this.getCanvasMenuOptions();f&&new l.ContextMenu(f,k,e)};this.CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.roundRect=function(a,c,b,d,f,k){void 0===f&&(f=5);void 0===k&&(k=f);this.moveTo(a+f,c);this.lineTo(a+b-f,c);this.quadraticCurveTo(a+b,c,a+b,c+f);this.lineTo(a+b,c+d-k);this.quadraticCurveTo(a+b,c+d,a+b-k,c+d);this.lineTo(a+k,c+d);this.quadraticCurveTo(a, -c+d,a,c+d-k);this.lineTo(a,c+f);this.quadraticCurveTo(a,c,a+f,c)});l.compareObjects=function(a,c){for(var b in a)if(a[b]!=c[b])return!1;return!0};l.distance=r;l.colorToString=function(a){return"rgba("+Math.round(255*a[0]).toFixed()+","+Math.round(255*a[1]).toFixed()+","+Math.round(255*a[2]).toFixed()+","+(4==a.length?a[3].toFixed(2):"1.0")+")"};l.isInsideRectangle=q;l.growBounding=function(a,c,b){ca[2]&&(a[2]=c);ba[3]&&(a[3]=b)};l.isInsideBounding=function(a,c){return a[0]< -c[0][0]||a[1]c[1][0]||a[1]>c[1][1]?!1:!0};l.overlapBounding=s;l.hex2num=function(a){"#"==a.charAt(0)&&(a=a.slice(1));a=a.toUpperCase();for(var c=Array(3),b=0,d,f,k=0;6>k;k+=2)d="0123456789ABCDEF".indexOf(a.charAt(k)),f="0123456789ABCDEF".indexOf(a.charAt(k+1)),c[b]=16*d+f,b++;return c};l.num2hex=function(a){for(var c="#",b,d,f=0;3>f;f++)b=a[f]/16,d=a[f]%16,c+="0123456789ABCDEF".charAt(b)+"0123456789ABCDEF".charAt(d);return c};u.prototype.addItem=function(a,c,b){function d(a){var c= -this.value;c&&c.has_submenu&&f.call(this,a)}function f(a){var c=this.value,d=!0;k.current_submenu&&k.current_submenu.close(a);if(b.callback){var f=b.callback.call(this,c,b,a,k,b.node);!0===f&&(d=!1)}if(c&&(c.callback&&!b.ignore_item_callbacks&&!0!==c.disabled&&(f=c.callback.call(this,c,b,a,k,b.node),!0===f&&(d=!1)),c.submenu)){if(!c.submenu.options)throw"ContextMenu submenu needs options";new k.constructor(c.submenu.options,{callback:c.submenu.callback,event:a,parentMenu:k,ignore_item_callbacks:c.submenu.ignore_item_callbacks, -title:c.submenu.title,autoopen:b.autoopen});d=!1}d&&!k.lock&&k.close()}var k=this;b=b||{};var e=document.createElement("div");e.className="litemenu-entry submenu";var n=!1;if(null===c)e.classList.add("separator");else{e.innerHTML=c&&c.title?c.title:a;if(e.value=c)c.disabled&&(n=!0,e.classList.add("disabled")),(c.submenu||c.has_submenu)&&e.classList.add("has_submenu");"function"==typeof c?(e.dataset.value=a,e.onclick_callback=c):e.dataset.value=c;c.className&&(e.className+=" "+c.className)}this.root.appendChild(e); -n||e.addEventListener("click",f);b.autoopen&&e.addEventListener("mouseenter",d);return e};u.prototype.close=function(a,c){this.root.parentNode&&this.root.parentNode.removeChild(this.root);this.parentMenu&&!c&&(this.parentMenu.lock=!1,this.parentMenu.current_submenu=null,void 0===a?this.parentMenu.close():a&&!u.isCursorOverElement(a,this.parentMenu.root)&&u.trigger(this.parentMenu.root,"mouseleave",a));this.current_submenu&&this.current_submenu.close(a,!0)};u.trigger=function(a,c,b,d){var f=document.createEvent("CustomEvent"); -f.initCustomEvent(c,!0,!0,b);f.srcElement=d;a.dispatchEvent?a.dispatchEvent(f):a.__events&&a.__events.dispatchEvent(f);return f};u.prototype.getTopMenu=function(){return this.options.parentMenu?this.options.parentMenu.getTopMenu():this};u.prototype.getFirstEvent=function(){return this.options.parentMenu?this.options.parentMenu.getFirstEvent():this.options.event};u.isCursorOverElement=function(a,c){var b=a.pageX,d=a.pageY,f=c.getBoundingClientRect();return f?d>f.top&&df.left&&b< -f.left+f.width?!0:!1:!1};l.ContextMenu=u;l.closeAllContextMenus=function(a){a=a||window;a=a.document.querySelectorAll(".litecontextmenu");if(a.length){for(var c=[],b=0;ba?c:bName",f),e=m.querySelector("input"); +m.querySelector("button").addEventListener("click",function(b){if(e.value){if(b=k.input?a.getInputInfo(k.slot):a.getOutputInfo(k.slot))b.label=e.value;c.setDirty(!0)}m.close()})}},node:a},e=null;a&&(e=a.getSlotInPosition(b.canvasX,b.canvasY),d.active_node=a);e?(f=[],f.push(e.locked?"Cannot remove":{content:"Remove Slot",slot:e}),f.push({content:"Rename Slot",slot:e}),k.title=(e.input?e.input.type:e.output.type)||"*",e.input&&e.input.type==l.ACTION&&(k.title="Action"),e.output&&e.output.type==l.EVENT&& +(k.title="Event")):a?f=this.getNodeMenuOptions(a):(f=this.graph.getGroupOnPos(b.canvasX,b.canvasY))?(k.node=f,f=this.getGroupMenuOptions(f)):f=this.getCanvasMenuOptions();f&&new l.ContextMenu(f,k,m)};this.CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.roundRect=function(a,b,c,d,f,k){void 0===f&&(f=5);void 0===k&&(k=f);this.moveTo(a+f,b);this.lineTo(a+c-f,b);this.quadraticCurveTo(a+c,b,a+c,b+f);this.lineTo(a+c,b+d-k);this.quadraticCurveTo(a+c,b+d,a+c-k,b+d);this.lineTo(a+k,b+d);this.quadraticCurveTo(a, +b+d,a,b+d-k);this.lineTo(a,b+f);this.quadraticCurveTo(a,b,a+f,b)});l.compareObjects=function(a,b){for(var c in a)if(a[c]!=b[c])return!1;return!0};l.distance=r;l.colorToString=function(a){return"rgba("+Math.round(255*a[0]).toFixed()+","+Math.round(255*a[1]).toFixed()+","+Math.round(255*a[2]).toFixed()+","+(4==a.length?a[3].toFixed(2):"1.0")+")"};l.isInsideRectangle=q;l.growBounding=function(a,b,c){ba[2]&&(a[2]=b);ca[3]&&(a[3]=c)};l.isInsideBounding=function(a,b){return a[0]< +b[0][0]||a[1]b[1][0]||a[1]>b[1][1]?!1:!0};l.overlapBounding=t;l.hex2num=function(a){"#"==a.charAt(0)&&(a=a.slice(1));a=a.toUpperCase();for(var b=Array(3),c=0,d,f,k=0;6>k;k+=2)d="0123456789ABCDEF".indexOf(a.charAt(k)),f="0123456789ABCDEF".indexOf(a.charAt(k+1)),b[c]=16*d+f,c++;return b};l.num2hex=function(a){for(var b="#",c,d,f=0;3>f;f++)c=a[f]/16,d=a[f]%16,b+="0123456789ABCDEF".charAt(c)+"0123456789ABCDEF".charAt(d);return b};u.prototype.addItem=function(a,b,c){function d(a){var b= +this.value;b&&b.has_submenu&&f.call(this,a)}function f(a){var b=this.value,f=!0;k.current_submenu&&k.current_submenu.close(a);if(c.callback){var d=c.callback.call(this,b,c,a,k,c.node);!0===d&&(f=!1)}if(b&&(b.callback&&!c.ignore_item_callbacks&&!0!==b.disabled&&(d=b.callback.call(this,b,c,a,k,c.node),!0===d&&(f=!1)),b.submenu)){if(!b.submenu.options)throw"ContextMenu submenu needs options";new k.constructor(b.submenu.options,{callback:b.submenu.callback,event:a,parentMenu:k,ignore_item_callbacks:b.submenu.ignore_item_callbacks, +title:b.submenu.title,autoopen:c.autoopen});f=!1}f&&!k.lock&&k.close()}var k=this;c=c||{};var m=document.createElement("div");m.className="litemenu-entry submenu";var e=!1;if(null===b)m.classList.add("separator");else{m.innerHTML=b&&b.title?b.title:a;if(m.value=b)b.disabled&&(e=!0,m.classList.add("disabled")),(b.submenu||b.has_submenu)&&m.classList.add("has_submenu");"function"==typeof b?(m.dataset.value=a,m.onclick_callback=b):m.dataset.value=b;b.className&&(m.className+=" "+b.className)}this.root.appendChild(m); +e||m.addEventListener("click",f);c.autoopen&&m.addEventListener("mouseenter",d);return m};u.prototype.close=function(a,b){this.root.parentNode&&this.root.parentNode.removeChild(this.root);this.parentMenu&&!b&&(this.parentMenu.lock=!1,this.parentMenu.current_submenu=null,void 0===a?this.parentMenu.close():a&&!u.isCursorOverElement(a,this.parentMenu.root)&&u.trigger(this.parentMenu.root,"mouseleave",a));this.current_submenu&&this.current_submenu.close(a,!0)};u.trigger=function(a,b,c,d){var f=document.createEvent("CustomEvent"); +f.initCustomEvent(b,!0,!0,c);f.srcElement=d;a.dispatchEvent?a.dispatchEvent(f):a.__events&&a.__events.dispatchEvent(f);return f};u.prototype.getTopMenu=function(){return this.options.parentMenu?this.options.parentMenu.getTopMenu():this};u.prototype.getFirstEvent=function(){return this.options.parentMenu?this.options.parentMenu.getFirstEvent():this.options.event};u.isCursorOverElement=function(a,b){var c=a.pageX,d=a.pageY,f=b.getBoundingClientRect();return f?d>f.top&&df.left&&c< +f.left+f.width?!0:!1:!1};l.ContextMenu=u;l.closeAllContextMenus=function(a){a=a||window;a=a.document.querySelectorAll(".litecontextmenu");if(a.length){for(var b=[],c=0;ca?b:ca[1]))return this.old_y=b.canvasY,this.captureInput(!0),this.mouse_captured=!0};m.prototype.onMouseMove=function(b){if(this.mouse_captured){var a=this.old_y-b.canvasY;b.shiftKey&&(a*=10);if(b.metaKey||b.altKey)a*=0.1;this.old_y=b.canvasY;b=this._remainder+a/m.pixels_threshold;this._remainder=b%1;b=Math.clamp(this.properties.value+(b|0)*this.properties.step,this.properties.min,this.properties.max);this.properties.value=b;this.graph._version++; -this.setDirtyCanvas(!0)}};m.prototype.onMouseUp=function(b,a){200>b.click_time&&(this.properties.value=Math.clamp(this.properties.value+(a[1]>0.5*this.size[1]?-1:1)*this.properties.step,this.properties.min,this.properties.max),this.graph._version++,this.setDirtyCanvas(!0));this.mouse_captured&&(this.mouse_captured=!1,this.captureInput(!1))};n.registerNodeType("widget/number",m);d.title="Knob";d.desc="Circular controller";d.widgets=[{name:"increase",text:"+",type:"minibutton"},{name:"decrease",text:"-", -type:"minibutton"}];d.prototype.onAdded=function(){this.value=(this.properties.value-this.properties.min)/(this.properties.max-this.properties.min);this.imgbg=this.loadImage("imgs/knob_bg.png");this.imgfg=this.loadImage("imgs/knob_fg.png")};d.prototype.onDrawImageKnob=function(b){if(this.imgfg&&this.imgfg.width){var a=0.5*this.imgbg.width,c=this.size[0]/this.imgfg.width;b.save();b.translate(0,20);b.scale(c,c);b.drawImage(this.imgbg,0,0);b.translate(a,a);b.rotate(2*this.value*Math.PI*6/8+10*Math.PI/ -8);b.translate(-a,-a);b.drawImage(this.imgfg,0,0);b.restore();this.title&&(b.font="bold 16px Criticized,Tahoma",b.fillStyle="rgba(100,100,100,0.8)",b.textAlign="center",b.fillText(this.title.toUpperCase(),0.5*this.size[0],18),b.textAlign="left")}};d.prototype.onDrawVectorKnob=function(b){if(this.imgfg&&this.imgfg.width){b.lineWidth=1;b.strokeStyle=this.mouseOver?"#FFF":"#AAA";b.fillStyle="#000";b.beginPath();b.arc(0.5*this.size[0],0.5*this.size[1]+10,0.5*this.properties.size,0,2*Math.PI,!0);b.stroke(); -0b.canvasY-this.pos[1]||n.distance([b.canvasX,b.canvasY],[this.pos[0]+this.center[0],this.pos[1]+this.center[1]])>this.radius)return!1;this.oldmouse=[b.canvasX-this.pos[0],b.canvasY-this.pos[1]];this.captureInput(!0); -return!0}};d.prototype.onMouseMove=function(b){if(this.oldmouse){b=[b.canvasX-this.pos[0],b.canvasY-this.pos[1]];var a=this.value,a=a-0.01*(b[1]-this.oldmouse[1]);1a&&(a=0);this.value=a;this.properties.value=this.properties.min+(this.properties.max-this.properties.min)*this.value;this.oldmouse=b;this.setDirtyCanvas(!0)}};d.prototype.onMouseUp=function(b){this.oldmouse&&(this.oldmouse=null,this.captureInput(!1))};d.prototype.onMouseLeave=function(b){};d.prototype.onWidget=function(b,a){if("increase"== -a.name)this.onPropertyChanged("size",this.properties.size+10);else if("decrease"==a.name)this.onPropertyChanged("size",this.properties.size-10)};d.prototype.onPropertyChanged=function(b,a){if("wcolor"==b)this.properties[b]=a;else if("size"==b)a=parseInt(a),this.properties[b]=a,this.size=[a+4,a+24],this.setDirtyCanvas(!0,!0);else if("min"==b||"max"==b||"value"==b)this.properties[b]=parseFloat(a);else return!1;return!0};n.registerNodeType("widget/knob",d);r.title="Internal Slider";r.prototype.onPropertyChanged= -function(b,a){"value"==b&&(this.slider.value=a)};r.prototype.onExecute=function(){this.setOutputData(0,this.properties.value)};n.registerNodeType("widget/internal_slider",r);q.title="H.Slider";q.desc="Linear slider controller";q.prototype.onAdded=function(){this.value=0.5;this.imgfg=this.loadImage("imgs/slider_fg.png")};q.prototype.onDrawVectorial=function(b){this.imgfg&&this.imgfg.width&&(b.lineWidth=1,b.strokeStyle=this.mouseOver?"#FFF":"#AAA",b.fillStyle="#000",b.beginPath(),b.rect(2,0,this.size[0]- -4,20),b.stroke(),b.fillStyle=this.properties.wcolor,b.beginPath(),b.rect(2+(this.size[0]-4-20)*this.value,0,20,20),b.fill())};q.prototype.onDrawImage=function(b){this.imgfg&&this.imgfg.width&&(b.lineWidth=1,b.fillStyle="#000",b.fillRect(2,9,this.size[0]-4,2),b.strokeStyle="#333",b.beginPath(),b.moveTo(2,9),b.lineTo(this.size[0]-4,9),b.stroke(),b.strokeStyle="#AAA",b.beginPath(),b.moveTo(2,11),b.lineTo(this.size[0]-4,11),b.stroke(),b.drawImage(this.imgfg,2+(this.size[0]-4)*this.value-0.5*this.imgfg.width, -0.5*-this.imgfg.height+10))};q.prototype.onDrawForeground=function(b){this.onDrawImage(b)};q.prototype.onExecute=function(){this.properties.value=this.properties.min+(this.properties.max-this.properties.min)*this.value;this.setOutputData(0,this.properties.value);this.boxcolor=n.colorToString([this.value,this.value,this.value])};q.prototype.onMouseDown=function(b){if(0>b.canvasY-this.pos[1])return!1;this.oldmouse=[b.canvasX-this.pos[0],b.canvasY-this.pos[1]];this.captureInput(!0);return!0};q.prototype.onMouseMove= -function(b){if(this.oldmouse){b=[b.canvasX-this.pos[0],b.canvasY-this.pos[1]];var a=this.value,a=a+(b[0]-this.oldmouse[0])/this.size[0];1a&&(a=0);this.value=a;this.oldmouse=b;this.setDirtyCanvas(!0)}};q.prototype.onMouseUp=function(b){this.oldmouse=null;this.captureInput(!1)};q.prototype.onMouseLeave=function(b){};q.prototype.onPropertyChanged=function(b,a){if("wcolor"==b)this.properties[b]=a;else return!1;return!0};n.registerNodeType("widget/hslider",q);s.title="Progress";s.desc="Shows data in linear progress"; -s.prototype.onExecute=function(){var b=this.getInputData(0);void 0!=b&&(this.properties.value=b)};s.prototype.onDrawForeground=function(b){b.lineWidth=1;b.fillStyle=this.properties.wcolor;var a=(this.properties.value-this.properties.min)/(this.properties.max-this.properties.min),a=Math.min(1,a),a=Math.max(0,a);b.fillRect(2,2,(this.size[0]-4)*a,this.size[1]-4)};n.registerNodeType("widget/progress",s);u.title="Text";u.desc="Shows the input value";u.widgets=[{name:"resize",text:"Resize box",type:"button"}, -{name:"led_text",text:"LED",type:"minibutton"},{name:"normal_text",text:"Normal",type:"minibutton"}];u.prototype.onDrawForeground=function(b){b.fillStyle=this.properties.color;var a=this.properties.value;this.properties.glowSize?(b.shadowColor=this.properties.color,b.shadowOffsetX=0,b.shadowOffsetY=0,b.shadowBlur=this.properties.glowSize):b.shadowColor="transparent";var c=this.properties.fontsize;b.textAlign=this.properties.align;b.font=c.toString()+"px "+this.properties.font;this.str="number"==typeof a? -a.toFixed(this.properties.decimals):a;if("string"==typeof this.str){var a=this.str.split("\\n"),d;for(d in a)b.fillText(a[d],"left"==this.properties.align?15:this.size[0]-15,-0.15*c+c*(parseInt(d)+1))}b.shadowColor="transparent";this.last_ctx=b;b.textAlign="left"};u.prototype.onExecute=function(){var b=this.getInputData(0);null!=b&&(this.properties.value=b)};u.prototype.resize=function(){if(this.last_ctx){var b=this.str.split("\\n");this.last_ctx.font=this.properties.fontsize+"px "+this.properties.font; -var a=0,c;for(c in b){var d=this.last_ctx.measureText(b[c]).width;aa[1]))return this.old_y=c.canvasY,this.captureInput(!0),this.mouse_captured=!0};n.prototype.onMouseMove=function(c){if(this.mouse_captured){var a=this.old_y-c.canvasY;c.shiftKey&&(a*=10);if(c.metaKey||c.altKey)a*=0.1;this.old_y=c.canvasY;c=this._remainder+a/n.pixels_threshold;this._remainder=c%1;c=Math.clamp(this.properties.value+(c|0)*this.properties.step,this.properties.min,this.properties.max);this.properties.value=c;this.graph._version++; +this.setDirtyCanvas(!0)}};n.prototype.onMouseUp=function(c,a){200>c.click_time&&(this.properties.value=Math.clamp(this.properties.value+(a[1]>0.5*this.size[1]?-1:1)*this.properties.step,this.properties.min,this.properties.max),this.graph._version++,this.setDirtyCanvas(!0));this.mouse_captured&&(this.mouse_captured=!1,this.captureInput(!1))};m.registerNodeType("widget/number",n);d.title="Knob";d.desc="Circular controller";d.widgets=[{name:"increase",text:"+",type:"minibutton"},{name:"decrease",text:"-", +type:"minibutton"}];d.prototype.onAdded=function(){this.value=(this.properties.value-this.properties.min)/(this.properties.max-this.properties.min);this.imgbg=this.loadImage("imgs/knob_bg.png");this.imgfg=this.loadImage("imgs/knob_fg.png")};d.prototype.onDrawImageKnob=function(c){if(this.imgfg&&this.imgfg.width){var a=0.5*this.imgbg.width,b=this.size[0]/this.imgfg.width;c.save();c.translate(0,20);c.scale(b,b);c.drawImage(this.imgbg,0,0);c.translate(a,a);c.rotate(2*this.value*Math.PI*6/8+10*Math.PI/ +8);c.translate(-a,-a);c.drawImage(this.imgfg,0,0);c.restore();this.title&&(c.font="bold 16px Criticized,Tahoma",c.fillStyle="rgba(100,100,100,0.8)",c.textAlign="center",c.fillText(this.title.toUpperCase(),0.5*this.size[0],18),c.textAlign="left")}};d.prototype.onDrawVectorKnob=function(c){if(this.imgfg&&this.imgfg.width){c.lineWidth=1;c.strokeStyle=this.mouseOver?"#FFF":"#AAA";c.fillStyle="#000";c.beginPath();c.arc(0.5*this.size[0],0.5*this.size[1]+10,0.5*this.properties.size,0,2*Math.PI,!0);c.stroke(); +0c.canvasY-this.pos[1]||m.distance([c.canvasX,c.canvasY],[this.pos[0]+this.center[0],this.pos[1]+this.center[1]])>this.radius)return!1;this.oldmouse=[c.canvasX-this.pos[0],c.canvasY-this.pos[1]];this.captureInput(!0); +return!0}};d.prototype.onMouseMove=function(c){if(this.oldmouse){c=[c.canvasX-this.pos[0],c.canvasY-this.pos[1]];var a=this.value,a=a-0.01*(c[1]-this.oldmouse[1]);1a&&(a=0);this.value=a;this.properties.value=this.properties.min+(this.properties.max-this.properties.min)*this.value;this.oldmouse=c;this.setDirtyCanvas(!0)}};d.prototype.onMouseUp=function(c){this.oldmouse&&(this.oldmouse=null,this.captureInput(!1))};d.prototype.onMouseLeave=function(c){};d.prototype.onWidget=function(c,a){if("increase"== +a.name)this.onPropertyChanged("size",this.properties.size+10);else if("decrease"==a.name)this.onPropertyChanged("size",this.properties.size-10)};d.prototype.onPropertyChanged=function(c,a){if("wcolor"==c)this.properties[c]=a;else if("size"==c)a=parseInt(a),this.properties[c]=a,this.size=[a+4,a+24],this.setDirtyCanvas(!0,!0);else if("min"==c||"max"==c||"value"==c)this.properties[c]=parseFloat(a);else return!1;return!0};m.registerNodeType("widget/knob",d);r.title="Internal Slider";r.prototype.onPropertyChanged= +function(c,a){"value"==c&&(this.slider.value=a)};r.prototype.onExecute=function(){this.setOutputData(0,this.properties.value)};m.registerNodeType("widget/internal_slider",r);q.title="H.Slider";q.desc="Linear slider controller";q.prototype.onAdded=function(){this.value=0.5;this.imgfg=this.loadImage("imgs/slider_fg.png")};q.prototype.onDrawVectorial=function(c){this.imgfg&&this.imgfg.width&&(c.lineWidth=1,c.strokeStyle=this.mouseOver?"#FFF":"#AAA",c.fillStyle="#000",c.beginPath(),c.rect(2,0,this.size[0]- +4,20),c.stroke(),c.fillStyle=this.properties.wcolor,c.beginPath(),c.rect(2+(this.size[0]-4-20)*this.value,0,20,20),c.fill())};q.prototype.onDrawImage=function(c){this.imgfg&&this.imgfg.width&&(c.lineWidth=1,c.fillStyle="#000",c.fillRect(2,9,this.size[0]-4,2),c.strokeStyle="#333",c.beginPath(),c.moveTo(2,9),c.lineTo(this.size[0]-4,9),c.stroke(),c.strokeStyle="#AAA",c.beginPath(),c.moveTo(2,11),c.lineTo(this.size[0]-4,11),c.stroke(),c.drawImage(this.imgfg,2+(this.size[0]-4)*this.value-0.5*this.imgfg.width, +0.5*-this.imgfg.height+10))};q.prototype.onDrawForeground=function(c){this.onDrawImage(c)};q.prototype.onExecute=function(){this.properties.value=this.properties.min+(this.properties.max-this.properties.min)*this.value;this.setOutputData(0,this.properties.value);this.boxcolor=m.colorToString([this.value,this.value,this.value])};q.prototype.onMouseDown=function(c){if(0>c.canvasY-this.pos[1])return!1;this.oldmouse=[c.canvasX-this.pos[0],c.canvasY-this.pos[1]];this.captureInput(!0);return!0};q.prototype.onMouseMove= +function(c){if(this.oldmouse){c=[c.canvasX-this.pos[0],c.canvasY-this.pos[1]];var a=this.value,a=a+(c[0]-this.oldmouse[0])/this.size[0];1a&&(a=0);this.value=a;this.oldmouse=c;this.setDirtyCanvas(!0)}};q.prototype.onMouseUp=function(c){this.oldmouse=null;this.captureInput(!1)};q.prototype.onMouseLeave=function(c){};q.prototype.onPropertyChanged=function(c,a){if("wcolor"==c)this.properties[c]=a;else return!1;return!0};m.registerNodeType("widget/hslider",q);t.title="Progress";t.desc="Shows data in linear progress"; +t.prototype.onExecute=function(){var c=this.getInputData(0);void 0!=c&&(this.properties.value=c)};t.prototype.onDrawForeground=function(c){c.lineWidth=1;c.fillStyle=this.properties.wcolor;var a=(this.properties.value-this.properties.min)/(this.properties.max-this.properties.min),a=Math.min(1,a),a=Math.max(0,a);c.fillRect(2,2,(this.size[0]-4)*a,this.size[1]-4)};m.registerNodeType("widget/progress",t);u.title="Text";u.desc="Shows the input value";u.widgets=[{name:"resize",text:"Resize box",type:"button"}, +{name:"led_text",text:"LED",type:"minibutton"},{name:"normal_text",text:"Normal",type:"minibutton"}];u.prototype.onDrawForeground=function(c){c.fillStyle=this.properties.color;var a=this.properties.value;this.properties.glowSize?(c.shadowColor=this.properties.color,c.shadowOffsetX=0,c.shadowOffsetY=0,c.shadowBlur=this.properties.glowSize):c.shadowColor="transparent";var b=this.properties.fontsize;c.textAlign=this.properties.align;c.font=b.toString()+"px "+this.properties.font;this.str="number"==typeof a? +a.toFixed(this.properties.decimals):a;if("string"==typeof this.str){var a=this.str.split("\\n"),d;for(d in a)c.fillText(a[d],"left"==this.properties.align?15:this.size[0]-15,-0.15*b+b*(parseInt(d)+1))}c.shadowColor="transparent";this.last_ctx=c;c.textAlign="left"};u.prototype.onExecute=function(){var c=this.getInputData(0);null!=c&&(this.properties.value=c)};u.prototype.resize=function(){if(this.last_ctx){var c=this.str.split("\\n");this.last_ctx.font=this.properties.fontsize+"px "+this.properties.font; +var a=0,b;for(b in c){var d=this.last_ctx.measureText(c[b]).width;ad?e.xbox.axes.lx:0,this._left_axis[1]=Math.abs(e.xbox.axes.ly)>d?e.xbox.axes.ly:0,this._right_axis[0]=Math.abs(e.xbox.axes.rx)>d?e.xbox.axes.rx:0,this._right_axis[1]=Math.abs(e.xbox.axes.ry)>d?e.xbox.axes.ry:0,this._triggers[0]=Math.abs(e.xbox.axes.ltrigger)>d?e.xbox.axes.ltrigger:0,this._triggers[1]=Math.abs(e.xbox.axes.rtrigger)>d?e.xbox.axes.rtrigger:0);if(this.outputs)for(d= 0;d","string",{values:f.values});this.size=[60,40]}function k(){this.addInput("inc","number");this.addOutput("total","number");this.addProperty("increment",1);this.addProperty("value",0)}function p(){this.addInput("v","number");this.addOutput("sin","number");this.addProperty("amplitude",1);this.addProperty("offset",0);this.bgImageUrl="nodes/imgs/icon-sin.png"}function C(){this.addInput("vec2", -"vec2");this.addOutput("x","number");this.addOutput("y","number")}function x(){this.addInputs([["x","number"],["y","number"]]);this.addOutput("vec2","vec2");this.properties={x:0,y:0};this._data=new Float32Array(2)}function z(){this.addInput("vec3","vec3");this.addOutput("x","number");this.addOutput("y","number");this.addOutput("z","number")}function A(){this.addInputs([["x","number"],["y","number"],["z","number"]]);this.addOutput("vec3","vec3");this.properties={x:0,y:0,z:0};this._data=new Float32Array(3)} -function D(){this.addInput("vec4","vec4");this.addOutput("x","number");this.addOutput("y","number");this.addOutput("z","number");this.addOutput("w","number")}function B(){this.addInputs([["x","number"],["y","number"],["z","number"],["w","number"]]);this.addOutput("vec4","vec4");this.properties={x:0,y:0,z:0,w:0};this._data=new Float32Array(4)}var v=t.LiteGraph;h.title="Converter";h.desc="type A to type B";h.prototype.onExecute=function(){var a=this.getInputData(0);if(null!=a&&this.outputs)for(var c= -0;cb&&(this._current=0);for(var c=a=0;cb&&(b=1);this.properties.samples=Math.round(b);var c=this._values;this._values=new Float32Array(this.properties.samples);c.length<=this._values.length?this._values.set(c):this._values.set(c.subarray(0,this._values.length))};v.registerNodeType("math/average",a);c.title="TendTo";c.desc="moves the output value always closer to the input";c.prototype.onExecute=function(){var a=this.getInputData(0);null==a&&(a=0);var b=this.properties.factor;this._value=null==this._value?a:this._value* -(1-b)+a*b;this.setOutputData(0,this._value)};v.registerNodeType("math/tendTo",c);g.values="+-*/%^".split("");g.title="Operation";g.desc="Easy math operators";g["@OP"]={type:"enum",title:"operation",values:g.values};g.prototype.setValue=function(a){"string"==typeof a&&(a=parseFloat(a));this.properties.value=a};g.prototype.onExecute=function(){var a=this.getInputData(0),b=this.getInputData(1);null!=a?this.properties.A=a:a=this.properties.A;null!=b?this.properties.B=b:b=this.properties.B;var c=0;switch(this.properties.OP){case "+":c= -a+b;break;case "-":c=a-b;break;case "x":case "X":case "*":c=a*b;break;case "/":c=a/b;break;case "%":c=a%b;break;case "^":c=Math.pow(a,b);break;default:console.warn("Unknown operation: "+this.properties.OP)}this.setOutputData(0,c)};g.prototype.onDrawBackground=function(a){this.flags.collapsed||(a.font="40px Arial",a.fillStyle="black",a.textAlign="center",a.fillText(this.properties.OP,0.5*this.size[0],0.5*this.size[1]+v.NODE_TITLE_HEIGHT),a.textAlign="left")};v.registerNodeType("math/operation",g); -w.title="Compare";w.desc="compares between two values";w.prototype.onExecute=function(){var a=this.getInputData(0),b=this.getInputData(1);void 0!==a?this.properties.A=a:a=this.properties.A;void 0!==b?this.properties.B=b:b=this.properties.B;for(var c=0,d=this.outputs.length;cB":value=a>b;break;case "A=B":value= -a>=b}this.setOutputData(c,value)}}};w.prototype.onGetOutputs=function(){return[["A==B","boolean"],["A!=B","boolean"],["A>B","boolean"],["A=B","boolean"],["A<=B","boolean"]]};v.registerNodeType("math/compare",w);f.values="> < == != <= >=".split(" ");f["@OP"]={type:"enum",title:"operation",values:f.values};f.title="Condition";f.desc="evaluates condition between A and B";f.prototype.onExecute=function(){var a=this.getInputData(0);void 0===a?a=this.properties.A:this.properties.A=a; -var b=this.getInputData(1);void 0===b?b=this.properties.B:this.properties.B=b;var c=!0;switch(this.properties.OP){case ">":c=a>b;break;case "<":c=a=":c=a>=b}this.setOutputData(0,c)};v.registerNodeType("math/condition",f);k.title="Accumulate";k.desc="Increments a value every time";k.prototype.onExecute=function(){null===this.properties.value&&(this.properties.value=0);var a=this.getInputData(0);this.properties.value= -null!==a?this.properties.value+a:this.properties.value+this.properties.increment;this.setOutputData(0,this.properties.value)};v.registerNodeType("math/accumulate",k);p.title="Trigonometry";p.desc="Sin Cos Tan";p.filter="shader";p.prototype.onExecute=function(){var a=this.getInputData(0);null==a&&(a=0);var b=this.properties.amplitude,c=this.findInputSlot("amplitude");-1!=c&&(b=this.getInputData(c));var d=this.properties.offset,c=this.findInputSlot("offset");-1!=c&&(d=this.getInputData(c));for(var c= -0,e=this.outputs.length;cXY";C.desc="vector 2 to components";C.prototype.onExecute=function(){var a=this.getInputData(0);null!=a&&(this.setOutputData(0,a[0]),this.setOutputData(1,a[1]))};v.registerNodeType("math3d/vec2-to-xyz",C);x.title= -"XY->Vec2";x.desc="components to vector2";x.prototype.onExecute=function(){var a=this.getInputData(0);null==a&&(a=this.properties.x);var b=this.getInputData(1);null==b&&(b=this.properties.y);var c=this._data;c[0]=a;c[1]=b;this.setOutputData(0,c)};v.registerNodeType("math3d/xy-to-vec2",x);z.title="Vec3->XYZ";z.desc="vector 3 to components";z.prototype.onExecute=function(){var a=this.getInputData(0);null!=a&&(this.setOutputData(0,a[0]),this.setOutputData(1,a[1]),this.setOutputData(2,a[2]))};v.registerNodeType("math3d/vec3-to-xyz", -z);A.title="XYZ->Vec3";A.desc="components to vector3";A.prototype.onExecute=function(){var a=this.getInputData(0);null==a&&(a=this.properties.x);var b=this.getInputData(1);null==b&&(b=this.properties.y);var c=this.getInputData(2);null==c&&(c=this.properties.z);var d=this._data;d[0]=a;d[1]=b;d[2]=c;this.setOutputData(0,d)};v.registerNodeType("math3d/xyz-to-vec3",A);D.title="Vec4->XYZW";D.desc="vector 4 to components";D.prototype.onExecute=function(){var a=this.getInputData(0);null!=a&&(this.setOutputData(0, -a[0]),this.setOutputData(1,a[1]),this.setOutputData(2,a[2]),this.setOutputData(3,a[3]))};v.registerNodeType("math3d/vec4-to-xyzw",D);B.title="XYZW->Vec4";B.desc="components to vector4";B.prototype.onExecute=function(){var a=this.getInputData(0);null==a&&(a=this.properties.x);var b=this.getInputData(1);null==b&&(b=this.properties.y);var c=this.getInputData(2);null==c&&(c=this.properties.z);var d=this.getInputData(3);null==d&&(d=this.properties.w);var e=this._data;e[0]=a;e[1]=b;e[2]=c;e[3]=d;this.setOutputData(0, -e)};v.registerNodeType("math3d/xyzw-to-vec4",B);t.glMatrix&&(t=function(){this.addOutput("quat","quat");this.properties={x:0,y:0,z:0,w:1};this._value=quat.create()},t.title="Quaternion",t.desc="quaternion",t.prototype.onExecute=function(){this._value[0]=this.properties.x;this._value[1]=this.properties.y;this._value[2]=this.properties.z;this._value[3]=this.properties.w;this.setOutputData(0,this._value)},v.registerNodeType("math3d/quaternion",t),t=function(){this.addInputs([["degrees","number"],["axis", -"vec3"]]);this.addOutput("quat","quat");this.properties={angle:90,axis:vec3.fromValues(0,1,0)};this._value=quat.create()},t.title="Rotation",t.desc="quaternion rotation",t.prototype.onExecute=function(){var a=this.getInputData(0);null==a&&(a=this.properties.angle);var b=this.getInputData(1);null==b&&(b=this.properties.axis);a=quat.setAxisAngle(this._value,b,0.0174532925*a);this.setOutputData(0,a)},v.registerNodeType("math3d/rotation",t),t=function(){this.addInputs([["vec3","vec3"],["quat","quat"]]); -this.addOutput("result","vec3");this.properties={vec:[0,0,1]}},t.title="Rot. Vec3",t.desc="rotate a point",t.prototype.onExecute=function(){var a=this.getInputData(0);null==a&&(a=this.properties.vec);var b=this.getInputData(1);null==b?this.setOutputData(a):this.setOutputData(0,vec3.transformQuat(vec3.create(),a,b))},v.registerNodeType("math3d/rotate_vec3",t),t=function(){this.addInputs([["A","quat"],["B","quat"]]);this.addOutput("A*B","quat");this._value=quat.create()},t.title="Mult. Quat",t.desc= -"rotate quaternion",t.prototype.onExecute=function(){var a=this.getInputData(0);if(null!=a){var b=this.getInputData(1);null!=b&&(a=quat.multiply(this._value,a,b),this.setOutputData(0,a))}},v.registerNodeType("math3d/mult-quat",t),t=function(){this.addInputs([["A","quat"],["B","quat"],["factor","number"]]);this.addOutput("slerp","quat");this.addProperty("factor",0.5);this._value=quat.create()},t.title="Quat Slerp",t.desc="quaternion spherical interpolation",t.prototype.onExecute=function(){var a=this.getInputData(0); -if(null!=a){var b=this.getInputData(1);if(null!=b){var c=this.properties.factor;null!=this.getInputData(2)&&(c=this.getInputData(2));a=quat.slerp(this._value,a,b,c);this.setOutputData(0,a)}}},v.registerNodeType("math3d/quat-slerp",t))})(this); -(function(t){function h(){this.addInput("sel","boolean");this.addOutput("value","number");this.properties={A:0,B:1};this.size=[60,20]}t=t.LiteGraph;h.title="Selector";h.desc="outputs A if selector is true, B if selector is false";h.prototype.onExecute=function(){var e=this.getInputData(0);if(void 0!==e){for(var h=1;hb;++b){var a=this.getInputData(b);if(null!=a){var c=this.values[b];c.push(a);c.length>d[0]&&c.shift()}}}};h.prototype.onDrawBackground=function(d){if(!this.flags.collapsed){var b=this.size,a=0.5*b[1]/this.properties.scale,c=h.colors,e=0.5*b[1];d.fillStyle="#000";d.fillRect(0,0,b[0],b[1]);d.strokeStyle="#555";d.beginPath();d.moveTo(0,e);d.lineTo(b[0], -e);d.stroke();for(var l=0;4>l;++l){var f=this.values[l];d.strokeStyle=c[l];d.beginPath();var k=f[0]*a*-1+e;d.moveTo(0,Math.clamp(k,0,b[1]));for(var p=1;pb&&(b=0);if(0!=d.length){var a=[0,0,0];if(0==b)a=d[0];else if(1==b)a=d[d.length-1];else{var c=(d.length-1)*b,b=d[Math.floor(c)],d=d[Math.floor(c)+1],c=c-Math.floor(c);a[0]=b[0]*(1-c)+d[0]*c;a[1]=b[1]*(1-c)+d[1]*c;a[2]=b[2]*(1-c)+d[2]*c}for(var e in a)a[e]/=255;this.boxcolor=colorToString(a);this.setOutputData(0,a)}};l.registerNodeType("color/palette",m);d.title="Frame";d.desc="Frame viewerew";d.widgets=[{name:"resize",text:"Resize box",type:"button"}, -{name:"view",text:"View Image",type:"button"}];d.prototype.onDrawBackground=function(d){this.frame&&d.drawImage(this.frame,0,0,this.size[0],this.size[1])};d.prototype.onExecute=function(){this.frame=this.getInputData(0);this.setDirtyCanvas(!0)};d.prototype.onWidget=function(d,b){if("resize"==b.name&&this.frame){var a=this.frame.width,c=this.frame.height;a||null==this.frame.videoWidth||(a=this.frame.videoWidth,c=this.frame.videoHeight);a&&c&&(this.size=[a,c]);this.setDirtyCanvas(!0,!0)}else"view"== -b.name&&this.show()};d.prototype.show=function(){showElement&&this.frame&&showElement(this.frame)};l.registerNodeType("graphics/frame",d);r.title="Image fade";r.desc="Fades between images";r.widgets=[{name:"resizeA",text:"Resize to A",type:"button"},{name:"resizeB",text:"Resize to B",type:"button"}];r.prototype.onAdded=function(){this.createCanvas();var d=this.canvas.getContext("2d");d.fillStyle="#000";d.fillRect(0,0,this.properties.width,this.properties.height)};r.prototype.createCanvas=function(){this.canvas= -document.createElement("canvas");this.canvas.width=this.properties.width;this.canvas.height=this.properties.height};r.prototype.onExecute=function(){var d=this.canvas.getContext("2d");this.canvas.width=this.canvas.width;var b=this.getInputData(0);null!=b&&d.drawImage(b,0,0,this.canvas.width,this.canvas.height);b=this.getInputData(2);null==b?b=this.properties.fade:this.properties.fade=b;d.globalAlpha=b;b=this.getInputData(1);null!=b&&d.drawImage(b,0,0,this.canvas.width,this.canvas.height);d.globalAlpha= +null==b&&(b=0);var c=this.properties.f,d=this.getInputData(2);void 0!==d&&(c=d);this.setOutputData(0,a*(1-c)+b*c)};q.prototype.onGetInputs=function(){return[["f","number"]]};v.registerNodeType("math/lerp",q);t.title="Abs";t.desc="Absolute";t.prototype.onExecute=function(){var a=this.getInputData(0);null!=a&&this.setOutputData(0,Math.abs(a))};v.registerNodeType("math/abs",t);u.title="Floor";u.desc="Floor number to remove fractional part";u.prototype.onExecute=function(){var a=this.getInputData(0); +null!=a&&this.setOutputData(0,Math.floor(a))};v.registerNodeType("math/floor",u);l.title="Frac";l.desc="Returns fractional part";l.prototype.onExecute=function(){var a=this.getInputData(0);null!=a&&this.setOutputData(0,a%1)};v.registerNodeType("math/frac",l);m.title="Smoothstep";m.desc="Smoothstep";m.prototype.onExecute=function(){var a=this.getInputData(0);if(void 0!==a){var b=this.properties.A,a=Math.clamp((a-b)/(this.properties.B-b),0,1);this.setOutputData(0,a*a*(3-2*a))}};v.registerNodeType("math/smoothstep", +m);c.title="Scale";c.desc="v * factor";c.prototype.onExecute=function(){var a=this.getInputData(0);null!=a&&this.setOutputData(0,a*this.properties.factor)};v.registerNodeType("math/scale",c);a.title="Average";a.desc="Average Filter";a.prototype.onExecute=function(){var a=this.getInputData(0);null==a&&(a=0);var b=this._values.length;this._values[this._current%b]=a;this._current+=1;this._current>b&&(this._current=0);for(var c=a=0;cb&&(b=1);this.properties.samples=Math.round(b);var c=this._values;this._values=new Float32Array(this.properties.samples);c.length<=this._values.length?this._values.set(c):this._values.set(c.subarray(0,this._values.length))};v.registerNodeType("math/average",a);b.title="TendTo";b.desc="moves the output value always closer to the input";b.prototype.onExecute=function(){var a=this.getInputData(0);null==a&&(a=0);var b=this.properties.factor;this._value=null==this._value?a:this._value* +(1-b)+a*b;this.setOutputData(0,this._value)};v.registerNodeType("math/tendTo",b);g.values="+-*/%^".split("");g.title="Operation";g.desc="Easy math operators";g["@OP"]={type:"enum",title:"operation",values:g.values};g.prototype.getTitle=function(){return"A "+this.properties.OP+" B"};g.prototype.setValue=function(a){"string"==typeof a&&(a=parseFloat(a));this.properties.value=a};g.prototype.onExecute=function(){var a=this.getInputData(0),b=this.getInputData(1);null!=a?this.properties.A=a:a=this.properties.A; +null!=b?this.properties.B=b:b=this.properties.B;var c=0;switch(this.properties.OP){case "+":c=a+b;break;case "-":c=a-b;break;case "x":case "X":case "*":c=a*b;break;case "/":c=a/b;break;case "%":c=a%b;break;case "^":c=Math.pow(a,b);break;default:console.warn("Unknown operation: "+this.properties.OP)}this.setOutputData(0,c)};g.prototype.onDrawBackground=function(a){this.flags.collapsed||(a.font="40px Arial",a.fillStyle="#CCC",a.textAlign="center",a.fillText(this.properties.OP,0.5*this.size[0],0.35* +this.size[1]+v.NODE_TITLE_HEIGHT),a.textAlign="left")};v.registerNodeType("math/operation",g);w.title="Compare";w.desc="compares between two values";w.prototype.onExecute=function(){var a=this.getInputData(0),b=this.getInputData(1);void 0!==a?this.properties.A=a:a=this.properties.A;void 0!==b?this.properties.B=b:b=this.properties.B;for(var c=0,d=this.outputs.length;cB":value= +a>b;break;case "A=B":value=a>=b}this.setOutputData(c,value)}}};w.prototype.onGetOutputs=function(){return[["A==B","boolean"],["A!=B","boolean"],["A>B","boolean"],["A=B","boolean"],["A<=B","boolean"]]};v.registerNodeType("math/compare",w);f.values="> < == != <= >=".split(" ");f["@OP"]={type:"enum",title:"operation",values:f.values};f.title="Condition";f.desc="evaluates condition between A and B";f.prototype.onExecute=function(){var a= +this.getInputData(0);void 0===a?a=this.properties.A:this.properties.A=a;var b=this.getInputData(1);void 0===b?b=this.properties.B:this.properties.B=b;var c=!0;switch(this.properties.OP){case ">":c=a>b;break;case "<":c=a=":c=a>=b}this.setOutputData(0,c)};v.registerNodeType("math/condition",f);k.title="Accumulate";k.desc="Increments a value every time";k.prototype.onExecute=function(){null===this.properties.value&& +(this.properties.value=0);var a=this.getInputData(0);this.properties.value=null!==a?this.properties.value+a:this.properties.value+this.properties.increment;this.setOutputData(0,this.properties.value)};v.registerNodeType("math/accumulate",k);p.title="Trigonometry";p.desc="Sin Cos Tan";p.filter="shader";p.prototype.onExecute=function(){var a=this.getInputData(0);null==a&&(a=0);var b=this.properties.amplitude,c=this.findInputSlot("amplitude");-1!=c&&(b=this.getInputData(c));var d=this.properties.offset, +c=this.findInputSlot("offset");-1!=c&&(d=this.getInputData(c));for(var c=0,f=this.outputs.length;cXY";C.desc="vector 2 to components";C.prototype.onExecute=function(){var a=this.getInputData(0);null!=a&&(this.setOutputData(0, +a[0]),this.setOutputData(1,a[1]))};v.registerNodeType("math3d/vec2-to-xyz",C);x.title="XY->Vec2";x.desc="components to vector2";x.prototype.onExecute=function(){var a=this.getInputData(0);null==a&&(a=this.properties.x);var b=this.getInputData(1);null==b&&(b=this.properties.y);var c=this._data;c[0]=a;c[1]=b;this.setOutputData(0,c)};v.registerNodeType("math3d/xy-to-vec2",x);y.title="Vec3->XYZ";y.desc="vector 3 to components";y.prototype.onExecute=function(){var a=this.getInputData(0);null!=a&&(this.setOutputData(0, +a[0]),this.setOutputData(1,a[1]),this.setOutputData(2,a[2]))};v.registerNodeType("math3d/vec3-to-xyz",y);A.title="XYZ->Vec3";A.desc="components to vector3";A.prototype.onExecute=function(){var a=this.getInputData(0);null==a&&(a=this.properties.x);var b=this.getInputData(1);null==b&&(b=this.properties.y);var c=this.getInputData(2);null==c&&(c=this.properties.z);var d=this._data;d[0]=a;d[1]=b;d[2]=c;this.setOutputData(0,d)};v.registerNodeType("math3d/xyz-to-vec3",A);D.title="Vec4->XYZW";D.desc="vector 4 to components"; +D.prototype.onExecute=function(){var a=this.getInputData(0);null!=a&&(this.setOutputData(0,a[0]),this.setOutputData(1,a[1]),this.setOutputData(2,a[2]),this.setOutputData(3,a[3]))};v.registerNodeType("math3d/vec4-to-xyzw",D);B.title="XYZW->Vec4";B.desc="components to vector4";B.prototype.onExecute=function(){var a=this.getInputData(0);null==a&&(a=this.properties.x);var b=this.getInputData(1);null==b&&(b=this.properties.y);var c=this.getInputData(2);null==c&&(c=this.properties.z);var d=this.getInputData(3); +null==d&&(d=this.properties.w);var f=this._data;f[0]=a;f[1]=b;f[2]=c;f[3]=d;this.setOutputData(0,f)};v.registerNodeType("math3d/xyzw-to-vec4",B);s.glMatrix&&(s=function(){this.addOutput("quat","quat");this.properties={x:0,y:0,z:0,w:1};this._value=quat.create()},s.title="Quaternion",s.desc="quaternion",s.prototype.onExecute=function(){this._value[0]=this.properties.x;this._value[1]=this.properties.y;this._value[2]=this.properties.z;this._value[3]=this.properties.w;this.setOutputData(0,this._value)}, +v.registerNodeType("math3d/quaternion",s),s=function(){this.addInputs([["degrees","number"],["axis","vec3"]]);this.addOutput("quat","quat");this.properties={angle:90,axis:vec3.fromValues(0,1,0)};this._value=quat.create()},s.title="Rotation",s.desc="quaternion rotation",s.prototype.onExecute=function(){var a=this.getInputData(0);null==a&&(a=this.properties.angle);var b=this.getInputData(1);null==b&&(b=this.properties.axis);a=quat.setAxisAngle(this._value,b,0.0174532925*a);this.setOutputData(0,a)}, +v.registerNodeType("math3d/rotation",s),s=function(){this.addInputs([["vec3","vec3"],["quat","quat"]]);this.addOutput("result","vec3");this.properties={vec:[0,0,1]}},s.title="Rot. Vec3",s.desc="rotate a point",s.prototype.onExecute=function(){var a=this.getInputData(0);null==a&&(a=this.properties.vec);var b=this.getInputData(1);null==b?this.setOutputData(a):this.setOutputData(0,vec3.transformQuat(vec3.create(),a,b))},v.registerNodeType("math3d/rotate_vec3",s),s=function(){this.addInputs([["A","quat"], +["B","quat"]]);this.addOutput("A*B","quat");this._value=quat.create()},s.title="Mult. Quat",s.desc="rotate quaternion",s.prototype.onExecute=function(){var a=this.getInputData(0);if(null!=a){var b=this.getInputData(1);null!=b&&(a=quat.multiply(this._value,a,b),this.setOutputData(0,a))}},v.registerNodeType("math3d/mult-quat",s),s=function(){this.addInputs([["A","quat"],["B","quat"],["factor","number"]]);this.addOutput("slerp","quat");this.addProperty("factor",0.5);this._value=quat.create()},s.title= +"Quat Slerp",s.desc="quaternion spherical interpolation",s.prototype.onExecute=function(){var a=this.getInputData(0);if(null!=a){var b=this.getInputData(1);if(null!=b){var c=this.properties.factor;null!=this.getInputData(2)&&(c=this.getInputData(2));a=quat.slerp(this._value,a,b,c);this.setOutputData(0,a)}}},v.registerNodeType("math3d/quat-slerp",s))})(this); +(function(s){function h(){this.addInput("sel","boolean");this.addOutput("value","number");this.properties={A:0,B:1};this.size=[60,20]}s=s.LiteGraph;h.title="Selector";h.desc="outputs A if selector is true, B if selector is false";h.prototype.onExecute=function(){var e=this.getInputData(0);if(void 0!==e){for(var h=1;hc;++c){var a=this.getInputData(c);if(null!=a){var b=this.values[c];b.push(a);b.length>d[0]&&b.shift()}}}};h.prototype.onDrawBackground=function(d){if(!this.flags.collapsed){var c=this.size,a=0.5*c[1]/this.properties.scale,b=h.colors,e=0.5*c[1];d.fillStyle="#000";d.fillRect(0,0,c[0],c[1]);d.strokeStyle="#555";d.beginPath();d.moveTo(0,e);d.lineTo(c[0], +e);d.stroke();for(var l=0;4>l;++l){var f=this.values[l];d.strokeStyle=b[l];d.beginPath();var k=f[0]*a*-1+e;d.moveTo(0,Math.clamp(k,0,c[1]));for(var p=1;pc&&(c=0);if(0!=d.length){var a=[0,0,0];if(0==c)a=d[0];else if(1==c)a=d[d.length-1];else{var b=(d.length-1)*c,c=d[Math.floor(b)],d=d[Math.floor(b)+1],b=b-Math.floor(b);a[0]=c[0]*(1-b)+d[0]*b;a[1]=c[1]*(1-b)+d[1]*b;a[2]=c[2]*(1-b)+d[2]*b}for(var e in a)a[e]/=255;this.boxcolor=colorToString(a);this.setOutputData(0,a)}};l.registerNodeType("color/palette",n);d.title="Frame";d.desc="Frame viewerew";d.widgets=[{name:"resize",text:"Resize box",type:"button"}, +{name:"view",text:"View Image",type:"button"}];d.prototype.onDrawBackground=function(d){this.frame&&d.drawImage(this.frame,0,0,this.size[0],this.size[1])};d.prototype.onExecute=function(){this.frame=this.getInputData(0);this.setDirtyCanvas(!0)};d.prototype.onWidget=function(d,c){if("resize"==c.name&&this.frame){var a=this.frame.width,b=this.frame.height;a||null==this.frame.videoWidth||(a=this.frame.videoWidth,b=this.frame.videoHeight);a&&b&&(this.size=[a,b]);this.setDirtyCanvas(!0,!0)}else"view"== +c.name&&this.show()};d.prototype.show=function(){showElement&&this.frame&&showElement(this.frame)};l.registerNodeType("graphics/frame",d);r.title="Image fade";r.desc="Fades between images";r.widgets=[{name:"resizeA",text:"Resize to A",type:"button"},{name:"resizeB",text:"Resize to B",type:"button"}];r.prototype.onAdded=function(){this.createCanvas();var d=this.canvas.getContext("2d");d.fillStyle="#000";d.fillRect(0,0,this.properties.width,this.properties.height)};r.prototype.createCanvas=function(){this.canvas= +document.createElement("canvas");this.canvas.width=this.properties.width;this.canvas.height=this.properties.height};r.prototype.onExecute=function(){var d=this.canvas.getContext("2d");this.canvas.width=this.canvas.width;var c=this.getInputData(0);null!=c&&d.drawImage(c,0,0,this.canvas.width,this.canvas.height);c=this.getInputData(2);null==c?c=this.properties.fade:this.properties.fade=c;d.globalAlpha=c;c=this.getInputData(1);null!=c&&d.drawImage(c,0,0,this.canvas.width,this.canvas.height);d.globalAlpha= 1;this.setOutputData(0,this.canvas);this.setDirtyCanvas(!0)};l.registerNodeType("graphics/imagefade",r);q.title="Crop";q.desc="Crop Image";q.prototype.onAdded=function(){this.createCanvas()};q.prototype.createCanvas=function(){this.canvas=document.createElement("canvas");this.canvas.width=this.properties.width;this.canvas.height=this.properties.height};q.prototype.onExecute=function(){var d=this.getInputData(0);d&&(d.width?(this.canvas.getContext("2d").drawImage(d,-this.properties.x,-this.properties.y, -d.width*this.properties.scale,d.height*this.properties.scale),this.setOutputData(0,this.canvas)):this.setOutputData(0,null))};q.prototype.onDrawBackground=function(d){this.flags.collapsed||this.canvas&&d.drawImage(this.canvas,0,0,this.canvas.width,this.canvas.height,0,0,this.size[0],this.size[1])};q.prototype.onPropertyChanged=function(d,b){this.properties[d]=b;"scale"==d?(this.properties[d]=parseFloat(b),0==this.properties[d]&&(this.trace("Error in scale"),this.properties[d]=1)):this.properties[d]= -parseInt(b);this.createCanvas();return!0};l.registerNodeType("graphics/cropImage",q);s.title="Video";s.desc="Video playback";s.widgets=[{name:"play",text:"PLAY",type:"minibutton"},{name:"stop",text:"STOP",type:"minibutton"},{name:"demo",text:"Demo video",type:"button"},{name:"mute",text:"Mute video",type:"button"}];s.prototype.onExecute=function(){if(this.properties.url&&(this.properties.url!=this._video_url&&this.loadVideo(this.properties.url),this._video&&0!=this._video.width)){var d=this.getInputData(0); -d&&0<=d&&1>=d&&(this._video.currentTime=d*this._video.duration,this._video.pause());this._video.dirty=!0;this.setOutputData(0,this._video);this.setOutputData(1,this._video.currentTime);this.setOutputData(2,this._video.duration);this.setDirtyCanvas(!0)}};s.prototype.onStart=function(){this.play()};s.prototype.onStop=function(){this.stop()};s.prototype.loadVideo=function(d){this._video_url=d;this.properties.use_proxy&&"http"==d.substr(0,4)&&l.proxy&&(d=l.proxy+d.substr(d.indexOf(":")+3));this._video= -document.createElement("video");this._video.src=d;this._video.type="type=video/mp4";this._video.muted=!0;this._video.autoplay=!0;var b=this;this._video.addEventListener("loadedmetadata",function(a){b.trace("Duration: "+this.duration+" seconds");b.trace("Size: "+this.videoWidth+","+this.videoHeight);b.setDirtyCanvas(!0);this.width=this.videoWidth;this.height=this.videoHeight});this._video.addEventListener("progress",function(a){});this._video.addEventListener("error",function(a){console.log("Error loading video: "+ -this.src);b.trace("Error loading video: "+this.src);if(this.error)switch(this.error.code){case this.error.MEDIA_ERR_ABORTED:b.trace("You stopped the video.");break;case this.error.MEDIA_ERR_NETWORK:b.trace("Network error - please try again later.");break;case this.error.MEDIA_ERR_DECODE:b.trace("Video is broken..");break;case this.error.MEDIA_ERR_SRC_NOT_SUPPORTED:b.trace("Sorry, your browser can't play this video.")}});this._video.addEventListener("ended",function(a){b.trace("Ended.");this.play()})}; -s.prototype.onPropertyChanged=function(d,b){this.properties[d]=b;"url"==d&&""!=b&&this.loadVideo(b);return!0};s.prototype.play=function(){this._video&&this._video.play()};s.prototype.playPause=function(){this._video&&(this._video.paused?this.play():this.pause())};s.prototype.stop=function(){this._video&&(this._video.pause(),this._video.currentTime=0)};s.prototype.pause=function(){this._video&&(this.trace("Video paused"),this._video.pause())};s.prototype.onWidget=function(d,b){};l.registerNodeType("graphics/video", -s);u.title="Webcam";u.desc="Webcam image";u.prototype.openStream=function(){function d(a){console.log("Webcam rejected",a);b._webcam_stream=!1;b.box_color="red"}navigator.getUserMedia=navigator.getUserMedia||navigator.webkitGetUserMedia||navigator.mozGetUserMedia||navigator.msGetUserMedia;window.URL=window.URL||window.webkitURL;if(navigator.getUserMedia){this._waiting_confirmation=!0;navigator.getUserMedia({video:!0},this.streamReady.bind(this),d);var b=this}};u.prototype.onRemoved=function(){this._webcam_stream&& -(this._webcam_stream.stop(),this._video=this._webcam_stream=null)};u.prototype.streamReady=function(d){this._webcam_stream=d;var b=this._video;b||(b=document.createElement("video"),b.autoplay=!0,b.src=window.URL.createObjectURL(d),this._video=b,b.onloadedmetadata=function(a){console.log(a)})};u.prototype.onExecute=function(){null!=this._webcam_stream||this._waiting_confirmation||this.openStream();this._video&&this._video.videoWidth&&(this._video.width=this._video.videoWidth,this._video.height=this._video.videoHeight, -this.setOutputData(0,this._video))};u.prototype.getExtraMenuOptions=function(d){var b=this;return[{content:b.properties.show?"Hide Frame":"Show Frame",callback:function(){b.properties.show=!b.properties.show}}]};u.prototype.onDrawBackground=function(d){this.flags.collapsed||20>=this.size[1]||!this.properties.show||!this._video||(d.save(),d.drawImage(this._video,0,0,this.size[0],this.size[1]),d.restore())};l.registerNodeType("graphics/webcam",u)})(this); -(function(t){var h=t.LiteGraph;t.LGraphTexture=null;if("undefined"!=typeof GL){var e=function(){this.addOutput("Texture","Texture");this.properties={name:"",filter:!0};this.size=[e.image_preview_size,e.image_preview_size]};t.LGraphTexture=e;e.title="Texture";e.desc="Texture";e.widgets_info={name:{widget:"texture"},filter:{widget:"checkbox"}};e.loadTextureCallback=null;e.image_preview_size=256;e.PASS_THROUGH=1;e.COPY=2;e.LOW=3;e.HIGH=4;e.REUSE=5;e.DEFAULT=2;e.MODE_VALUES={"pass through":e.PASS_THROUGH, +d.width*this.properties.scale,d.height*this.properties.scale),this.setOutputData(0,this.canvas)):this.setOutputData(0,null))};q.prototype.onDrawBackground=function(d){this.flags.collapsed||this.canvas&&d.drawImage(this.canvas,0,0,this.canvas.width,this.canvas.height,0,0,this.size[0],this.size[1])};q.prototype.onPropertyChanged=function(d,c){this.properties[d]=c;"scale"==d?(this.properties[d]=parseFloat(c),0==this.properties[d]&&(this.trace("Error in scale"),this.properties[d]=1)):this.properties[d]= +parseInt(c);this.createCanvas();return!0};l.registerNodeType("graphics/cropImage",q);t.title="Video";t.desc="Video playback";t.widgets=[{name:"play",text:"PLAY",type:"minibutton"},{name:"stop",text:"STOP",type:"minibutton"},{name:"demo",text:"Demo video",type:"button"},{name:"mute",text:"Mute video",type:"button"}];t.prototype.onExecute=function(){if(this.properties.url&&(this.properties.url!=this._video_url&&this.loadVideo(this.properties.url),this._video&&0!=this._video.width)){var d=this.getInputData(0); +d&&0<=d&&1>=d&&(this._video.currentTime=d*this._video.duration,this._video.pause());this._video.dirty=!0;this.setOutputData(0,this._video);this.setOutputData(1,this._video.currentTime);this.setOutputData(2,this._video.duration);this.setDirtyCanvas(!0)}};t.prototype.onStart=function(){this.play()};t.prototype.onStop=function(){this.stop()};t.prototype.loadVideo=function(d){this._video_url=d;this.properties.use_proxy&&"http"==d.substr(0,4)&&l.proxy&&(d=l.proxy+d.substr(d.indexOf(":")+3));this._video= +document.createElement("video");this._video.src=d;this._video.type="type=video/mp4";this._video.muted=!0;this._video.autoplay=!0;var c=this;this._video.addEventListener("loadedmetadata",function(a){c.trace("Duration: "+this.duration+" seconds");c.trace("Size: "+this.videoWidth+","+this.videoHeight);c.setDirtyCanvas(!0);this.width=this.videoWidth;this.height=this.videoHeight});this._video.addEventListener("progress",function(a){});this._video.addEventListener("error",function(a){console.log("Error loading video: "+ +this.src);c.trace("Error loading video: "+this.src);if(this.error)switch(this.error.code){case this.error.MEDIA_ERR_ABORTED:c.trace("You stopped the video.");break;case this.error.MEDIA_ERR_NETWORK:c.trace("Network error - please try again later.");break;case this.error.MEDIA_ERR_DECODE:c.trace("Video is broken..");break;case this.error.MEDIA_ERR_SRC_NOT_SUPPORTED:c.trace("Sorry, your browser can't play this video.")}});this._video.addEventListener("ended",function(a){c.trace("Ended.");this.play()})}; +t.prototype.onPropertyChanged=function(d,c){this.properties[d]=c;"url"==d&&""!=c&&this.loadVideo(c);return!0};t.prototype.play=function(){this._video&&this._video.play()};t.prototype.playPause=function(){this._video&&(this._video.paused?this.play():this.pause())};t.prototype.stop=function(){this._video&&(this._video.pause(),this._video.currentTime=0)};t.prototype.pause=function(){this._video&&(this.trace("Video paused"),this._video.pause())};t.prototype.onWidget=function(d,c){};l.registerNodeType("graphics/video", +t);u.title="Webcam";u.desc="Webcam image";u.prototype.openStream=function(){function d(a){console.log("Webcam rejected",a);c._webcam_stream=!1;c.box_color="red"}navigator.getUserMedia=navigator.getUserMedia||navigator.webkitGetUserMedia||navigator.mozGetUserMedia||navigator.msGetUserMedia;window.URL=window.URL||window.webkitURL;if(navigator.getUserMedia){this._waiting_confirmation=!0;navigator.getUserMedia({video:!0},this.streamReady.bind(this),d);var c=this}};u.prototype.onRemoved=function(){this._webcam_stream&& +(this._webcam_stream.stop(),this._video=this._webcam_stream=null)};u.prototype.streamReady=function(d){this._webcam_stream=d;var c=this._video;c||(c=document.createElement("video"),c.autoplay=!0,c.src=window.URL.createObjectURL(d),this._video=c,c.onloadedmetadata=function(a){console.log(a)})};u.prototype.onExecute=function(){null!=this._webcam_stream||this._waiting_confirmation||this.openStream();this._video&&this._video.videoWidth&&(this._video.width=this._video.videoWidth,this._video.height=this._video.videoHeight, +this.setOutputData(0,this._video))};u.prototype.getExtraMenuOptions=function(d){var c=this;return[{content:c.properties.show?"Hide Frame":"Show Frame",callback:function(){c.properties.show=!c.properties.show}}]};u.prototype.onDrawBackground=function(d){this.flags.collapsed||20>=this.size[1]||!this.properties.show||!this._video||(d.save(),d.drawImage(this._video,0,0,this.size[0],this.size[1]),d.restore())};l.registerNodeType("graphics/webcam",u)})(this); +(function(s){var h=s.LiteGraph;s.LGraphTexture=null;if("undefined"!=typeof GL){var e=function(){this.addOutput("Texture","Texture");this.properties={name:"",filter:!0};this.size=[e.image_preview_size,e.image_preview_size]};s.LGraphTexture=e;e.title="Texture";e.desc="Texture";e.widgets_info={name:{widget:"texture"},filter:{widget:"checkbox"}};e.loadTextureCallback=null;e.image_preview_size=256;e.PASS_THROUGH=1;e.COPY=2;e.LOW=3;e.HIGH=4;e.REUSE=5;e.DEFAULT=2;e.MODE_VALUES={"pass through":e.PASS_THROUGH, copy:e.COPY,low:e.LOW,high:e.HIGH,reuse:e.REUSE,"default":e.DEFAULT};e.getTexturesContainer=function(){return gl.textures};e.loadTexture=function(a,b){b=b||{};var c=a;"http://"==c.substr(0,7)&&h.proxy&&(c=h.proxy+c.substr(7));return e.getTexturesContainer()[a]=GL.Texture.fromURL(c,b)};e.getTexture=function(a){var b=this.getTexturesContainer();if(!b)throw"Cannot load texture, container of textures not found";b=b[a];return!b&&a&&":"!=a[0]?this.loadTexture(a):b};e.getTargetTexture=function(a,b,c){if(!a)throw"LGraphTexture.getTargetTexture expects a reference texture"; var d=null;switch(c){case e.LOW:d=gl.UNSIGNED_BYTE;break;case e.HIGH:d=gl.HIGH_PRECISION_FORMAT;break;case e.REUSE:return a;default:d=a?a.type:gl.UNSIGNED_BYTE}b&&b.width==a.width&&b.height==a.height&&b.type==d||(b=new GL.Texture(a.width,a.height,{type:d,format:gl.RGBA,filter:gl.LINEAR}));return b};e.getTextureType=function(a,b){var c=b?b.type:gl.UNSIGNED_BYTE;switch(a){case e.HIGH:c=gl.HIGH_PRECISION_FORMAT;break;case e.LOW:c=gl.UNSIGNED_BYTE}return c};e.getNoiseTexture=function(){if(this._noise_texture)return this._noise_texture; for(var a=new Uint8Array(1048576),b=0;1048576>b;++b)a[b]=255*Math.random();return this._noise_texture=a=GL.Texture.fromMemory(512,512,a,{format:gl.RGBA,wrap:gl.REPEAT,filter:gl.NEAREST})};e.prototype.onDropFile=function(a,b,c){if(a){var d=null;"string"==typeof a?d=GL.Texture.fromURL(a):-1!=b.toLowerCase().indexOf(".dds")?d=GL.Texture.fromDDSInMemory(a):(a=new Blob([c]),a=URL.createObjectURL(a),d=GL.Texture.fromURL(a));this._drop_texture=d;this.properties.name=b}else this._drop_texture=null,this.properties.name= @@ -300,23 +303,23 @@ for(var a=new Uint8Array(1048576),b=0;1048576>b;++b)a[b]=255*Math.random();retur gl.LINEAR);this.setOutputData(0,a);for(var b=1;b=this.size[1]))if(this._drop_texture&&a.webgl)a.drawImage(this._drop_texture,0,0,this.size[0],this.size[1]); else{if(this._last_preview_tex!=this._last_tex)if(a.webgl)this._canvas=this._last_tex;else{var b=e.generateLowResTexturePreview(this._last_tex);if(!b)return;this._last_preview_tex=this._last_tex;this._canvas=cloneCanvas(b)}this._canvas&&(a.save(),a.webgl||(a.translate(0,this.size[1]),a.scale(1,-1)),a.drawImage(this._canvas,0,0,this.size[0],this.size[1]),a.restore())}};e.generateLowResTexturePreview=function(a){if(!a)return null;var b=e.image_preview_size,c=a;if(a.format==gl.DEPTH_COMPONENT)return null; if(a.width>b||a.height>b)c=this._preview_temp_tex,this._preview_temp_tex||(this._preview_temp_tex=c=new GL.Texture(b,b,{minFilter:gl.NEAREST})),a.copyTo(c);a=this._preview_canvas;a||(this._preview_canvas=a=createCanvas(b,b));c&&c.toCanvas(a);return a};e.prototype.getResources=function(a){a[this.properties.name]=GL.Texture;return a};e.prototype.onGetInputs=function(){return[["in","Texture"]]};e.prototype.onGetOutputs=function(){return[["width","number"],["height","number"],["aspect","number"]]};h.registerNodeType("texture/texture", -e);var m=function(){this.addInput("Texture","Texture");this.properties={flipY:!1};this.size=[e.image_preview_size,e.image_preview_size]};m.title="Preview";m.desc="Show a texture in the graph canvas";m.allow_preview=!1;m.prototype.onDrawBackground=function(a){if(!this.flags.collapsed&&(a.webgl||m.allow_preview)){var b=this.getInputData(0);if(b){var c=null,c=!b.handle&&a.webgl?b:e.generateLowResTexturePreview(b);a.save();this.properties.flipY&&(a.translate(0,this.size[1]),a.scale(1,-1));a.drawImage(c, -0,0,this.size[0],this.size[1]);a.restore()}}};h.registerNodeType("texture/preview",m);var d=function(){this.addInput("Texture","Texture");this.addOutput("","Texture");this.properties={name:""}};d.title="Save";d.desc="Save a texture in the repository";d.prototype.onExecute=function(){var a=this.getInputData(0);a&&(this.properties.name&&(e.storeTexture?e.storeTexture(this.properties.name,a):e.getTexturesContainer()[this.properties.name]=a),this.setOutputData(0,a))};h.registerNodeType("texture/save", +e);var n=function(){this.addInput("Texture","Texture");this.properties={flipY:!1};this.size=[e.image_preview_size,e.image_preview_size]};n.title="Preview";n.desc="Show a texture in the graph canvas";n.allow_preview=!1;n.prototype.onDrawBackground=function(a){if(!this.flags.collapsed&&(a.webgl||n.allow_preview)){var b=this.getInputData(0);if(b){var c=null,c=!b.handle&&a.webgl?b:e.generateLowResTexturePreview(b);a.save();this.properties.flipY&&(a.translate(0,this.size[1]),a.scale(1,-1));a.drawImage(c, +0,0,this.size[0],this.size[1]);a.restore()}}};h.registerNodeType("texture/preview",n);var d=function(){this.addInput("Texture","Texture");this.addOutput("","Texture");this.properties={name:""}};d.title="Save";d.desc="Save a texture in the repository";d.prototype.onExecute=function(){var a=this.getInputData(0);a&&(this.properties.name&&(e.storeTexture?e.storeTexture(this.properties.name,a):e.getTexturesContainer()[this.properties.name]=a),this.setOutputData(0,a))};h.registerNodeType("texture/save", d);var r=function(){this.addInput("Texture","Texture");this.addInput("TextureB","Texture");this.addInput("value","number");this.addOutput("Texture","Texture");this.help="

pixelcode must be vec3

\t\t\t

uvcode must be vec2, is optional

\t\t\t

uv: tex. coords

color: texture

colorB: textureB

time: scene time

value: input value

";this.properties={value:1,uvcode:"",pixelcode:"color + colorB * value", precision:e.DEFAULT}};r.widgets_info={uvcode:{widget:"textarea",height:100},pixelcode:{widget:"textarea",height:100},precision:{widget:"combo",values:e.MODE_VALUES}};r.title="Operation";r.desc="Texture shader operation";r.prototype.getExtraMenuOptions=function(a){var b=this;return[{content:b.properties.show?"Hide Texture":"Show Texture",callback:function(){b.properties.show=!b.properties.show}}]};r.prototype.onDrawBackground=function(a){this.flags.collapsed||20>=this.size[1]||!this.properties.show|| !this._tex||this._tex.gl!=a||(a.save(),a.drawImage(this._tex,0,0,this.size[0],this.size[1]),a.restore())};r.prototype.onExecute=function(){var a=this.getInputData(0);if(this.isOutputConnected(0))if(this.properties.precision===e.PASS_THROUGH)this.setOutputData(0,a);else{var b=this.getInputData(1);if(this.properties.uvcode||this.properties.pixelcode){var c=512,d=512;a?(c=a.width,d=a.height):b&&(c=b.width,d=b.height);var f=e.getTextureType(this.properties.precision,a);this._tex=a||this._tex?e.getTargetTexture(a|| this._tex,this._tex,this.properties.precision):new GL.Texture(c,d,{type:f,format:gl.RGBA,filter:gl.LINEAR});f="";this.properties.uvcode&&(f="uv = "+this.properties.uvcode,-1!=this.properties.uvcode.indexOf(";")&&(f=this.properties.uvcode));var k="";this.properties.pixelcode&&(k="result = "+this.properties.pixelcode,-1!=this.properties.pixelcode.indexOf(";")&&(k=this.properties.pixelcode));var g=this._shader;if(!g||this._shader_code!=f+"|"+k){try{this._shader=new GL.Shader(Shader.SCREEN_VERTEX_SHADER, -r.pixel_shader,{UV_CODE:f,PIXEL_CODE:k}),this.boxcolor="#00FF00"}catch(h){console.log("Error compiling shader: ",h);this.boxcolor="#FF0000";return}this.boxcolor="#FF0000";this._shader_code=f+"|"+k;g=this._shader}if(g){this.boxcolor="green";var n=this.getInputData(2);null!=n?this.properties.value=n:n=parseFloat(this.properties.value);var l=this.graph.getTime();this._tex.drawTo(function(){gl.disable(gl.DEPTH_TEST);gl.disable(gl.CULL_FACE);gl.disable(gl.BLEND);a&&a.bind(0);b&&b.bind(1);var e=Mesh.getScreenQuad(); -g.uniforms({u_texture:0,u_textureB:1,value:n,texSize:[c,d],time:l}).draw(e)});this.setOutputData(0,this._tex)}else this.boxcolor="red"}}};r.pixel_shader="precision highp float;\n\t\t\t\n\t\t\tuniform sampler2D u_texture;\n\t\t\tuniform sampler2D u_textureB;\n\t\t\tvarying vec2 v_coord;\n\t\t\tuniform vec2 texSize;\n\t\t\tuniform float time;\n\t\t\tuniform float value;\n\t\t\t\n\t\t\tvoid main() {\n\t\t\t\tvec2 uv = v_coord;\n\t\t\t\tUV_CODE;\n\t\t\t\tvec4 color4 = texture2D(u_texture, uv);\n\t\t\t\tvec3 color = color4.rgb;\n\t\t\t\tvec4 color4B = texture2D(u_textureB, uv);\n\t\t\t\tvec3 colorB = color4B.rgb;\n\t\t\t\tvec3 result = color;\n\t\t\t\tfloat alpha = 1.0;\n\t\t\t\tPIXEL_CODE;\n\t\t\t\tgl_FragColor = vec4(result, alpha);\n\t\t\t}\n\t\t\t"; +r.pixel_shader,{UV_CODE:f,PIXEL_CODE:k}),this.boxcolor="#00FF00"}catch(h){console.log("Error compiling shader: ",h);this.boxcolor="#FF0000";return}this.boxcolor="#FF0000";this._shader_code=f+"|"+k;g=this._shader}if(g){this.boxcolor="green";var m=this.getInputData(2);null!=m?this.properties.value=m:m=parseFloat(this.properties.value);var l=this.graph.getTime();this._tex.drawTo(function(){gl.disable(gl.DEPTH_TEST);gl.disable(gl.CULL_FACE);gl.disable(gl.BLEND);a&&a.bind(0);b&&b.bind(1);var f=Mesh.getScreenQuad(); +g.uniforms({u_texture:0,u_textureB:1,value:m,texSize:[c,d],time:l}).draw(f)});this.setOutputData(0,this._tex)}else this.boxcolor="red"}}};r.pixel_shader="precision highp float;\n\t\t\t\n\t\t\tuniform sampler2D u_texture;\n\t\t\tuniform sampler2D u_textureB;\n\t\t\tvarying vec2 v_coord;\n\t\t\tuniform vec2 texSize;\n\t\t\tuniform float time;\n\t\t\tuniform float value;\n\t\t\t\n\t\t\tvoid main() {\n\t\t\t\tvec2 uv = v_coord;\n\t\t\t\tUV_CODE;\n\t\t\t\tvec4 color4 = texture2D(u_texture, uv);\n\t\t\t\tvec3 color = color4.rgb;\n\t\t\t\tvec4 color4B = texture2D(u_textureB, uv);\n\t\t\t\tvec3 colorB = color4B.rgb;\n\t\t\t\tvec3 result = color;\n\t\t\t\tfloat alpha = 1.0;\n\t\t\t\tPIXEL_CODE;\n\t\t\t\tgl_FragColor = vec4(result, alpha);\n\t\t\t}\n\t\t\t"; h.registerNodeType("texture/operation",r);var q=function(){this.addOutput("out","Texture");this.properties={code:"",width:512,height:512,precision:e.DEFAULT};this.properties.code="\nvoid main() {\n vec2 uv = v_coord;\n vec3 color = vec3(0.0);\n//your code here\n\ngl_FragColor = vec4(color, 1.0);\n}\n";this._uniforms={in_texture:0,texSize:vec2.create(),time:0}};q.title="Shader";q.desc="Texture shader";q.widgets_info={code:{type:"code"},precision:{widget:"combo",values:e.MODE_VALUES}};q.prototype.onPropertyChanged= -function(a,b){if("code"==a){var c=this.getShader();if(c){var d=c.uniformInfo;if(this.inputs)for(var e={},f=0;f>1||0;d=d>>1||0;h=GL.Texture.getTemporary(c,d,a);l.push(h);g.setParameter(GL.TEXTURE_MAG_FILTER, -GL.NEAREST);g.copyTo(h,b,p);if(1==c&&1==d)break;g=h}this._texture=l.pop();for(m=0;md;++d)c[d]=Math.random();b._shader.uniforms({u_samples_a:c.subarray(0,16),u_samples_b:c.subarray(16,32)})}d=this._temp_texture;c=gl.UNSIGNED_BYTE;a.type!=c&&(c=gl.FLOAT);d&&d.type==c||(this._temp_texture=new GL.Texture(1,1,{type:c,format:gl.RGBA,filter:gl.NEAREST})); -var f=b._shader,e=this._uniforms;e.u_mipmap_offset=this.properties.mipmap_offset;this._temp_texture.drawTo(function(){a.toViewport(f,e)});this.setOutputData(0,this._temp_texture);if(this.isOutputConnected(1)||this.isOutputConnected(2))if(d=this._temp_texture.getPixels()){var k=this._luminance,c=this._temp_texture.type;k.set(d);c==gl.UNSIGNED_BYTE?vec4.scale(k,k,1/255):c!=GL.HALF_FLOAT&&c!=GL.HALF_FLOAT_OES||vec4.scale(k,k,1/65025);this.setOutputData(1,k);this.setOutputData(2,(k[0]+k[1]+k[2])/3)}}}; -b.pixel_shader="precision highp float;\n\t\t\tprecision highp float;\n\t\t\tuniform mat4 u_samples_a;\n\t\t\tuniform mat4 u_samples_b;\n\t\t\tuniform sampler2D u_texture;\n\t\t\tuniform float u_mipmap_offset;\n\t\t\tvarying vec2 v_coord;\n\t\t\t\n\t\t\tvoid main() {\n\t\t\t\tvec4 color = vec4(0.0);\n\t\t\t\tfor(int i = 0; i < 4; ++i)\n\t\t\t\t\tfor(int j = 0; j < 4; ++j)\n\t\t\t\t\t{\n\t\t\t\t\t\tcolor += texture2D(u_texture, vec2( u_samples_a[i][j], u_samples_b[i][j] ), u_mipmap_offset );\n\t\t\t\t\t\tcolor += texture2D(u_texture, vec2( 1.0 - u_samples_a[i][j], 1.0 - u_samples_b[i][j] ), u_mipmap_offset );\n\t\t\t\t\t}\n\t\t\t gl_FragColor = color * 0.03125;\n\t\t\t}\n\t\t\t"; -h.registerNodeType("texture/average",b);d=function(){this.addInput("Image","image");this.addOutput("","Texture");this.properties={}};d.title="Image to Texture";d.desc="Uploads an image to the GPU";d.prototype.onExecute=function(){var a=this.getInputData(0);if(a){var b=a.videoWidth||a.width,c=a.videoHeight||a.height;if(a.gltexture)this.setOutputData(0,a.gltexture);else{var d=this._temp_texture;d&&d.width==b&&d.height==c||(this._temp_texture=new GL.Texture(b,c,{format:gl.RGBA,filter:gl.LINEAR}));try{this._temp_texture.uploadImage(a)}catch(f){console.error("image comes from an unsafe location, cannot be uploaded to webgl: "+ +d);var m=function(){this.addInput("Texture","Texture");this.addOutput("","Texture");this.properties={iterations:1,generate_mipmaps:!1,precision:e.DEFAULT}};m.title="Downsample";m.desc="Downsample Texture";m.widgets_info={iterations:{type:"number",step:1,precision:0,min:1},precision:{widget:"combo",values:e.MODE_VALUES}};m.prototype.onExecute=function(){var a=this.getInputData(0);if((a||this._temp_texture)&&this.isOutputConnected(0)&&a&&a.texture_type===GL.TEXTURE_2D){var b=m._shader;b||(m._shader= +b=new GL.Shader(GL.Shader.SCREEN_VERTEX_SHADER,m.pixel_shader));var c=a.width|0,d=a.height|0,f=a.type;this.properties.precision===e.LOW?f=gl.UNSIGNED_BYTE:this.properties.precision===e.HIGH&&(f=gl.HIGH_PRECISION_FORMAT);var k=this.properties.iterations||1,g=a,h=null,l=[],a={type:f,format:a.format},f=vec2.create(),p={u_offset:f};this._texture&&GL.Texture.releaseTemporary(this._texture);for(var q=0;q>1||0;d=d>>1||0;h=GL.Texture.getTemporary(c,d,a);l.push(h);g.setParameter(GL.TEXTURE_MAG_FILTER, +GL.NEAREST);g.copyTo(h,b,p);if(1==c&&1==d)break;g=h}this._texture=l.pop();for(q=0;qd;++d)b[d]=Math.random();c._shader.uniforms({u_samples_a:b.subarray(0,16),u_samples_b:b.subarray(16,32)})}d=this._temp_texture;b=gl.UNSIGNED_BYTE;a.type!=b&&(b=gl.FLOAT);d&&d.type==b||(this._temp_texture=new GL.Texture(1,1,{type:b,format:gl.RGBA,filter:gl.NEAREST})); +var f=c._shader,k=this._uniforms;k.u_mipmap_offset=this.properties.mipmap_offset;this._temp_texture.drawTo(function(){a.toViewport(f,k)});this.setOutputData(0,this._temp_texture);if(this.isOutputConnected(1)||this.isOutputConnected(2))if(d=this._temp_texture.getPixels()){var e=this._luminance,b=this._temp_texture.type;e.set(d);b==gl.UNSIGNED_BYTE?vec4.scale(e,e,1/255):b!=GL.HALF_FLOAT&&b!=GL.HALF_FLOAT_OES||vec4.scale(e,e,1/65025);this.setOutputData(1,e);this.setOutputData(2,(e[0]+e[1]+e[2])/3)}}}; +c.pixel_shader="precision highp float;\n\t\t\tprecision highp float;\n\t\t\tuniform mat4 u_samples_a;\n\t\t\tuniform mat4 u_samples_b;\n\t\t\tuniform sampler2D u_texture;\n\t\t\tuniform float u_mipmap_offset;\n\t\t\tvarying vec2 v_coord;\n\t\t\t\n\t\t\tvoid main() {\n\t\t\t\tvec4 color = vec4(0.0);\n\t\t\t\tfor(int i = 0; i < 4; ++i)\n\t\t\t\t\tfor(int j = 0; j < 4; ++j)\n\t\t\t\t\t{\n\t\t\t\t\t\tcolor += texture2D(u_texture, vec2( u_samples_a[i][j], u_samples_b[i][j] ), u_mipmap_offset );\n\t\t\t\t\t\tcolor += texture2D(u_texture, vec2( 1.0 - u_samples_a[i][j], 1.0 - u_samples_b[i][j] ), u_mipmap_offset );\n\t\t\t\t\t}\n\t\t\t gl_FragColor = color * 0.03125;\n\t\t\t}\n\t\t\t"; +h.registerNodeType("texture/average",c);d=function(){this.addInput("Image","image");this.addOutput("","Texture");this.properties={}};d.title="Image to Texture";d.desc="Uploads an image to the GPU";d.prototype.onExecute=function(){var a=this.getInputData(0);if(a){var b=a.videoWidth||a.width,c=a.videoHeight||a.height;if(a.gltexture)this.setOutputData(0,a.gltexture);else{var d=this._temp_texture;d&&d.width==b&&d.height==c||(this._temp_texture=new GL.Texture(b,c,{format:gl.RGBA,filter:gl.LINEAR}));try{this._temp_texture.uploadImage(a)}catch(f){console.error("image comes from an unsafe location, cannot be uploaded to webgl: "+ f);return}this.setOutputData(0,this._temp_texture)}}};h.registerNodeType("texture/imageToTexture",d);var a=function(){this.addInput("Texture","Texture");this.addInput("LUT","Texture");this.addInput("Intensity","number");this.addOutput("","Texture");this.properties={intensity:1,precision:e.DEFAULT,texture:null};a._shader||(a._shader=new GL.Shader(Shader.SCREEN_VERTEX_SHADER,a.pixel_shader))};a.widgets_info={texture:{widget:"texture"},precision:{widget:"combo",values:e.MODE_VALUES}};a.title="LUT";a.desc= "Apply LUT to Texture";a.prototype.onExecute=function(){if(this.isOutputConnected(0)){var b=this.getInputData(0);if(this.properties.precision===e.PASS_THROUGH)this.setOutputData(0,b);else if(b){var c=this.getInputData(1);c||(c=e.getTexture(this.properties.texture));if(c){c.bind(0);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);gl.bindTexture(gl.TEXTURE_2D, null);var d=this.properties.intensity;this.isInputConnected(2)&&(this.properties.intensity=d=this.getInputData(2));this._tex=e.getTargetTexture(b,this._tex,this.properties.precision);this._tex.drawTo(function(){c.bind(1);b.toViewport(a._shader,{u_texture:0,u_textureB:1,u_amount:d})});this.setOutputData(0,this._tex)}else this.setOutputData(0,b)}}};a.pixel_shader="precision highp float;\n\t\t\tprecision highp float;\n\t\t\tvarying vec2 v_coord;\n\t\t\tuniform sampler2D u_texture;\n\t\t\tuniform sampler2D u_textureB;\n\t\t\tuniform float u_amount;\n\t\t\t\n\t\t\tvoid main() {\n\t\t\t\t lowp vec4 textureColor = clamp( texture2D(u_texture, v_coord), vec4(0.0), vec4(1.0) );\n\t\t\t\t mediump float blueColor = textureColor.b * 63.0;\n\t\t\t\t mediump vec2 quad1;\n\t\t\t\t quad1.y = floor(floor(blueColor) / 8.0);\n\t\t\t\t quad1.x = floor(blueColor) - (quad1.y * 8.0);\n\t\t\t\t mediump vec2 quad2;\n\t\t\t\t quad2.y = floor(ceil(blueColor) / 8.0);\n\t\t\t\t quad2.x = ceil(blueColor) - (quad2.y * 8.0);\n\t\t\t\t highp vec2 texPos1;\n\t\t\t\t texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);\n\t\t\t\t texPos1.y = 1.0 - ((quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g));\n\t\t\t\t highp vec2 texPos2;\n\t\t\t\t texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);\n\t\t\t\t texPos2.y = 1.0 - ((quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g));\n\t\t\t\t lowp vec4 newColor1 = texture2D(u_textureB, texPos1);\n\t\t\t\t lowp vec4 newColor2 = texture2D(u_textureB, texPos2);\n\t\t\t\t lowp vec4 newColor = mix(newColor1, newColor2, fract(blueColor));\n\t\t\t\t gl_FragColor = vec4( mix( textureColor.rgb, newColor.rgb, u_amount), textureColor.w);\n\t\t\t}\n\t\t\t"; -h.registerNodeType("texture/LUT",a);var c=function(){this.addInput("Texture","Texture");this.addOutput("R","Texture");this.addOutput("G","Texture");this.addOutput("B","Texture");this.addOutput("A","Texture");this.properties={};c._shader||(c._shader=new GL.Shader(Shader.SCREEN_VERTEX_SHADER,c.pixel_shader))};c.title="Texture to Channels";c.desc="Split texture channels";c.prototype.onExecute=function(){var a=this.getInputData(0);if(a){this._channels||(this._channels=Array(4));for(var b=0,d=0;4>d;d++)this.isOutputConnected(d)? -(this._channels[d]&&this._channels[d].width==a.width&&this._channels[d].height==a.height&&this._channels[d].type==a.type||(this._channels[d]=new GL.Texture(a.width,a.height,{type:a.type,format:gl.RGBA,filter:gl.LINEAR})),b++):this._channels[d]=null;if(b){gl.disable(gl.BLEND);gl.disable(gl.DEPTH_TEST);for(var f=Mesh.getScreenQuad(),e=c._shader,k=[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]],d=0;4>d;d++)this._channels[d]&&(this._channels[d].drawTo(function(){a.bind(0);e.uniforms({u_texture:0,u_mask:k[d]}).draw(f)}), -this.setOutputData(d,this._channels[d]))}}};c.pixel_shader="precision highp float;\n\t\t\tprecision highp float;\n\t\t\tvarying vec2 v_coord;\n\t\t\tuniform sampler2D u_texture;\n\t\t\tuniform vec4 u_mask;\n\t\t\t\n\t\t\tvoid main() {\n\t\t\t gl_FragColor = vec4( vec3( length( texture2D(u_texture, v_coord) * u_mask )), 1.0 );\n\t\t\t}\n\t\t\t";h.registerNodeType("texture/textureChannels",c);var g=function(){this.addInput("R","Texture");this.addInput("G","Texture");this.addInput("B","Texture");this.addInput("A", +h.registerNodeType("texture/LUT",a);var b=function(){this.addInput("Texture","Texture");this.addOutput("R","Texture");this.addOutput("G","Texture");this.addOutput("B","Texture");this.addOutput("A","Texture");this.properties={};b._shader||(b._shader=new GL.Shader(Shader.SCREEN_VERTEX_SHADER,b.pixel_shader))};b.title="Texture to Channels";b.desc="Split texture channels";b.prototype.onExecute=function(){var a=this.getInputData(0);if(a){this._channels||(this._channels=Array(4));for(var c=0,d=0;4>d;d++)this.isOutputConnected(d)? +(this._channels[d]&&this._channels[d].width==a.width&&this._channels[d].height==a.height&&this._channels[d].type==a.type||(this._channels[d]=new GL.Texture(a.width,a.height,{type:a.type,format:gl.RGBA,filter:gl.LINEAR})),c++):this._channels[d]=null;if(c){gl.disable(gl.BLEND);gl.disable(gl.DEPTH_TEST);for(var f=Mesh.getScreenQuad(),e=b._shader,k=[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]],d=0;4>d;d++)this._channels[d]&&(this._channels[d].drawTo(function(){a.bind(0);e.uniforms({u_texture:0,u_mask:k[d]}).draw(f)}), +this.setOutputData(d,this._channels[d]))}}};b.pixel_shader="precision highp float;\n\t\t\tprecision highp float;\n\t\t\tvarying vec2 v_coord;\n\t\t\tuniform sampler2D u_texture;\n\t\t\tuniform vec4 u_mask;\n\t\t\t\n\t\t\tvoid main() {\n\t\t\t gl_FragColor = vec4( vec3( length( texture2D(u_texture, v_coord) * u_mask )), 1.0 );\n\t\t\t}\n\t\t\t";h.registerNodeType("texture/textureChannels",b);var g=function(){this.addInput("R","Texture");this.addInput("G","Texture");this.addInput("B","Texture");this.addInput("A", "Texture");this.addOutput("Texture","Texture");this.properties={};g._shader||(g._shader=new GL.Shader(Shader.SCREEN_VERTEX_SHADER,g.pixel_shader))};g.title="Channels to Texture";g.desc="Split texture channels";g.prototype.onExecute=function(){var a=[this.getInputData(0),this.getInputData(1),this.getInputData(2),this.getInputData(3)];if(a[0]&&a[1]&&a[2]&&a[3]){gl.disable(gl.BLEND);gl.disable(gl.DEPTH_TEST);var b=Mesh.getScreenQuad(),c=g._shader;this._tex=e.getTargetTexture(a[0],this._tex);this._tex.drawTo(function(){a[0].bind(0); a[1].bind(1);a[2].bind(2);a[3].bind(3);c.uniforms({u_textureR:0,u_textureG:1,u_textureB:2,u_textureA:3}).draw(b)});this.setOutputData(0,this._tex)}};g.pixel_shader="precision highp float;\n\t\t\tprecision highp float;\n\t\t\tvarying vec2 v_coord;\n\t\t\tuniform sampler2D u_textureR;\n\t\t\tuniform sampler2D u_textureG;\n\t\t\tuniform sampler2D u_textureB;\n\t\t\tuniform sampler2D u_textureA;\n\t\t\t\n\t\t\tvoid main() {\n\t\t\t gl_FragColor = vec4( \t\t\t\t\t\ttexture2D(u_textureR, v_coord).r,\t\t\t\t\t\ttexture2D(u_textureG, v_coord).r,\t\t\t\t\t\ttexture2D(u_textureB, v_coord).r,\t\t\t\t\t\ttexture2D(u_textureA, v_coord).r);\n\t\t\t}\n\t\t\t"; h.registerNodeType("texture/channelsTexture",g);var w=function(){this.addInput("A","color");this.addInput("B","color");this.addOutput("Texture","Texture");this.properties={angle:0,scale:1,A:[0,0,0],B:[1,1,1],texture_size:32};w._shader||(w._shader=new GL.Shader(Shader.SCREEN_VERTEX_SHADER,w.pixel_shader));this._uniforms={u_angle:0,u_colorA:vec3.create(),u_colorB:vec3.create()}};w.title="Gradient";w.desc="Generates a gradient";w["@A"]={type:"color"};w["@B"]={type:"color"};w["@texture_size"]={type:"enum", @@ -357,17 +360,17 @@ this._final_texture;b&&b.width==a.width&&b.height==a.height&&b.type==a.type||(b= f||void 0===window.gl||(f=gl.canvas.height/gl.canvas.width);f||(f=1);var f=this.properties.preserve_aspect?f:1,e=this.properties.scale||[1,1];a.applyBlur(f*e[0],e[1],d,b);for(a=1;a>=1;1<(c|0)&&(c>>=1);if(2>b)break;n=g[r]=GL.Texture.getTemporary(b,c,d);m[0]=1/l.width;m[1]=1/l.height;l.blit(n,h.uniforms(k));l=n}this.isOutputConnected(2)&&(b=this._average_texture,b&&b.type==a.type&&b.format==a.format||(b=this._average_texture=new GL.Texture(1,1,{type:a.type,format:a.format,filter:gl.LINEAR})),m[0]=1/l.width,m[1]=1/l.height,k.u_intensity= -q,k.u_delta=1,l.blit(b,h.uniforms(k)),this.setOutputData(2,b));gl.enable(gl.BLEND);gl.blendFunc(gl.ONE,gl.ONE);k.u_intensity=this.getInputOrProperty("persistence");k.u_delta=0.5;for(r-=2;0<=r;r--)n=g[r],g[r]=null,m[0]=1/l.width,m[1]=1/l.height,l.blit(n,h.uniforms(k)),GL.Texture.releaseTemporary(l),l=n;gl.disable(gl.BLEND);this.isOutputConnected(1)&&(g=this._glow_texture,g&&g.width==a.width&&g.height==a.height&&g.type==f&&g.format==a.format||(g=this._glow_texture=new GL.Texture(a.width,a.height,{type:f, -format:a.format,filter:gl.LINEAR})),l.blit(g),this.setOutputData(1,g));if(this.isOutputConnected(0)){g=this._final_texture;g&&g.width==a.width&&g.height==a.height&&g.type==f&&g.format==a.format||(g=this._final_texture=new GL.Texture(a.width,a.height,{type:f,format:a.format,filter:gl.LINEAR}));var s=this.getInputData(1),C=this.getInputOrProperty("dirt_factor");k.u_intensity=q;h=s?x._dirt_final_shader:x._final_shader;h||(h=s?x._dirt_final_shader=new GL.Shader(GL.Shader.SCREEN_VERTEX_SHADER,x.final_pixel_shader, -{USE_DIRT:""}):x._final_shader=new GL.Shader(GL.Shader.SCREEN_VERTEX_SHADER,x.final_pixel_shader));g.drawTo(function(){a.bind(0);l.bind(1);s&&(h.setUniform("u_dirt_factor",C),h.setUniform("u_dirt_texture",s.bind(2)));h.toViewport(k)});this.setOutputData(0,g)}GL.Texture.releaseTemporary(l)}};x.cut_pixel_shader="precision highp float;\n\t\tvarying vec2 v_coord;\n\t\tuniform sampler2D u_texture;\n\t\tuniform float u_threshold;\n\t\tvoid main() {\n\t\t\tgl_FragColor = max( texture2D( u_texture, v_coord ) - vec4( u_threshold ), vec4(0.0) );\n\t\t}"; +wrap:gl.CLAMP_TO_EDGE},f=e.getTextureType(this.properties.precision,a),k=this._uniforms,g=this._textures,h=x._cut_shader;h||(h=x._cut_shader=new GL.Shader(GL.Shader.SCREEN_VERTEX_SHADER,x.cut_pixel_shader));gl.disable(gl.DEPTH_TEST);gl.disable(gl.BLEND);k.u_threshold=this.getInputOrProperty("threshold");var m=g[0]=GL.Texture.getTemporary(b,c,d);a.blit(m,h.uniforms(k));var l=m,p=this.getInputOrProperty("iterations"),p=Math.clamp(p,1,16)|0,q=k.u_texel_size,n=this.getInputOrProperty("intensity");k.u_intensity= +1;k.u_delta=this.properties.scale;h=x._shader;h||(h=x._shader=new GL.Shader(GL.Shader.SCREEN_VERTEX_SHADER,x.scale_pixel_shader));for(var r=1;r>=1;1<(c|0)&&(c>>=1);if(2>b)break;m=g[r]=GL.Texture.getTemporary(b,c,d);q[0]=1/l.width;q[1]=1/l.height;l.blit(m,h.uniforms(k));l=m}this.isOutputConnected(2)&&(b=this._average_texture,b&&b.type==a.type&&b.format==a.format||(b=this._average_texture=new GL.Texture(1,1,{type:a.type,format:a.format,filter:gl.LINEAR})),q[0]=1/l.width,q[1]=1/l.height,k.u_intensity= +n,k.u_delta=1,l.blit(b,h.uniforms(k)),this.setOutputData(2,b));gl.enable(gl.BLEND);gl.blendFunc(gl.ONE,gl.ONE);k.u_intensity=this.getInputOrProperty("persistence");k.u_delta=0.5;for(r-=2;0<=r;r--)m=g[r],g[r]=null,q[0]=1/l.width,q[1]=1/l.height,l.blit(m,h.uniforms(k)),GL.Texture.releaseTemporary(l),l=m;gl.disable(gl.BLEND);this.isOutputConnected(1)&&(g=this._glow_texture,g&&g.width==a.width&&g.height==a.height&&g.type==f&&g.format==a.format||(g=this._glow_texture=new GL.Texture(a.width,a.height,{type:f, +format:a.format,filter:gl.LINEAR})),l.blit(g),this.setOutputData(1,g));if(this.isOutputConnected(0)){g=this._final_texture;g&&g.width==a.width&&g.height==a.height&&g.type==f&&g.format==a.format||(g=this._final_texture=new GL.Texture(a.width,a.height,{type:f,format:a.format,filter:gl.LINEAR}));var t=this.getInputData(1),C=this.getInputOrProperty("dirt_factor");k.u_intensity=n;h=t?x._dirt_final_shader:x._final_shader;h||(h=t?x._dirt_final_shader=new GL.Shader(GL.Shader.SCREEN_VERTEX_SHADER,x.final_pixel_shader, +{USE_DIRT:""}):x._final_shader=new GL.Shader(GL.Shader.SCREEN_VERTEX_SHADER,x.final_pixel_shader));g.drawTo(function(){a.bind(0);l.bind(1);t&&(h.setUniform("u_dirt_factor",C),h.setUniform("u_dirt_texture",t.bind(2)));h.toViewport(k)});this.setOutputData(0,g)}GL.Texture.releaseTemporary(l)}};x.cut_pixel_shader="precision highp float;\n\t\tvarying vec2 v_coord;\n\t\tuniform sampler2D u_texture;\n\t\tuniform float u_threshold;\n\t\tvoid main() {\n\t\t\tgl_FragColor = max( texture2D( u_texture, v_coord ) - vec4( u_threshold ), vec4(0.0) );\n\t\t}"; x.scale_pixel_shader="precision highp float;\n\t\tvarying vec2 v_coord;\n\t\tuniform sampler2D u_texture;\n\t\tuniform vec2 u_texel_size;\n\t\tuniform float u_delta;\n\t\tuniform float u_intensity;\n\t\t\n\t\tvec4 sampleBox(vec2 uv) {\n\t\t\tvec4 o = u_texel_size.xyxy * vec2(-u_delta, u_delta).xxyy;\n\t\t\tvec4 s = texture2D( u_texture, uv + o.xy ) + texture2D( u_texture, uv + o.zy) + texture2D( u_texture, uv + o.xw) + texture2D( u_texture, uv + o.zw);\n\t\t\treturn s * 0.25;\n\t\t}\n\t\tvoid main() {\n\t\t\tgl_FragColor = u_intensity * sampleBox( v_coord );\n\t\t}"; x.final_pixel_shader="precision highp float;\n\t\tvarying vec2 v_coord;\n\t\tuniform sampler2D u_texture;\n\t\tuniform sampler2D u_glow_texture;\n\t\t#ifdef USE_DIRT\n\t\t\tuniform sampler2D u_dirt_texture;\n\t\t#endif\n\t\tuniform vec2 u_texel_size;\n\t\tuniform float u_delta;\n\t\tuniform float u_intensity;\n\t\tuniform float u_dirt_factor;\n\t\t\n\t\tvec4 sampleBox(vec2 uv) {\n\t\t\tvec4 o = u_texel_size.xyxy * vec2(-u_delta, u_delta).xxyy;\n\t\t\tvec4 s = texture2D( u_glow_texture, uv + o.xy ) + texture2D( u_glow_texture, uv + o.zy) + texture2D( u_glow_texture, uv + o.xw) + texture2D( u_glow_texture, uv + o.zw);\n\t\t\treturn s * 0.25;\n\t\t}\n\t\tvoid main() {\n\t\t\tvec4 glow = sampleBox( v_coord );\n\t\t\t#ifdef USE_DIRT\n\t\t\t\tglow = mix( glow, glow * texture2D( u_dirt_texture, v_coord ), u_dirt_factor );\n\t\t\t#endif\n\t\t\tgl_FragColor = texture2D( u_texture, v_coord ) + u_intensity * glow;\n\t\t}"; -h.registerNodeType("texture/glow",x);var z=function(){this.addInput("Texture","Texture");this.addOutput("Filtered","Texture");this.properties={intensity:1,radius:5}};z.title="Kuwahara Filter";z.desc="Filters a texture giving an artistic oil canvas painting";z.max_radius=10;z._shaders=[];z.prototype.onExecute=function(){var a=this.getInputData(0);if(a&&this.isOutputConnected(0)){var b=this._temp_texture;b&&b.width==a.width&&b.height==a.height&&b.type==a.type||(this._temp_texture=new GL.Texture(a.width, -a.height,{type:a.type,format:gl.RGBA,filter:gl.LINEAR}));b=this.properties.radius;b=Math.min(Math.floor(b),z.max_radius);if(0==b)this.setOutputData(0,a);else{var c=this.properties.intensity,d=h.camera_aspect;d||void 0===window.gl||(d=gl.canvas.height/gl.canvas.width);d||(d=1);d=this.properties.preserve_aspect?d:1;z._shaders[b]||(z._shaders[b]=new GL.Shader(Shader.SCREEN_VERTEX_SHADER,z.pixel_shader,{RADIUS:b.toFixed(0)}));var f=z._shaders[b],e=GL.Mesh.getScreenQuad();a.bind(0);this._temp_texture.drawTo(function(){f.uniforms({u_texture:0, -u_intensity:c,u_resolution:[a.width,a.height],u_iResolution:[1/a.width,1/a.height]}).draw(e)});this.setOutputData(0,this._temp_texture)}}};z.pixel_shader="\n\tprecision highp float;\n\tvarying vec2 v_coord;\n\tuniform sampler2D u_texture;\n\tuniform float u_intensity;\n\tuniform vec2 u_resolution;\n\tuniform vec2 u_iResolution;\n\t#ifndef RADIUS\n\t\t#define RADIUS 7\n\t#endif\n\tvoid main() {\n\t\n\t\tconst int radius = RADIUS;\n\t\tvec2 fragCoord = v_coord;\n\t\tvec2 src_size = u_iResolution;\n\t\tvec2 uv = v_coord;\n\t\tfloat n = float((radius + 1) * (radius + 1));\n\t\tint i;\n\t\tint j;\n\t\tvec3 m0 = vec3(0.0); vec3 m1 = vec3(0.0); vec3 m2 = vec3(0.0); vec3 m3 = vec3(0.0);\n\t\tvec3 s0 = vec3(0.0); vec3 s1 = vec3(0.0); vec3 s2 = vec3(0.0); vec3 s3 = vec3(0.0);\n\t\tvec3 c;\n\t\t\n\t\tfor (int j = -radius; j <= 0; ++j) {\n\t\t\tfor (int i = -radius; i <= 0; ++i) {\n\t\t\t\tc = texture2D(u_texture, uv + vec2(i,j) * src_size).rgb;\n\t\t\t\tm0 += c;\n\t\t\t\ts0 += c * c;\n\t\t\t}\n\t\t}\n\t\t\n\t\tfor (int j = -radius; j <= 0; ++j) {\n\t\t\tfor (int i = 0; i <= radius; ++i) {\n\t\t\t\tc = texture2D(u_texture, uv + vec2(i,j) * src_size).rgb;\n\t\t\t\tm1 += c;\n\t\t\t\ts1 += c * c;\n\t\t\t}\n\t\t}\n\t\t\n\t\tfor (int j = 0; j <= radius; ++j) {\n\t\t\tfor (int i = 0; i <= radius; ++i) {\n\t\t\t\tc = texture2D(u_texture, uv + vec2(i,j) * src_size).rgb;\n\t\t\t\tm2 += c;\n\t\t\t\ts2 += c * c;\n\t\t\t}\n\t\t}\n\t\t\n\t\tfor (int j = 0; j <= radius; ++j) {\n\t\t\tfor (int i = -radius; i <= 0; ++i) {\n\t\t\t\tc = texture2D(u_texture, uv + vec2(i,j) * src_size).rgb;\n\t\t\t\tm3 += c;\n\t\t\t\ts3 += c * c;\n\t\t\t}\n\t\t}\n\t\t\n\t\tfloat min_sigma2 = 1e+2;\n\t\tm0 /= n;\n\t\ts0 = abs(s0 / n - m0 * m0);\n\t\t\n\t\tfloat sigma2 = s0.r + s0.g + s0.b;\n\t\tif (sigma2 < min_sigma2) {\n\t\t\tmin_sigma2 = sigma2;\n\t\t\tgl_FragColor = vec4(m0, 1.0);\n\t\t}\n\t\t\n\t\tm1 /= n;\n\t\ts1 = abs(s1 / n - m1 * m1);\n\t\t\n\t\tsigma2 = s1.r + s1.g + s1.b;\n\t\tif (sigma2 < min_sigma2) {\n\t\t\tmin_sigma2 = sigma2;\n\t\t\tgl_FragColor = vec4(m1, 1.0);\n\t\t}\n\t\t\n\t\tm2 /= n;\n\t\ts2 = abs(s2 / n - m2 * m2);\n\t\t\n\t\tsigma2 = s2.r + s2.g + s2.b;\n\t\tif (sigma2 < min_sigma2) {\n\t\t\tmin_sigma2 = sigma2;\n\t\t\tgl_FragColor = vec4(m2, 1.0);\n\t\t}\n\t\t\n\t\tm3 /= n;\n\t\ts3 = abs(s3 / n - m3 * m3);\n\t\t\n\t\tsigma2 = s3.r + s3.g + s3.b;\n\t\tif (sigma2 < min_sigma2) {\n\t\t\tmin_sigma2 = sigma2;\n\t\t\tgl_FragColor = vec4(m3, 1.0);\n\t\t}\n\t}\n\t"; -h.registerNodeType("texture/kuwahara",z);d=function(){this.addOutput("Webcam","Texture");this.properties={texture_name:""}};d.title="Webcam";d.desc="Webcam texture";d.prototype.openStream=function(){function a(c){console.log("Webcam rejected",c);b._webcam_stream=!1;b.box_color="red"}navigator.getUserMedia=navigator.getUserMedia||navigator.webkitGetUserMedia||navigator.mozGetUserMedia||navigator.msGetUserMedia;window.URL=window.URL||window.webkitURL;if(navigator.getUserMedia){this._waiting_confirmation= +h.registerNodeType("texture/glow",x);var y=function(){this.addInput("Texture","Texture");this.addOutput("Filtered","Texture");this.properties={intensity:1,radius:5}};y.title="Kuwahara Filter";y.desc="Filters a texture giving an artistic oil canvas painting";y.max_radius=10;y._shaders=[];y.prototype.onExecute=function(){var a=this.getInputData(0);if(a&&this.isOutputConnected(0)){var b=this._temp_texture;b&&b.width==a.width&&b.height==a.height&&b.type==a.type||(this._temp_texture=new GL.Texture(a.width, +a.height,{type:a.type,format:gl.RGBA,filter:gl.LINEAR}));b=this.properties.radius;b=Math.min(Math.floor(b),y.max_radius);if(0==b)this.setOutputData(0,a);else{var c=this.properties.intensity,d=h.camera_aspect;d||void 0===window.gl||(d=gl.canvas.height/gl.canvas.width);d||(d=1);d=this.properties.preserve_aspect?d:1;y._shaders[b]||(y._shaders[b]=new GL.Shader(Shader.SCREEN_VERTEX_SHADER,y.pixel_shader,{RADIUS:b.toFixed(0)}));var f=y._shaders[b],k=GL.Mesh.getScreenQuad();a.bind(0);this._temp_texture.drawTo(function(){f.uniforms({u_texture:0, +u_intensity:c,u_resolution:[a.width,a.height],u_iResolution:[1/a.width,1/a.height]}).draw(k)});this.setOutputData(0,this._temp_texture)}}};y.pixel_shader="\n\tprecision highp float;\n\tvarying vec2 v_coord;\n\tuniform sampler2D u_texture;\n\tuniform float u_intensity;\n\tuniform vec2 u_resolution;\n\tuniform vec2 u_iResolution;\n\t#ifndef RADIUS\n\t\t#define RADIUS 7\n\t#endif\n\tvoid main() {\n\t\n\t\tconst int radius = RADIUS;\n\t\tvec2 fragCoord = v_coord;\n\t\tvec2 src_size = u_iResolution;\n\t\tvec2 uv = v_coord;\n\t\tfloat n = float((radius + 1) * (radius + 1));\n\t\tint i;\n\t\tint j;\n\t\tvec3 m0 = vec3(0.0); vec3 m1 = vec3(0.0); vec3 m2 = vec3(0.0); vec3 m3 = vec3(0.0);\n\t\tvec3 s0 = vec3(0.0); vec3 s1 = vec3(0.0); vec3 s2 = vec3(0.0); vec3 s3 = vec3(0.0);\n\t\tvec3 c;\n\t\t\n\t\tfor (int j = -radius; j <= 0; ++j) {\n\t\t\tfor (int i = -radius; i <= 0; ++i) {\n\t\t\t\tc = texture2D(u_texture, uv + vec2(i,j) * src_size).rgb;\n\t\t\t\tm0 += c;\n\t\t\t\ts0 += c * c;\n\t\t\t}\n\t\t}\n\t\t\n\t\tfor (int j = -radius; j <= 0; ++j) {\n\t\t\tfor (int i = 0; i <= radius; ++i) {\n\t\t\t\tc = texture2D(u_texture, uv + vec2(i,j) * src_size).rgb;\n\t\t\t\tm1 += c;\n\t\t\t\ts1 += c * c;\n\t\t\t}\n\t\t}\n\t\t\n\t\tfor (int j = 0; j <= radius; ++j) {\n\t\t\tfor (int i = 0; i <= radius; ++i) {\n\t\t\t\tc = texture2D(u_texture, uv + vec2(i,j) * src_size).rgb;\n\t\t\t\tm2 += c;\n\t\t\t\ts2 += c * c;\n\t\t\t}\n\t\t}\n\t\t\n\t\tfor (int j = 0; j <= radius; ++j) {\n\t\t\tfor (int i = -radius; i <= 0; ++i) {\n\t\t\t\tc = texture2D(u_texture, uv + vec2(i,j) * src_size).rgb;\n\t\t\t\tm3 += c;\n\t\t\t\ts3 += c * c;\n\t\t\t}\n\t\t}\n\t\t\n\t\tfloat min_sigma2 = 1e+2;\n\t\tm0 /= n;\n\t\ts0 = abs(s0 / n - m0 * m0);\n\t\t\n\t\tfloat sigma2 = s0.r + s0.g + s0.b;\n\t\tif (sigma2 < min_sigma2) {\n\t\t\tmin_sigma2 = sigma2;\n\t\t\tgl_FragColor = vec4(m0, 1.0);\n\t\t}\n\t\t\n\t\tm1 /= n;\n\t\ts1 = abs(s1 / n - m1 * m1);\n\t\t\n\t\tsigma2 = s1.r + s1.g + s1.b;\n\t\tif (sigma2 < min_sigma2) {\n\t\t\tmin_sigma2 = sigma2;\n\t\t\tgl_FragColor = vec4(m1, 1.0);\n\t\t}\n\t\t\n\t\tm2 /= n;\n\t\ts2 = abs(s2 / n - m2 * m2);\n\t\t\n\t\tsigma2 = s2.r + s2.g + s2.b;\n\t\tif (sigma2 < min_sigma2) {\n\t\t\tmin_sigma2 = sigma2;\n\t\t\tgl_FragColor = vec4(m2, 1.0);\n\t\t}\n\t\t\n\t\tm3 /= n;\n\t\ts3 = abs(s3 / n - m3 * m3);\n\t\t\n\t\tsigma2 = s3.r + s3.g + s3.b;\n\t\tif (sigma2 < min_sigma2) {\n\t\t\tmin_sigma2 = sigma2;\n\t\t\tgl_FragColor = vec4(m3, 1.0);\n\t\t}\n\t}\n\t"; +h.registerNodeType("texture/kuwahara",y);d=function(){this.addOutput("Webcam","Texture");this.properties={texture_name:""}};d.title="Webcam";d.desc="Webcam texture";d.prototype.openStream=function(){function a(c){console.log("Webcam rejected",c);b._webcam_stream=!1;b.box_color="red"}navigator.getUserMedia=navigator.getUserMedia||navigator.webkitGetUserMedia||navigator.mozGetUserMedia||navigator.msGetUserMedia;window.URL=window.URL||window.webkitURL;if(navigator.getUserMedia){this._waiting_confirmation= !0;var b=this;navigator.getUserMedia({video:!0},this.streamReady.bind(this),a)}};d.prototype.streamReady=function(a){this._webcam_stream=a;var b=this._video;b||(b=document.createElement("video"),b.autoplay=!0,b.src=window.URL.createObjectURL(a),this._video=b,b.onloadedmetadata=function(a){console.log(a)})};d.prototype.onRemoved=function(){if(this._webcam_stream){var a=this._webcam_stream.getVideoTracks();a.length&&(a=a[0],a.stop&&a.stop());this._video=this._webcam_stream=null}};d.prototype.onDrawBackground= function(a){this.flags.collapsed||20>=this.size[1]||!this._video||(a.save(),a.webgl?this._temp_texture&&a.drawImage(this._temp_texture,0,0,this.size[0],this.size[1]):(a.translate(0,this.size[1]),a.scale(1,-1),a.drawImage(this._video,0,0,this.size[0],this.size[1])),a.restore())};d.prototype.onExecute=function(){null!=this._webcam_stream||this._waiting_confirmation||this.openStream();if(this._video&&this._video.videoWidth){var a=this._video.videoWidth,b=this._video.videoHeight,c=this._temp_texture; c&&c.width==a&&c.height==b||(this._temp_texture=new GL.Texture(a,b,{format:gl.RGB,filter:gl.LINEAR}));this._temp_texture.uploadImage(this._video);this.properties.texture_name&&(e.getTexturesContainer()[this.properties.texture_name]=this._temp_texture);this.setOutputData(0,this._temp_texture)}};h.registerNodeType("texture/webcam",d);var A=function(){this.addInput("in","Texture");this.addInput("f","number");this.addOutput("out","Texture");this.properties={enabled:!0,factor:1,precision:e.LOW};this._uniforms= @@ -384,59 +387,59 @@ precision:0,step:1},octaves:{type:"Number",precision:0,step:1,min:1,max:50}};v.p this.properties.scale+this.properties.seed+this.properties.offset[0]+this.properties.offset[1]+this.properties.amplitude;if(c!=this._key){this._key=c;var f=this._uniforms;f.u_persistence=this.properties.persistence;f.u_octaves=this.properties.octaves;f.u_offset[0]=this.properties.offset[0];f.u_offset[1]=this.properties.offset[1];f.u_scale=this.properties.scale;f.u_amplitude=this.properties.amplitude;f.u_viewport[0]=a;f.u_viewport[1]=b;f.u_seed=128*this.properties.seed;var k=v._shader;k||(k=v._shader= new GL.Shader(GL.Shader.SCREEN_VERTEX_SHADER,v.pixel_shader));gl.disable(gl.BLEND);gl.disable(gl.DEPTH_TEST);d.drawTo(function(){k.uniforms(f).draw(GL.Mesh.getScreenQuad())})}this.setOutputData(0,d)}};v.pixel_shader="precision highp float;\n\t\t\tvarying vec2 v_coord;\n\t\t\tuniform vec2 u_offset;\n\t\t\tuniform float u_scale;\n\t\t\tuniform float u_persistence;\n\t\t\tuniform int u_octaves;\n\t\t\tuniform float u_amplitude;\n\t\t\tuniform vec2 u_viewport;\n\t\t\tuniform float u_seed;\n\t\t\t#define M_PI 3.14159265358979323846\n\t\t\t\n\t\t\tfloat rand(vec2 c){\treturn fract(sin(dot(c.xy ,vec2( 12.9898 + u_seed,78.233 + u_seed))) * 43758.5453); }\n\t\t\t\n\t\t\tfloat noise(vec2 p, float freq ){\n\t\t\t\tfloat unit = u_viewport.x/freq;\n\t\t\t\tvec2 ij = floor(p/unit);\n\t\t\t\tvec2 xy = mod(p,unit)/unit;\n\t\t\t\t//xy = 3.*xy*xy-2.*xy*xy*xy;\n\t\t\t\txy = .5*(1.-cos(M_PI*xy));\n\t\t\t\tfloat a = rand((ij+vec2(0.,0.)));\n\t\t\t\tfloat b = rand((ij+vec2(1.,0.)));\n\t\t\t\tfloat c = rand((ij+vec2(0.,1.)));\n\t\t\t\tfloat d = rand((ij+vec2(1.,1.)));\n\t\t\t\tfloat x1 = mix(a, b, xy.x);\n\t\t\t\tfloat x2 = mix(c, d, xy.x);\n\t\t\t\treturn mix(x1, x2, xy.y);\n\t\t\t}\n\t\t\t\n\t\t\tfloat pNoise(vec2 p, int res){\n\t\t\t\tfloat persistance = u_persistence;\n\t\t\t\tfloat n = 0.;\n\t\t\t\tfloat normK = 0.;\n\t\t\t\tfloat f = 4.;\n\t\t\t\tfloat amp = 1.0;\n\t\t\t\tint iCount = 0;\n\t\t\t\tfor (int i = 0; i<50; i++){\n\t\t\t\t\tn+=amp*noise(p, f);\n\t\t\t\t\tf*=2.;\n\t\t\t\t\tnormK+=amp;\n\t\t\t\t\tamp*=persistance;\n\t\t\t\t\tif (iCount >= res)\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tiCount++;\n\t\t\t\t}\n\t\t\t\tfloat nf = n/normK;\n\t\t\t\treturn nf*nf*nf*nf;\n\t\t\t}\n\t\t\tvoid main() {\n\t\t\t\tvec2 uv = v_coord * u_scale * u_viewport + u_offset * u_scale;\n\t\t\t\tvec4 color = vec4( pNoise( uv, u_octaves ) * u_amplitude );\n\t\t\t\tgl_FragColor = color;\n\t\t\t}"; h.registerNodeType("texture/perlin",v);d=function(){this.addOutput("out","Texture");this.properties={code:"",width:512,height:512,precision:e.DEFAULT};this._temp_texture=this._func=null};d.title="Canvas2D";d.desc="Executes Canvas2D code inside a texture or the viewport";d.widgets_info={precision:{widget:"combo",values:e.MODE_VALUES},code:{type:"code"},width:{type:"Number",precision:0,step:1},height:{type:"Number",precision:0,step:1}};d.prototype.onPropertyChanged=function(a,b){if("code"==a&&h.allow_scripts){this._func= -null;try{this._func=new Function("canvas","ctx","time","script",b),this.boxcolor="#00FF00"}catch(c){this.boxcolor="#FF0000",console.error("Error parsing script"),console.error(c)}}};d.prototype.onExecute=function(){var a=this._func;if(a&&this.isOutputConnected(0))if(t.enableWebGLCanvas){var b=this.properties.width||gl.canvas.width,c=this.properties.height||gl.canvas.height,d=this._temp_texture;d&&d.width==b&&d.height==c||(d=this._temp_texture=new GL.Texture(b,c,{format:gl.RGBA,filter:gl.LINEAR})); -var f=this,e=this.graph.getTime();d.drawTo(function(){gl.start2D();try{a.draw?a.draw.call(f,gl.canvas,gl,e,a):a.call(f,gl.canvas,gl,e,a),f.boxcolor="#00FF00"}catch(b){f.boxcolor="#FF0000",console.error("Error executing script"),console.error(b)}gl.finish2D()});this.setOutputData(0,d)}else console.warn("cannot use LGraphTextureCanvas2D if Canvas2DtoWebGL is not included")};h.registerNodeType("texture/canvas2D",d);var y=function(){this.addInput("in","Texture");this.addOutput("out","Texture");this.properties= -{key_color:vec3.fromValues(0,1,0),threshold:0.8,slope:0.2,precision:e.DEFAULT}};y.title="Matte";y.desc="Extracts background";y.widgets_info={key_color:{widget:"color"},precision:{widget:"combo",values:e.MODE_VALUES}};y.prototype.onExecute=function(){if(this.isOutputConnected(0)){var a=this.getInputData(0);if(this.properties.precision===e.PASS_THROUGH)this.setOutputData(0,a);else if(a){this._tex=e.getTargetTexture(a,this._tex,this.properties.precision);gl.disable(gl.BLEND);gl.disable(gl.DEPTH_TEST); -this._uniforms||(this._uniforms={u_texture:0,u_key_color:this.properties.key_color,u_threshold:1,u_slope:1});var b=this._uniforms,c=Mesh.getScreenQuad(),d=y._shader;d||(d=y._shader=new GL.Shader(GL.Shader.SCREEN_VERTEX_SHADER,y.pixel_shader));b.u_key_color=this.properties.key_color;b.u_threshold=this.properties.threshold;b.u_slope=this.properties.slope;this._tex.drawTo(function(){a.bind(0);d.uniforms(b).draw(c)});this.setOutputData(0,this._tex)}}};y.pixel_shader="precision highp float;\n\t\t\tvarying vec2 v_coord;\n\t\t\tuniform sampler2D u_texture;\n\t\t\tuniform vec3 u_key_color;\n\t\t\tuniform float u_threshold;\n\t\t\tuniform float u_slope;\n\t\t\t\n\t\t\tvoid main() {\n\t\t\t\tvec3 color = texture2D( u_texture, v_coord ).xyz;\n\t\t\t\tfloat diff = length( normalize(color) - normalize(u_key_color) );\n\t\t\t\tfloat edge = u_threshold * (1.0 - u_slope);\n\t\t\t\tfloat alpha = smoothstep( edge, u_threshold, diff);\n\t\t\t\tgl_FragColor = vec4( color, alpha );\n\t\t\t}"; -h.registerNodeType("texture/matte",y);d=function(){this.addOutput("Cubemap","Cubemap");this.properties={name:""};this.size=[e.image_preview_size,e.image_preview_size]};d.title="Cubemap";d.prototype.onDropFile=function(a,b,c){a?(this._drop_texture="string"==typeof a?GL.Texture.fromURL(a):GL.Texture.fromDDSInMemory(a),this.properties.name=b):(this._drop_texture=null,this.properties.name="")};d.prototype.onExecute=function(){if(this._drop_texture)this.setOutputData(0,this._drop_texture);else if(this.properties.name){var a= +null;try{this._func=new Function("canvas","ctx","time","script",b),this.boxcolor="#00FF00"}catch(c){this.boxcolor="#FF0000",console.error("Error parsing script"),console.error(c)}}};d.prototype.onExecute=function(){var a=this._func;if(a&&this.isOutputConnected(0))if(s.enableWebGLCanvas){var b=this.properties.width||gl.canvas.width,c=this.properties.height||gl.canvas.height,d=this._temp_texture;d&&d.width==b&&d.height==c||(d=this._temp_texture=new GL.Texture(b,c,{format:gl.RGBA,filter:gl.LINEAR})); +var f=this,k=this.graph.getTime();d.drawTo(function(){gl.start2D();try{a.draw?a.draw.call(f,gl.canvas,gl,k,a):a.call(f,gl.canvas,gl,k,a),f.boxcolor="#00FF00"}catch(b){f.boxcolor="#FF0000",console.error("Error executing script"),console.error(b)}gl.finish2D()});this.setOutputData(0,d)}else console.warn("cannot use LGraphTextureCanvas2D if Canvas2DtoWebGL is not included")};h.registerNodeType("texture/canvas2D",d);var z=function(){this.addInput("in","Texture");this.addOutput("out","Texture");this.properties= +{key_color:vec3.fromValues(0,1,0),threshold:0.8,slope:0.2,precision:e.DEFAULT}};z.title="Matte";z.desc="Extracts background";z.widgets_info={key_color:{widget:"color"},precision:{widget:"combo",values:e.MODE_VALUES}};z.prototype.onExecute=function(){if(this.isOutputConnected(0)){var a=this.getInputData(0);if(this.properties.precision===e.PASS_THROUGH)this.setOutputData(0,a);else if(a){this._tex=e.getTargetTexture(a,this._tex,this.properties.precision);gl.disable(gl.BLEND);gl.disable(gl.DEPTH_TEST); +this._uniforms||(this._uniforms={u_texture:0,u_key_color:this.properties.key_color,u_threshold:1,u_slope:1});var b=this._uniforms,c=Mesh.getScreenQuad(),d=z._shader;d||(d=z._shader=new GL.Shader(GL.Shader.SCREEN_VERTEX_SHADER,z.pixel_shader));b.u_key_color=this.properties.key_color;b.u_threshold=this.properties.threshold;b.u_slope=this.properties.slope;this._tex.drawTo(function(){a.bind(0);d.uniforms(b).draw(c)});this.setOutputData(0,this._tex)}}};z.pixel_shader="precision highp float;\n\t\t\tvarying vec2 v_coord;\n\t\t\tuniform sampler2D u_texture;\n\t\t\tuniform vec3 u_key_color;\n\t\t\tuniform float u_threshold;\n\t\t\tuniform float u_slope;\n\t\t\t\n\t\t\tvoid main() {\n\t\t\t\tvec3 color = texture2D( u_texture, v_coord ).xyz;\n\t\t\t\tfloat diff = length( normalize(color) - normalize(u_key_color) );\n\t\t\t\tfloat edge = u_threshold * (1.0 - u_slope);\n\t\t\t\tfloat alpha = smoothstep( edge, u_threshold, diff);\n\t\t\t\tgl_FragColor = vec4( color, alpha );\n\t\t\t}"; +h.registerNodeType("texture/matte",z);d=function(){this.addOutput("Cubemap","Cubemap");this.properties={name:""};this.size=[e.image_preview_size,e.image_preview_size]};d.title="Cubemap";d.prototype.onDropFile=function(a,b,c){a?(this._drop_texture="string"==typeof a?GL.Texture.fromURL(a):GL.Texture.fromDDSInMemory(a),this.properties.name=b):(this._drop_texture=null,this.properties.name="")};d.prototype.onExecute=function(){if(this._drop_texture)this.setOutputData(0,this._drop_texture);else if(this.properties.name){var a= e.getTexture(this.properties.name);a&&(this._last_tex=a,this.setOutputData(0,a))}};d.prototype.onDrawBackground=function(a){this.flags.collapsed||20>=this.size[1]||a.webgl&&(gl.meshes.cube||(gl.meshes.cube=GL.Mesh.cube({size:1})))};h.registerNodeType("texture/cubemap",d)}})(this); -(function(t){var h=t.LiteGraph;if("undefined"!=typeof GL){var e=function(){this.addInput("Texture","Texture");this.addInput("Aberration","number");this.addInput("Distortion","number");this.addInput("Blur","number");this.addOutput("Texture","Texture");this.properties={aberration:1,distortion:1,blur:1,precision:LGraphTexture.DEFAULT};e._shader||(e._shader=new GL.Shader(GL.Shader.SCREEN_VERTEX_SHADER,e.pixel_shader),e._texture=new GL.Texture(3,1,{format:gl.RGB,wrap:gl.CLAMP_TO_EDGE,magFilter:gl.LINEAR, +(function(s){var h=s.LiteGraph;if("undefined"!=typeof GL){var e=function(){this.addInput("Texture","Texture");this.addInput("Aberration","number");this.addInput("Distortion","number");this.addInput("Blur","number");this.addOutput("Texture","Texture");this.properties={aberration:1,distortion:1,blur:1,precision:LGraphTexture.DEFAULT};e._shader||(e._shader=new GL.Shader(GL.Shader.SCREEN_VERTEX_SHADER,e.pixel_shader),e._texture=new GL.Texture(3,1,{format:gl.RGB,wrap:gl.CLAMP_TO_EDGE,magFilter:gl.LINEAR, minFilter:gl.LINEAR,pixel_data:[255,0,0,0,255,0,0,0,255]}))};e.title="Lens";e.desc="Camera Lens distortion";e.widgets_info={precision:{widget:"combo",values:LGraphTexture.MODE_VALUES}};e.prototype.onExecute=function(){var d=this.getInputData(0);if(this.properties.precision===LGraphTexture.PASS_THROUGH)this.setOutputData(0,d);else if(d){this._tex=LGraphTexture.getTargetTexture(d,this._tex,this.properties.precision);var h=this.properties.aberration;this.isInputConnected(1)&&(h=this.getInputData(1), -this.properties.aberration=h);var m=this.properties.distortion;this.isInputConnected(2)&&(m=this.getInputData(2),this.properties.distortion=m);var l=this.properties.blur;this.isInputConnected(3)&&(l=this.getInputData(3),this.properties.blur=l);gl.disable(gl.BLEND);gl.disable(gl.DEPTH_TEST);var n=Mesh.getScreenQuad(),b=e._shader;this._tex.drawTo(function(){d.bind(0);b.uniforms({u_texture:0,u_aberration:h,u_distortion:m,u_blur:l}).draw(n)});this.setOutputData(0,this._tex)}};e.pixel_shader="precision highp float;\n\t\t\tprecision highp float;\n\t\t\tvarying vec2 v_coord;\n\t\t\tuniform sampler2D u_texture;\n\t\t\tuniform vec2 u_camera_planes;\n\t\t\tuniform float u_aberration;\n\t\t\tuniform float u_distortion;\n\t\t\tuniform float u_blur;\n\t\t\t\n\t\t\tvoid main() {\n\t\t\t\tvec2 coord = v_coord;\n\t\t\t\tfloat dist = distance(vec2(0.5), coord);\n\t\t\t\tvec2 dist_coord = coord - vec2(0.5);\n\t\t\t\tfloat percent = 1.0 + ((0.5 - dist) / 0.5) * u_distortion;\n\t\t\t\tdist_coord *= percent;\n\t\t\t\tcoord = dist_coord + vec2(0.5);\n\t\t\t\tvec4 color = texture2D(u_texture,coord, u_blur * dist);\n\t\t\t\tcolor.r = texture2D(u_texture,vec2(0.5) + dist_coord * (1.0+0.01*u_aberration), u_blur * dist ).r;\n\t\t\t\tcolor.b = texture2D(u_texture,vec2(0.5) + dist_coord * (1.0-0.01*u_aberration), u_blur * dist ).b;\n\t\t\t\tgl_FragColor = color;\n\t\t\t}\n\t\t\t"; -h.registerNodeType("fx/lens",e);t.LGraphFXLens=e;var m=function(){this.addInput("Texture","Texture");this.addInput("Blurred","Texture");this.addInput("Mask","Texture");this.addInput("Threshold","number");this.addOutput("Texture","Texture");this.properties={shape:"",size:10,alpha:1,threshold:1,high_precision:!1}};m.title="Bokeh";m.desc="applies an Bokeh effect";m.widgets_info={shape:{widget:"texture"}};m.prototype.onExecute=function(){var d=this.getInputData(0),e=this.getInputData(1),h=this.getInputData(2); -if(d&&h&&this.properties.shape){e||(e=d);var l=LGraphTexture.getTexture(this.properties.shape);if(l){var n=this.properties.threshold;this.isInputConnected(3)&&(n=this.getInputData(3),this.properties.threshold=n);var b=gl.UNSIGNED_BYTE;this.properties.high_precision&&(b=gl.half_float_ext?gl.HALF_FLOAT_OES:gl.FLOAT);this._temp_texture&&this._temp_texture.type==b&&this._temp_texture.width==d.width&&this._temp_texture.height==d.height||(this._temp_texture=new GL.Texture(d.width,d.height,{type:b,format:gl.RGBA, -filter:gl.LINEAR}));var a=m._first_shader;a||(a=m._first_shader=new GL.Shader(Shader.SCREEN_VERTEX_SHADER,m._first_pixel_shader));var c=m._second_shader;c||(c=m._second_shader=new GL.Shader(m._second_vertex_shader,m._second_pixel_shader));var g=this._points_mesh;g&&g._width==d.width&&g._height==d.height&&2==g._spacing||(g=this.createPointsMesh(d.width,d.height,2));var r=Mesh.getScreenQuad(),f=this.properties.size,k=this.properties.alpha;gl.disable(gl.DEPTH_TEST);gl.disable(gl.BLEND);this._temp_texture.drawTo(function(){d.bind(0); -e.bind(1);h.bind(2);a.uniforms({u_texture:0,u_texture_blur:1,u_mask:2,u_texsize:[d.width,d.height]}).draw(r)});this._temp_texture.drawTo(function(){gl.enable(gl.BLEND);gl.blendFunc(gl.ONE,gl.ONE);d.bind(0);l.bind(3);c.uniforms({u_texture:0,u_mask:2,u_shape:3,u_alpha:k,u_threshold:n,u_pointSize:f,u_itexsize:[1/d.width,1/d.height]}).draw(g,gl.POINTS)});this.setOutputData(0,this._temp_texture)}}else this.setOutputData(0,d)};m.prototype.createPointsMesh=function(d,e,h){for(var l=Math.round(d/h),n=Math.round(e/ -h),b=new Float32Array(l*n*2),a=-1,c=2/d*h,g=2/e*h,m=0;m=h.NOTEON||b<=h.NOTEOFF)this.channel=d&15};Object.defineProperty(h.prototype,"velocity",{get:function(){return this.cmd==h.NOTEON?this.data[2]: --1},set:function(d){this.data[2]=d},enumerable:!0});h.notes="A A# B C C# D D# E F F# G G#".split(" ");h.prototype.getPitch=function(){return 440*Math.pow(2,(this.data[1]-69)/12)};h.computePitch=function(d){return 440*Math.pow(2,(d-69)/12)};h.prototype.getCC=function(){return this.data[1]};h.prototype.getCCValue=function(){return this.data[2]};h.prototype.getPitchBend=function(){return this.data[1]+(this.data[2]<<7)-8192};h.computePitchBend=function(d,b){return d+(b<<7)-8192};h.prototype.setCommandFromString= +var e=this.properties.intensity;this.isInputConnected(1)&&(e=this.getInputData(1),this.properties.intensity=e);gl.disable(gl.BLEND);gl.disable(gl.DEPTH_TEST);var h=Mesh.getScreenQuad(),l=r._shader,m=this.properties.invert;this._tex.drawTo(function(){d.bind(0);l.uniforms({u_texture:0,u_intensity:e,u_isize:[1/d.width,1/d.height],u_invert:m?1:0}).draw(h)});this.setOutputData(0,this._tex)}};r.pixel_shader="precision highp float;\n\t\t\tprecision highp float;\n\t\t\tvarying vec2 v_coord;\n\t\t\tuniform sampler2D u_texture;\n\t\t\tuniform float u_intensity;\n\t\t\tuniform int u_invert;\n\t\t\t\n\t\t\tvoid main() {\n\t\t\t\tfloat luminance = 1.0 - length( v_coord - vec2(0.5) ) * 1.414;\n\t\t\t\tvec4 color = texture2D(u_texture, v_coord);\n\t\t\t\tif(u_invert == 1)\n\t\t\t\t\tluminance = 1.0 - luminance;\n\t\t\t\tluminance = mix(1.0, luminance, u_intensity);\n\t\t\t gl_FragColor = vec4( luminance * color.xyz, color.a);\n\t\t\t}\n\t\t\t"; +h.registerNodeType("fx/vigneting",r);s.LGraphFXVigneting=r}})(this); +(function(s){function h(d){this.cmd=this.channel=0;d?this.setup(d):this.data=[0,0,0]}function e(d,c){navigator.requestMIDIAccess?(this.on_ready=d,this.state={note:[],cc:[]},navigator.requestMIDIAccess().then(this.onMIDISuccess.bind(this),this.onMIDIFailure.bind(this))):(this.error="not suppoorted",c?c("Not supported"):console.error("MIDI NOT SUPPORTED, enable by chrome://flags"))}function n(){this.addOutput("on_midi",l.EVENT);this.addOutput("out","midi");this.properties={port:0};this._current_midi_event= +this._last_midi_event=null;var d=this;new e(function(c){d._midi=c;if(d._waiting)d.onStart();d._waiting=!1})}function d(){this.addInput("send",l.EVENT);this.properties={port:0};var d=this;new e(function(c){d._midi=c})}function r(){this.addInput("on_midi",l.EVENT);this._str="";this.size=[200,40]}function q(){this.properties={channel:-1,cmd:-1,min_value:-1,max_value:-1};this.addInput("in",l.EVENT);this.addOutput("on_midi",l.EVENT)}function t(){this.properties={channel:0,cmd:"CC",value1:1,value2:1};this.addInput("send", +l.EVENT);this.addInput("assign",l.EVENT);this.addOutput("on_midi",l.EVENT)}function u(){this.properties={cc:1,value:0};this.addOutput("value","number")}var l=s.LiteGraph;h.prototype.setup=function(d){this.data=d;this.status=d=d[0];var c=d&240;this.cmd=240<=d?d:c;this.cmd==h.NOTEON&&0==this.velocity&&(this.cmd=h.NOTEOFF);this.cmd_str=h.commands[this.cmd]||"";if(c>=h.NOTEON||c<=h.NOTEOFF)this.channel=d&15};Object.defineProperty(h.prototype,"velocity",{get:function(){return this.cmd==h.NOTEON?this.data[2]: +-1},set:function(d){this.data[2]=d},enumerable:!0});h.notes="A A# B C C# D D# E F F# G G#".split(" ");h.prototype.getPitch=function(){return 440*Math.pow(2,(this.data[1]-69)/12)};h.computePitch=function(d){return 440*Math.pow(2,(d-69)/12)};h.prototype.getCC=function(){return this.data[1]};h.prototype.getCCValue=function(){return this.data[2]};h.prototype.getPitchBend=function(){return this.data[1]+(this.data[2]<<7)-8192};h.computePitchBend=function(d,c){return d+(c<<7)-8192};h.prototype.setCommandFromString= function(d){this.cmd=h.computeCommandFromString(d)};h.computeCommandFromString=function(d){if(!d)return 0;if(d&&d.constructor===Number)return d;d=d.toUpperCase();switch(d){case "NOTE ON":case "NOTEON":return h.NOTEON;case "NOTE OFF":case "NOTEOFF":return h.NOTEON;case "KEY PRESSURE":case "KEYPRESSURE":return h.KEYPRESSURE;case "CONTROLLER CHANGE":case "CONTROLLERCHANGE":case "CC":return h.CONTROLLERCHANGE;case "PROGRAM CHANGE":case "PROGRAMCHANGE":case "PC":return h.PROGRAMCHANGE;case "CHANNEL PRESSURE":case "CHANNELPRESSURE":return h.CHANNELPRESSURE; -case "PITCH BEND":case "PITCHBEND":return h.PITCHBEND;case "TIME TICK":case "TIMETICK":return h.TIMETICK;default:return Number(d)}};h.toNoteString=function(d){var b;b=(d-21)%12;0>b&&(b=12+b);return h.notes[b]+Math.floor((d-24)/12+1)};h.prototype.toString=function(){var d=""+this.channel+". ";switch(this.cmd){case h.NOTEON:d+="NOTEON "+h.toNoteString(this.data[1]);break;case h.NOTEOFF:d+="NOTEOFF "+h.toNoteString(this.data[1]);break;case h.CONTROLLERCHANGE:d+="CC "+this.data[1]+" "+this.data[2];break; -case h.PROGRAMCHANGE:d+="PC "+this.data[1];break;case h.PITCHBEND:d+="PITCHBEND "+this.getPitchBend();break;case h.KEYPRESSURE:d+="KEYPRESS "+this.data[1]}return d};h.prototype.toHexString=function(){for(var d="",b=0;bthis.properties.max_value||this.trigger("on_midi",b)};l.registerNodeType("midi/filter",q);s.title="MIDIEvent";s.desc="Create a MIDI Event";s.prototype.onAction=function(d,b){"assign"==d?(this.properties.channel=b.channel,this.properties.cmd=b.cmd,this.properties.value1= -b.data[1],this.properties.value2=b.data[2]):(b=new h,b.channel=this.properties.channel,this.properties.cmd&&this.properties.cmd.constructor===String?b.setCommandFromString(this.properties.cmd):b.cmd=this.properties.cmd,b.data[0]=b.cmd|b.channel,b.data[1]=Number(this.properties.value1),b.data[2]=Number(this.properties.value2),this.trigger("on_midi",b))};s.prototype.onExecute=function(){var d=this.properties;if(this.outputs)for(var b=0;bc&&(c=12+c);return h.notes[c]+Math.floor((d-24)/12+1)};h.prototype.toString=function(){var d=""+this.channel+". ";switch(this.cmd){case h.NOTEON:d+="NOTEON "+h.toNoteString(this.data[1]);break;case h.NOTEOFF:d+="NOTEOFF "+h.toNoteString(this.data[1]);break;case h.CONTROLLERCHANGE:d+="CC "+this.data[1]+" "+this.data[2];break; +case h.PROGRAMCHANGE:d+="PC "+this.data[1];break;case h.PITCHBEND:d+="PITCHBEND "+this.getPitchBend();break;case h.KEYPRESSURE:d+="KEYPRESS "+this.data[1]}return d};h.prototype.toHexString=function(){for(var d="",c=0;cthis.properties.max_value||this.trigger("on_midi",c)};l.registerNodeType("midi/filter",q);t.title="MIDIEvent";t.desc="Create a MIDI Event";t.prototype.onAction=function(d,c){"assign"==d?(this.properties.channel=c.channel,this.properties.cmd=c.cmd,this.properties.value1= +c.data[1],this.properties.value2=c.data[2]):(c=new h,c.channel=this.properties.channel,this.properties.cmd&&this.properties.cmd.constructor===String?c.setCommandFromString(this.properties.cmd):c.cmd=this.properties.cmd,c.data[0]=c.cmd|c.channel,c.data[1]=Number(this.properties.value1),c.data[2]=Number(this.properties.value2),this.trigger("on_midi",c))};t.prototype.onExecute=function(){var d=this.properties;if(this.outputs)for(var c=0;c=this.size[0]&&(e=this.size[0]-1),a.strokeStyle="red",a.beginPath(),a.moveTo(e,d),a.lineTo(e,0),a.stroke())}};b.title="Visualization";b.desc="Audio Visualization";w.registerNodeType("audio/visualization", -b);a.prototype.onExecute=function(){if(this._freqs=this.getInputData(0)){var a=this.properties.band,b=this.getInputData(1);void 0!==b&&(a=b);b=f.getAudioContext().sampleRate/this._freqs.length;b=a/b*2;b>=this._freqs.length?b=this._freqs[this._freqs.length-1]:(a=b|0,b-=a,b=this._freqs[a]*(1-b)+this._freqs[a+1]*b);this.setOutputData(0,b/255*this.properties.amplitude)}};a.prototype.onGetInputs=function(){return[["band","number"]]};a.title="Signal";a.desc="extract the signal of some frequency";w.registerNodeType("audio/signal", -a);c.prototype.onAdded=function(a){a.status==LGraph.STATUS_RUNNING&&(this.audionode.onaudioprocess=this._callback)};c["@code"]={widget:"code"};c.prototype.onStart=function(){this.audionode.onaudioprocess=this._callback};c.prototype.onStop=function(){this.audionode.onaudioprocess=c._bypass_function};c.prototype.onPause=function(){this.audionode.onaudioprocess=c._bypass_function};c.prototype.onUnpause=function(){this.audionode.onaudioprocess=this._callback};c.prototype.onExecute=function(){};c.prototype.onRemoved= -function(){this.audionode.onaudioprocess=c._bypass_function};c.prototype.processCode=function(){try{this._script=new new Function("properties",this.properties.code)(this.properties),this._old_code=this.properties.code,this._callback=this._script.onaudioprocess}catch(a){console.error("Error in onaudioprocess code",a),this._callback=c._bypass_function,this.audionode.onaudioprocess=this._callback}};c.prototype.onPropertyChanged=function(a,b){"code"==a&&(this.properties.code=b,this.processCode(),this.graph&& -this.graph.status==LGraph.STATUS_RUNNING&&(this.audionode.onaudioprocess=this._callback))};c.default_function=function(){this.onaudioprocess=function(a){var b=a.inputBuffer;a=a.outputBuffer;for(var c=0;c=this.size[0]&&(e=this.size[0]-1),a.strokeStyle="red",a.beginPath(),a.moveTo(e,d),a.lineTo(e,0),a.stroke())}};c.title="Visualization";c.desc="Audio Visualization";w.registerNodeType("audio/visualization", +c);a.prototype.onExecute=function(){if(this._freqs=this.getInputData(0)){var a=this.properties.band,b=this.getInputData(1);void 0!==b&&(a=b);b=f.getAudioContext().sampleRate/this._freqs.length;b=a/b*2;b>=this._freqs.length?b=this._freqs[this._freqs.length-1]:(a=b|0,b-=a,b=this._freqs[a]*(1-b)+this._freqs[a+1]*b);this.setOutputData(0,b/255*this.properties.amplitude)}};a.prototype.onGetInputs=function(){return[["band","number"]]};a.title="Signal";a.desc="extract the signal of some frequency";w.registerNodeType("audio/signal", +a);b.prototype.onAdded=function(a){a.status==LGraph.STATUS_RUNNING&&(this.audionode.onaudioprocess=this._callback)};b["@code"]={widget:"code"};b.prototype.onStart=function(){this.audionode.onaudioprocess=this._callback};b.prototype.onStop=function(){this.audionode.onaudioprocess=b._bypass_function};b.prototype.onPause=function(){this.audionode.onaudioprocess=b._bypass_function};b.prototype.onUnpause=function(){this.audionode.onaudioprocess=this._callback};b.prototype.onExecute=function(){};b.prototype.onRemoved= +function(){this.audionode.onaudioprocess=b._bypass_function};b.prototype.processCode=function(){try{this._script=new new Function("properties",this.properties.code)(this.properties),this._old_code=this.properties.code,this._callback=this._script.onaudioprocess}catch(a){console.error("Error in onaudioprocess code",a),this._callback=b._bypass_function,this.audionode.onaudioprocess=this._callback}};b.prototype.onPropertyChanged=function(a,b){"code"==a&&(this.properties.code=b,this.processCode(),this.graph&& +this.graph.status==LGraph.STATUS_RUNNING&&(this.audionode.onaudioprocess=this._callback))};b.default_function=function(){this.onaudioprocess=function(a){var b=a.inputBuffer;a=a.outputBuffer;for(var c=0;c - Defined in: ../src/litegraph.js:6207 + Defined in: ../src/litegraph.js:7219 @@ -123,7 +123,7 @@

Defined in - ../src/litegraph.js:6207 + ../src/litegraph.js:7219

diff --git a/doc/classes/LGraph.html b/doc/classes/LGraph.html index 225af2685..5118294fa 100644 --- a/doc/classes/LGraph.html +++ b/doc/classes/LGraph.html @@ -85,7 +85,7 @@
- Defined in: ../src/litegraph.js:378 + Defined in: ../src/litegraph.js:405
@@ -102,7 +102,13 @@

LGraph

- () +
+ (
    +
  • + o +
  • +
) +
@@ -114,7 +120,7 @@

Defined in - ../src/litegraph.js:378 + ../src/litegraph.js:405

@@ -125,6 +131,23 @@
+
+

Parameters:

+ +
    +
  • + o + Object + + +
    +

    data from previous serialization [optional]

    + +
    + +
  • +
+
@@ -197,6 +220,10 @@
  • findNodesByType +
  • +
  • + getAncestors +
  • getElapsedTime @@ -213,6 +240,10 @@
  • getGlobalOutputData +
  • +
  • + getGroupOnPos +
  • getNodeById @@ -321,7 +352,7 @@

    Defined in - ../src/litegraph.js:856 + ../src/litegraph.js:947

    @@ -381,7 +412,7 @@

    Defined in - ../src/litegraph.js:1065 + ../src/litegraph.js:1194

    @@ -461,7 +492,7 @@

    Defined in - ../src/litegraph.js:1190 + ../src/litegraph.js:1323

    @@ -528,7 +559,7 @@

    Defined in - ../src/litegraph.js:736 + ../src/litegraph.js:827

    @@ -565,7 +596,7 @@

    Defined in - ../src/litegraph.js:453 + ../src/litegraph.js:489

    @@ -621,7 +652,7 @@

    Defined in - ../src/litegraph.js:1150 + ../src/litegraph.js:1281

    @@ -687,7 +718,7 @@

    Defined in - ../src/litegraph.js:1272 + ../src/litegraph.js:1407

    @@ -744,7 +775,7 @@

    Defined in - ../src/litegraph.js:404 + ../src/litegraph.js:435

    @@ -768,6 +799,9 @@
  • str
  • +
  • + returns +
  • )
    @@ -781,7 +815,7 @@

    Defined in - ../src/litegraph.js:1403 + ../src/litegraph.js:1542

    @@ -808,6 +842,17 @@
    +
  • + returns + Boolean + + +
    +

    if there was any error parsing

    + +
    + +
  • @@ -835,7 +880,7 @@

    Defined in - ../src/litegraph.js:472 + ../src/litegraph.js:508

    @@ -891,7 +936,7 @@

    Defined in - ../src/litegraph.js:994 + ../src/litegraph.js:1105

    @@ -957,7 +1002,7 @@

    Defined in - ../src/litegraph.js:1027 + ../src/litegraph.js:1138

    @@ -1023,7 +1068,7 @@

    Defined in - ../src/litegraph.js:1010 + ../src/litegraph.js:1121

    @@ -1064,6 +1109,50 @@
    +
    +
    +

    getAncestors

    + + () + + + Array + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:790 +

    + + + +
    + +
    +

    Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively. +It doesnt include the node itself

    + +
    + + +
    +

    Returns:

    + +
    + Array: +

    an array with all the LGraphNodes that affect this node, in order of execution

    + +
    +
    + +

    getElapsedTime

    @@ -1083,7 +1172,7 @@

    Defined in - ../src/litegraph.js:801 + ../src/litegraph.js:892

    @@ -1127,7 +1216,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:790 + ../src/litegraph.js:881

    @@ -1176,7 +1265,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:1106 + ../src/litegraph.js:1236

    @@ -1241,7 +1330,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:1222 + ../src/litegraph.js:1356

    @@ -1281,6 +1370,86 @@ if the nodes are using graphical actions

    +
    +
    +

    getGroupOnPos

    + +
    + (
      +
    • + x +
    • +
    • + y +
    • +
    ) +
    + + + LGraphGroup + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:1174 +

    + + + +
    + +
    +

    Returns the top-most group in that position

    + +
    + +
    +

    Parameters:

    + +
      +
    • + x + Number + + +
      +

      the x coordinate in canvas space

      + +
      + +
    • +
    • + y + Number + + +
      +

      the y coordinate in canvas space

      + +
      + +
    • +
    +
    + +
    +

    Returns:

    + +
    + LGraphGroup: +

    the group or null

    + +
    +
    + +

    getNodeById

    @@ -1303,7 +1472,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:981 + ../src/litegraph.js:1092

    @@ -1353,7 +1522,7 @@ if the nodes are using graphical actions

    - Array + LGraphNode @@ -1365,7 +1534,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:1043 + ../src/litegraph.js:1154

    @@ -1421,8 +1590,8 @@ if the nodes are using graphical actions

    Returns:

    - Array: -

    a list with all the nodes that intersect this coordinate

    + LGraphNode: +

    the node at this position or null

    @@ -1453,7 +1622,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:1236 + ../src/litegraph.js:1370

    @@ -1512,7 +1681,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:780 + ../src/litegraph.js:871

    @@ -1552,7 +1721,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:1333 + ../src/litegraph.js:1471

    @@ -1589,7 +1758,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:912 + ../src/litegraph.js:1011

    @@ -1646,7 +1815,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:1169 + ../src/litegraph.js:1301

    @@ -1709,7 +1878,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:1291 + ../src/litegraph.js:1427

    @@ -1765,7 +1934,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:1120 + ../src/litegraph.js:1250

    @@ -1831,7 +2000,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:1245 + ../src/litegraph.js:1379

    @@ -1894,7 +2063,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:540 + ../src/litegraph.js:576

    @@ -1951,7 +2120,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:813 + ../src/litegraph.js:904

    @@ -2013,7 +2182,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:1370 + ../src/litegraph.js:1506

    @@ -2062,7 +2231,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:1083 + ../src/litegraph.js:1213

    @@ -2128,7 +2297,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:1208 + ../src/litegraph.js:1342

    @@ -2194,7 +2363,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:1097 + ../src/litegraph.js:1227

    @@ -2257,7 +2426,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:490 + ../src/litegraph.js:525

    @@ -2305,7 +2474,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:518 + ../src/litegraph.js:554

    @@ -2336,7 +2505,7 @@ if the nodes are using graphical actions

    Defined in - ../src/litegraph.js:619 + ../src/litegraph.js:658

    diff --git a/doc/classes/LGraphCanvas.html b/doc/classes/LGraphCanvas.html index 058a806be..6bf525ced 100644 --- a/doc/classes/LGraphCanvas.html +++ b/doc/classes/LGraphCanvas.html @@ -85,7 +85,7 @@ @@ -126,7 +126,7 @@

    Defined in - ../src/litegraph.js:3325 + ../src/litegraph.js:3624

    @@ -197,6 +197,22 @@

    Methods

    @@ -239,6 +383,130 @@

    Methods

    +
    +

    adjustMouseEvent

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4730 +

    + + + +
    + +
    +

    adds some useful properties to a mouse event, like the position in graph coordinates

    + +
    + + + + +
    +
    +

    bindEvents

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:3498 +

    + + + +
    + +
    +

    binds mouse, keyboard, touch and drag events to the canvas

    + +
    + + + + +
    +
    +

    bringToFront

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4816 +

    + + + +
    + +
    +

    brings a node to front (above all other nodes)

    + +
    + + + + +
    +
    +

    centerOnNode

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4719 +

    + + + +
    + +
    +

    centers the camera on a given node

    + +
    + + + + +

    clear

    @@ -254,7 +522,7 @@

    Defined in - ../src/litegraph.js:2974 + ../src/litegraph.js:3306

    @@ -291,7 +559,7 @@

    Defined in - ../src/litegraph.js:3071 + ../src/litegraph.js:3406

    @@ -323,6 +591,473 @@ +
    +
    +

    computeVisibleNodes

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4849 +

    + + + +
    + +
    +

    checks which nodes are visible (inside the camera area)

    + +
    + + + + +
    +
    +

    convertCanvasToOffset

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4798 +

    + + + +
    + +
    +

    converts a coordinate in graphcanvas space to canvas2D space (NAME IS CONFUSION, SHOULD BE THE OTHER WAY AROUND)

    + +
    + + + + +
    +
    +

    convertOffsetToCanvas

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4786 +

    + + + +
    + +
    +

    converts a coordinate in canvas2D space to graphcanvas space (NAME IS CONFUSION, SHOULD BE THE OTHER WAY AROUND)

    + +
    + + + + +
    +
    +

    deleteSelectedNodes

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4702 +

    + + + +
    + +
    +

    deletes all nodes in the current selection from the graph

    + +
    + + + + +
    +
    +

    deselectAllNodes

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4679 +

    + + + +
    + +
    +

    removes all nodes from the current selection

    + +
    + + + + +
    +
    +

    deselectNode

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4653 +

    + + + +
    + +
    +

    removes a node from the current selection

    + +
    + + + + +
    +
    +

    draw

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4874 +

    + + + +
    + +
    +

    renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes)

    + +
    + + + + +
    +
    +

    drawBackCanvas

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:5064 +

    + + + +
    + +
    +

    draws the back canvas (the one containing the background and the connections)

    + +
    + + + + +
    +
    +

    drawConnections

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:5639 +

    + + + +
    + +
    +

    draws every connection visible in the canvas +OPTIMIZE THIS: precatch connections position instead of recomputing them every time

    + +
    + + + + +
    +
    +

    drawFrontCanvas

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4905 +

    + + + +
    + +
    +

    draws the front canvas (the one containing all the nodes)

    + +
    + + + + +
    +
    +

    drawGroups

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:5990 +

    + + + +
    + +
    +

    draws every group area in the background

    + +
    + + + + +
    +
    +

    drawNode

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:5196 +

    + + + +
    + +
    +

    draws the given node inside the canvas

    + +
    + + + + +
    +
    +

    drawNodeShape

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:5467 +

    + + + +
    + +
    +

    draws the shape of the given node in the canvas

    + +
    + + + + +
    +
    +

    drawNodeWidgets

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:5815 +

    + + + +
    + +
    +

    draws the widgets stored inside a node

    + +
    + + + + +
    +
    +

    enableWebGL

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:3598 +

    + + + +
    + +
    +

    this function allows to render the canvas using WebGL instead of Canvas2D +this is useful if you plant to render 3D objects inside your nodes, it uses litegl.js for webgl and canvas2DtoWebGL to emulate the Canvas2D calls in webGL

    + +
    + + + +

    getCanvasWindow

    @@ -342,7 +1077,7 @@

    Defined in - ../src/litegraph.js:3341 + ../src/litegraph.js:3640

    @@ -366,6 +1101,68 @@
    +
    +
    +

    isOverNodeBox

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4296 +

    + + + +
    + +
    +

    retuns true if a position (in graph space) is on top of a node little corner box

    + +
    + + + + +
    +
    +

    isOverNodeInput

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4308 +

    + + + +
    + +
    +

    retuns true if a position (in graph space) is on top of a node input slot

    + +
    + + + +

    openSubgraph

    @@ -388,7 +1185,7 @@

    Defined in - ../src/litegraph.js:3044 + ../src/litegraph.js:3379

    @@ -419,6 +1216,378 @@ +
    +
    +

    processDrop

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4485 +

    + + + +
    + +
    +

    process a item drop event on top the canvas

    + +
    + + + + +
    +
    +

    processKey

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4332 +

    + + + +
    + +
    +

    process a key event

    + +
    + + + + +
    +
    +

    processMouseMove

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:3921 +

    + + + +
    + +
    +

    Called when a mouse move event has to be processed

    + +
    + + + + +
    +
    +

    processMouseUp

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4112 +

    + + + +
    + +
    +

    Called when a mouse up event has to be processed

    + +
    + + + + +
    +
    +

    processMouseWheel

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4263 +

    + + + +
    + +
    +

    Called when a mouse wheel event has to be processed

    + +
    + + + + +
    +
    +

    processNodeWidgets

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:5911 +

    + + + +
    + +
    +

    process an event on widgets

    + +
    + + + + +
    +
    +

    renderInfo

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:5038 +

    + + + +
    + +
    +

    draws some useful stats in the corner of the canvas

    + +
    + + + + +
    + +
    +

    resize

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:6035 +

    + + + +
    + +
    +

    resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode

    + +
    + + + + +
    +
    +

    selectNode

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4603 +

    + + + +
    + +
    +

    selects a given node (or adds it to the current selection)

    + +
    + + + + +
    +
    +

    selectNodes

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4615 +

    + + + +
    + +
    +

    selects several nodes (or adds them to the current selection)

    + +
    + + + + +
    +
    +

    sendToBack

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4829 +

    + + + +
    + +
    +

    sends a node to the back (below all other nodes)

    + +
    + + + +

    setCanvas

    @@ -441,7 +1610,7 @@

    Defined in - ../src/litegraph.js:3088 + ../src/litegraph.js:3423

    @@ -495,7 +1664,7 @@

    Defined in - ../src/litegraph.js:3013 + ../src/litegraph.js:3348

    @@ -526,6 +1695,37 @@ +
    +
    +

    setZoom

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:4758 +

    + + + +
    + +
    +

    changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom

    + +
    + + + +

    startRendering

    @@ -542,7 +1742,7 @@

    Defined in - ../src/litegraph.js:3355 + ../src/litegraph.js:3654

    @@ -573,7 +1773,7 @@

    Defined in - ../src/litegraph.js:3379 + ../src/litegraph.js:3678

    @@ -588,6 +1788,69 @@ +
    +
    +

    switchLiveMode

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:6058 +

    + + + +
    + +
    +

    switches to live mode (node shapes are not rendered, only the content) +this feature was designed when graphs where meant to create user interfaces

    + +
    + + + + +
    +
    +

    unbindEvents

    + + () + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:3550 +

    + + + +
    + +
    +

    unbinds mouse events from the canvas

    + +
    + + + +
    diff --git a/doc/classes/LGraphNode.html b/doc/classes/LGraphNode.html index ca2e83908..f8c018b31 100644 --- a/doc/classes/LGraphNode.html +++ b/doc/classes/LGraphNode.html @@ -85,7 +85,7 @@ @@ -136,6 +136,10 @@
  • addProperty +
  • +
  • + addWidget +
  • collapse @@ -230,7 +234,7 @@
  • - isPointInsideNode + isPointInside
  • @@ -305,7 +309,7 @@

    Defined in - ../src/litegraph.js:2276 + ../src/litegraph.js:2434

    @@ -397,7 +401,7 @@

    Defined in - ../src/litegraph.js:2213 + ../src/litegraph.js:2371

    @@ -472,7 +476,7 @@

    Defined in - ../src/litegraph.js:2237 + ../src/litegraph.js:2395

    @@ -532,7 +536,7 @@

    Defined in - ../src/litegraph.js:2151 + ../src/litegraph.js:2309

    @@ -607,7 +611,7 @@

    Defined in - ../src/litegraph.js:2174 + ../src/litegraph.js:2332

    @@ -670,7 +674,7 @@

    Defined in - ../src/litegraph.js:2125 + ../src/litegraph.js:2283

    @@ -733,6 +737,49 @@ +
    +
    +

    addWidget

    + + () + + + Float32Array4 + + + + + + + + +
    +

    + Defined in + ../src/litegraph.js:2510 +

    + + + +
    + +
    +

    Allows to pass

    + +
    + + +
    +

    Returns:

    + +
    + Float32Array4: +

    the total size

    + +
    +
    + +

    collapse

    @@ -749,7 +796,7 @@

    Defined in - ../src/litegraph.js:2860 + ../src/litegraph.js:3072

    @@ -789,7 +836,7 @@

    Defined in - ../src/litegraph.js:2297 + ../src/litegraph.js:2455

    @@ -845,7 +892,7 @@

    Defined in - ../src/litegraph.js:1590 + ../src/litegraph.js:1744

    @@ -891,7 +938,7 @@

    Defined in - ../src/litegraph.js:2450 + ../src/litegraph.js:2640

    @@ -979,7 +1026,7 @@

    Defined in - ../src/litegraph.js:2658 + ../src/litegraph.js:2870

    @@ -1048,7 +1095,7 @@

    Defined in - ../src/litegraph.js:2568 + ../src/litegraph.js:2768

    @@ -1125,7 +1172,7 @@

    Defined in - ../src/litegraph.js:2419 + ../src/litegraph.js:2609

    @@ -1191,7 +1238,7 @@

    Defined in - ../src/litegraph.js:2435 + ../src/litegraph.js:2625

    @@ -1251,7 +1298,7 @@

    Defined in - ../src/litegraph.js:2348 + ../src/litegraph.js:2538

    @@ -1304,7 +1351,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

    Defined in - ../src/litegraph.js:2726 + ../src/litegraph.js:2939

    @@ -1384,7 +1431,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

    Defined in - ../src/litegraph.js:1833 + ../src/litegraph.js:1991

    @@ -1463,7 +1510,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

    Defined in - ../src/litegraph.js:1869 + ../src/litegraph.js:2027

    @@ -1539,7 +1586,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

    Defined in - ../src/litegraph.js:1898 + ../src/litegraph.js:2056

    @@ -1604,7 +1651,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

    Defined in - ../src/litegraph.js:1913 + ../src/litegraph.js:2071

    @@ -1669,7 +1716,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

    Defined in - ../src/litegraph.js:1935 + ../src/litegraph.js:2093

    @@ -1734,7 +1781,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

    Defined in - ../src/litegraph.js:1959 + ../src/litegraph.js:2117

    @@ -1799,7 +1846,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

    Defined in - ../src/litegraph.js:1977 + ../src/litegraph.js:2135

    @@ -1864,7 +1911,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

    Defined in - ../src/litegraph.js:2022 + ../src/litegraph.js:2180

    @@ -1930,7 +1977,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

    Defined in - ../src/litegraph.js:2388 + ../src/litegraph.js:2578

    @@ -1996,7 +2043,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

    Defined in - ../src/litegraph.js:1784 + ../src/litegraph.js:1942

    @@ -2030,7 +2077,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

    Defined in - ../src/litegraph.js:2006 + ../src/litegraph.js:2164

    @@ -2077,7 +2124,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

    Defined in - ../src/litegraph.js:1885 + ../src/litegraph.js:2043

    @@ -2140,7 +2187,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

    Defined in - ../src/litegraph.js:1993 + ../src/litegraph.js:2151

    @@ -2179,8 +2226,8 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

    -
    -

    isPointInsideNode

    +
    +

    isPointInside

    (
      @@ -2206,7 +2253,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

      Defined in - ../src/litegraph.js:2364 + ../src/litegraph.js:2554

      @@ -2270,7 +2317,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

      Defined in - ../src/litegraph.js:2873 + ../src/litegraph.js:3088

      @@ -2307,7 +2354,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

      Defined in - ../src/litegraph.js:2262 + ../src/litegraph.js:2420

      @@ -2360,7 +2407,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

      Defined in - ../src/litegraph.js:2199 + ../src/litegraph.js:2357

      @@ -2407,7 +2454,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

      Defined in - ../src/litegraph.js:1690 + ../src/litegraph.js:1848

      @@ -2447,7 +2494,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

      Defined in - ../src/litegraph.js:1797 + ../src/litegraph.js:1955

      @@ -2504,7 +2551,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

      Defined in - ../src/litegraph.js:1772 + ../src/litegraph.js:1930

      @@ -2544,7 +2591,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

      Defined in - ../src/litegraph.js:2055 + ../src/litegraph.js:2213

      @@ -2611,7 +2658,7 @@ bounding is: [topleft_cornerx, topleft_cornery, width, height]

      Defined in - ../src/litegraph.js:2078 + ../src/litegraph.js:2236

      diff --git a/doc/classes/LiteGraph.html b/doc/classes/LiteGraph.html index 5107d175d..5f5241314 100644 --- a/doc/classes/LiteGraph.html +++ b/doc/classes/LiteGraph.html @@ -205,7 +205,7 @@

      Defined in - ../src/litegraph.js:165 + ../src/litegraph.js:179

      @@ -265,7 +265,7 @@

      Defined in - ../src/litegraph.js:183 + ../src/litegraph.js:197

      @@ -344,7 +344,7 @@

      Defined in - ../src/litegraph.js:226 + ../src/litegraph.js:240

      @@ -410,7 +410,7 @@

      Defined in - ../src/litegraph.js:239 + ../src/litegraph.js:253

      @@ -470,7 +470,7 @@

      Defined in - ../src/litegraph.js:261 + ../src/litegraph.js:281

      @@ -519,7 +519,7 @@

      Defined in - ../src/litegraph.js:70 + ../src/litegraph.js:82

      @@ -593,7 +593,7 @@

      Defined in - ../src/litegraph.js:135 + ../src/litegraph.js:149

      diff --git a/doc/data.json b/doc/data.json index 1c97a5a45..bf60ccdd5 100644 --- a/doc/data.json +++ b/doc/data.json @@ -39,9 +39,16 @@ "plugin_for": [], "extension_for": [], "file": "../src/litegraph.js", - "line": 378, + "line": 405, "description": "LGraph is the class that contain a full graph. We instantiate one and add nodes to it, and then we can run the execution loop.", - "is_constructor": 1 + "is_constructor": 1, + "params": [ + { + "name": "o", + "description": "data from previous serialization [optional]", + "type": "Object" + } + ] }, "LGraphNode": { "name": "LGraphNode", @@ -52,7 +59,7 @@ "plugin_for": [], "extension_for": [], "file": "../src/litegraph.js", - "line": 1535, + "line": 1692, "description": "Base Class for all the node type classes", "params": [ { @@ -71,7 +78,7 @@ "plugin_for": [], "extension_for": [], "file": "../src/litegraph.js", - "line": 3325, + "line": 3624, "description": "marks as dirty the canvas, this way it will be rendered again", "is_constructor": 1, "params": [ @@ -102,7 +109,7 @@ "plugin_for": [], "extension_for": [], "file": "../src/litegraph.js", - "line": 6207, + "line": 7219, "description": "ContextMenu from LiteGUI", "is_constructor": 1, "params": [ @@ -123,7 +130,7 @@ "classitems": [ { "file": "../src/litegraph.js", - "line": 70, + "line": 82, "description": "Register a node class so it can be listed when the user wants to create a new one", "itemtype": "method", "name": "registerNodeType", @@ -143,7 +150,7 @@ }, { "file": "../src/litegraph.js", - "line": 135, + "line": 149, "description": "Create a new node type by passing a function, it wraps it with a propper class and generates inputs according to the parameters of the function.\nUseful to wrap simple methods that do not require properties, and that only process some input to generate an output.", "itemtype": "method", "name": "wrapFunctionAsNode", @@ -173,7 +180,7 @@ }, { "file": "../src/litegraph.js", - "line": 165, + "line": 179, "description": "Adds this method to all nodetypes, existing and to be created\n(You can add it to LGraphNode.prototype but then existing node types wont have it)", "itemtype": "method", "name": "addNodeMethod", @@ -188,7 +195,7 @@ }, { "file": "../src/litegraph.js", - "line": 183, + "line": 197, "description": "Create a node of a given type with a name. The node is not attached to any graph yet.", "itemtype": "method", "name": "createNode", @@ -213,7 +220,7 @@ }, { "file": "../src/litegraph.js", - "line": 226, + "line": 240, "description": "Returns a registered node type with a given name", "itemtype": "method", "name": "getNodeType", @@ -232,7 +239,7 @@ }, { "file": "../src/litegraph.js", - "line": 239, + "line": 253, "description": "Returns a list of node types matching one category", "itemtype": "method", "name": "getNodeType", @@ -251,7 +258,7 @@ }, { "file": "../src/litegraph.js", - "line": 261, + "line": 281, "description": "Returns a list with all the node type categories", "itemtype": "method", "name": "getNodeTypesCategories", @@ -263,7 +270,7 @@ }, { "file": "../src/litegraph.js", - "line": 404, + "line": 435, "description": "Removes all nodes from this graph", "itemtype": "method", "name": "clear", @@ -271,7 +278,7 @@ }, { "file": "../src/litegraph.js", - "line": 453, + "line": 489, "description": "Attach Canvas to this graph", "itemtype": "method", "name": "attachCanvas", @@ -286,7 +293,7 @@ }, { "file": "../src/litegraph.js", - "line": 472, + "line": 508, "description": "Detach Canvas from this graph", "itemtype": "method", "name": "detachCanvas", @@ -301,7 +308,7 @@ }, { "file": "../src/litegraph.js", - "line": 490, + "line": 525, "description": "Starts running this graph every interval milliseconds.", "itemtype": "method", "name": "start", @@ -316,7 +323,7 @@ }, { "file": "../src/litegraph.js", - "line": 518, + "line": 554, "description": "Stops the execution loop of the graph", "itemtype": "method", "name": "stop execution", @@ -324,7 +331,7 @@ }, { "file": "../src/litegraph.js", - "line": 540, + "line": 576, "description": "Run N steps (cycles) of the graph", "itemtype": "method", "name": "runStep", @@ -339,7 +346,7 @@ }, { "file": "../src/litegraph.js", - "line": 619, + "line": 658, "description": "Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than\nnodes with only inputs.", "itemtype": "method", "name": "updateExecutionOrder", @@ -347,7 +354,19 @@ }, { "file": "../src/litegraph.js", - "line": 736, + "line": 790, + "description": "Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively.\nIt doesnt include the node itself", + "itemtype": "method", + "name": "getAncestors", + "return": { + "description": "an array with all the LGraphNodes that affect this node, in order of execution", + "type": "Array" + }, + "class": "LGraph" + }, + { + "file": "../src/litegraph.js", + "line": 827, "description": "Positions every node in a more readable manner", "itemtype": "method", "name": "arrange", @@ -355,7 +374,7 @@ }, { "file": "../src/litegraph.js", - "line": 780, + "line": 871, "description": "Returns the amount of time the graph has been running in milliseconds", "itemtype": "method", "name": "getTime", @@ -367,7 +386,7 @@ }, { "file": "../src/litegraph.js", - "line": 790, + "line": 881, "description": "Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant", "itemtype": "method", "name": "getFixedTime", @@ -379,7 +398,7 @@ }, { "file": "../src/litegraph.js", - "line": 801, + "line": 892, "description": "Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct\nif the nodes are using graphical actions", "itemtype": "method", "name": "getElapsedTime", @@ -391,7 +410,7 @@ }, { "file": "../src/litegraph.js", - "line": 813, + "line": 904, "description": "Sends an event to all the nodes, useful to trigger stuff", "itemtype": "method", "name": "sendEventToAllNodes", @@ -411,7 +430,7 @@ }, { "file": "../src/litegraph.js", - "line": 856, + "line": 947, "description": "Adds a new node instasnce to this graph", "itemtype": "method", "name": "add", @@ -426,7 +445,7 @@ }, { "file": "../src/litegraph.js", - "line": 912, + "line": 1011, "description": "Removes a node from the graph", "itemtype": "method", "name": "remove", @@ -441,7 +460,7 @@ }, { "file": "../src/litegraph.js", - "line": 981, + "line": 1092, "description": "Returns a node by its id.", "itemtype": "method", "name": "getNodeById", @@ -456,7 +475,7 @@ }, { "file": "../src/litegraph.js", - "line": 994, + "line": 1105, "description": "Returns a list of nodes that matches a class", "itemtype": "method", "name": "findNodesByClass", @@ -475,7 +494,7 @@ }, { "file": "../src/litegraph.js", - "line": 1010, + "line": 1121, "description": "Returns a list of nodes that matches a type", "itemtype": "method", "name": "findNodesByType", @@ -494,7 +513,7 @@ }, { "file": "../src/litegraph.js", - "line": 1027, + "line": 1138, "description": "Returns a list of nodes that matches a name", "itemtype": "method", "name": "findNodesByTitle", @@ -513,7 +532,7 @@ }, { "file": "../src/litegraph.js", - "line": 1043, + "line": 1154, "description": "Returns the top-most node in this position of the canvas", "itemtype": "method", "name": "getNodeOnPos", @@ -535,14 +554,38 @@ } ], "return": { - "description": "a list with all the nodes that intersect this coordinate", - "type": "Array" + "description": "the node at this position or null", + "type": "LGraphNode" }, "class": "LGraph" }, { "file": "../src/litegraph.js", - "line": 1065, + "line": 1174, + "description": "Returns the top-most group in that position", + "itemtype": "method", + "name": "getGroupOnPos", + "params": [ + { + "name": "x", + "description": "the x coordinate in canvas space", + "type": "Number" + }, + { + "name": "y", + "description": "the y coordinate in canvas space", + "type": "Number" + } + ], + "return": { + "description": "the group or null", + "type": "LGraphGroup" + }, + "class": "LGraph" + }, + { + "file": "../src/litegraph.js", + "line": 1194, "description": "Tell this graph it has a global graph input of this type", "itemtype": "method", "name": "addGlobalInput", @@ -567,7 +610,7 @@ }, { "file": "../src/litegraph.js", - "line": 1083, + "line": 1213, "description": "Assign a data to the global graph input", "itemtype": "method", "name": "setGlobalInputData", @@ -587,7 +630,7 @@ }, { "file": "../src/litegraph.js", - "line": 1097, + "line": 1227, "description": "Assign a data to the global graph input (same as setGlobalInputData)", "itemtype": "method", "name": "setInputData", @@ -607,7 +650,7 @@ }, { "file": "../src/litegraph.js", - "line": 1106, + "line": 1236, "description": "Returns the current value of a global graph input", "itemtype": "method", "name": "getGlobalInputData", @@ -626,7 +669,7 @@ }, { "file": "../src/litegraph.js", - "line": 1120, + "line": 1250, "description": "Changes the name of a global graph input", "itemtype": "method", "name": "renameGlobalInput", @@ -646,7 +689,7 @@ }, { "file": "../src/litegraph.js", - "line": 1150, + "line": 1281, "description": "Changes the type of a global graph input", "itemtype": "method", "name": "changeGlobalInputType", @@ -666,7 +709,7 @@ }, { "file": "../src/litegraph.js", - "line": 1169, + "line": 1301, "description": "Removes a global graph input", "itemtype": "method", "name": "removeGlobalInput", @@ -686,7 +729,7 @@ }, { "file": "../src/litegraph.js", - "line": 1190, + "line": 1323, "description": "Creates a global graph output", "itemtype": "method", "name": "addGlobalOutput", @@ -711,7 +754,7 @@ }, { "file": "../src/litegraph.js", - "line": 1208, + "line": 1342, "description": "Assign a data to the global output", "itemtype": "method", "name": "setGlobalOutputData", @@ -731,7 +774,7 @@ }, { "file": "../src/litegraph.js", - "line": 1222, + "line": 1356, "description": "Returns the current value of a global graph output", "itemtype": "method", "name": "getGlobalOutputData", @@ -750,7 +793,7 @@ }, { "file": "../src/litegraph.js", - "line": 1236, + "line": 1370, "description": "Returns the current value of a global graph output (sames as getGlobalOutputData)", "itemtype": "method", "name": "getOutputData", @@ -769,7 +812,7 @@ }, { "file": "../src/litegraph.js", - "line": 1245, + "line": 1379, "description": "Renames a global graph output", "itemtype": "method", "name": "renameGlobalOutput", @@ -789,7 +832,7 @@ }, { "file": "../src/litegraph.js", - "line": 1272, + "line": 1407, "description": "Changes the type of a global graph output", "itemtype": "method", "name": "changeGlobalOutputType", @@ -809,7 +852,7 @@ }, { "file": "../src/litegraph.js", - "line": 1291, + "line": 1427, "description": "Removes a global graph output", "itemtype": "method", "name": "removeGlobalOutput", @@ -824,7 +867,7 @@ }, { "file": "../src/litegraph.js", - "line": 1333, + "line": 1471, "description": "returns if the graph is in live mode", "itemtype": "method", "name": "isLive", @@ -832,7 +875,7 @@ }, { "file": "../src/litegraph.js", - "line": 1370, + "line": 1506, "description": "Creates a Object containing all the info about this graph, it can be serialized", "itemtype": "method", "name": "serialize", @@ -844,7 +887,7 @@ }, { "file": "../src/litegraph.js", - "line": 1403, + "line": 1542, "description": "Configure a graph from a JSON string", "itemtype": "method", "name": "configure", @@ -853,13 +896,18 @@ "name": "str", "description": "configure a graph from a JSON string", "type": "String" + }, + { + "name": "returns", + "description": "if there was any error parsing", + "type": "Boolean" } ], "class": "LGraph" }, { "file": "../src/litegraph.js", - "line": 1590, + "line": 1744, "description": "configure a node from an object containing the serialized info", "itemtype": "method", "name": "configure", @@ -867,7 +915,7 @@ }, { "file": "../src/litegraph.js", - "line": 1690, + "line": 1848, "description": "serialize the content", "itemtype": "method", "name": "serialize", @@ -875,7 +923,7 @@ }, { "file": "../src/litegraph.js", - "line": 1772, + "line": 1930, "description": "serialize and stringify", "itemtype": "method", "name": "toString", @@ -883,7 +931,7 @@ }, { "file": "../src/litegraph.js", - "line": 1784, + "line": 1942, "description": "get the title string", "itemtype": "method", "name": "getTitle", @@ -891,7 +939,7 @@ }, { "file": "../src/litegraph.js", - "line": 1797, + "line": 1955, "description": "sets the output data", "itemtype": "method", "name": "setOutputData", @@ -911,7 +959,7 @@ }, { "file": "../src/litegraph.js", - "line": 1833, + "line": 1991, "description": "Retrieves the input data (data traveling through the connection) from one slot", "itemtype": "method", "name": "getInputData", @@ -935,7 +983,7 @@ }, { "file": "../src/litegraph.js", - "line": 1869, + "line": 2027, "description": "Retrieves the input data from one slot using its name instead of slot number", "itemtype": "method", "name": "getInputDataByName", @@ -959,7 +1007,7 @@ }, { "file": "../src/litegraph.js", - "line": 1885, + "line": 2043, "description": "tells you if there is a connection in one input slot", "itemtype": "method", "name": "isInputConnected", @@ -978,7 +1026,7 @@ }, { "file": "../src/litegraph.js", - "line": 1898, + "line": 2056, "description": "tells you info about an input connection (which node, type, etc)", "itemtype": "method", "name": "getInputInfo", @@ -997,7 +1045,7 @@ }, { "file": "../src/litegraph.js", - "line": 1913, + "line": 2071, "description": "returns the node connected in the input slot", "itemtype": "method", "name": "getInputNode", @@ -1016,7 +1064,7 @@ }, { "file": "../src/litegraph.js", - "line": 1935, + "line": 2093, "description": "returns the value of an input with this name, otherwise checks if there is a property with that name", "itemtype": "method", "name": "getInputOrProperty", @@ -1035,7 +1083,7 @@ }, { "file": "../src/litegraph.js", - "line": 1959, + "line": 2117, "description": "tells you the last output data that went in that slot", "itemtype": "method", "name": "getOutputData", @@ -1054,7 +1102,7 @@ }, { "file": "../src/litegraph.js", - "line": 1977, + "line": 2135, "description": "tells you info about an output connection (which node, type, etc)", "itemtype": "method", "name": "getOutputInfo", @@ -1073,7 +1121,7 @@ }, { "file": "../src/litegraph.js", - "line": 1993, + "line": 2151, "description": "tells you if there is a connection in one output slot", "itemtype": "method", "name": "isOutputConnected", @@ -1092,7 +1140,7 @@ }, { "file": "../src/litegraph.js", - "line": 2006, + "line": 2164, "description": "tells you if there is any connection in the output slots", "itemtype": "method", "name": "isAnyOutputConnected", @@ -1104,7 +1152,7 @@ }, { "file": "../src/litegraph.js", - "line": 2022, + "line": 2180, "description": "retrieves all the nodes connected to this output slot", "itemtype": "method", "name": "getOutputNodes", @@ -1123,7 +1171,7 @@ }, { "file": "../src/litegraph.js", - "line": 2055, + "line": 2213, "description": "Triggers an event in this node, this will trigger any output with the same name", "itemtype": "method", "name": "trigger", @@ -1143,7 +1191,7 @@ }, { "file": "../src/litegraph.js", - "line": 2078, + "line": 2236, "description": "Triggers an slot event in this node", "itemtype": "method", "name": "triggerSlot", @@ -1163,7 +1211,7 @@ }, { "file": "../src/litegraph.js", - "line": 2125, + "line": 2283, "description": "add a new property to this node", "itemtype": "method", "name": "addProperty", @@ -1193,7 +1241,7 @@ }, { "file": "../src/litegraph.js", - "line": 2151, + "line": 2309, "description": "add a new output slot to use in this node", "itemtype": "method", "name": "addOutput", @@ -1218,7 +1266,7 @@ }, { "file": "../src/litegraph.js", - "line": 2174, + "line": 2332, "description": "add a new output slot to use in this node", "itemtype": "method", "name": "addOutputs", @@ -1233,7 +1281,7 @@ }, { "file": "../src/litegraph.js", - "line": 2199, + "line": 2357, "description": "remove an existing output slot", "itemtype": "method", "name": "removeOutput", @@ -1248,7 +1296,7 @@ }, { "file": "../src/litegraph.js", - "line": 2213, + "line": 2371, "description": "add a new input slot to use in this node", "itemtype": "method", "name": "addInput", @@ -1273,7 +1321,7 @@ }, { "file": "../src/litegraph.js", - "line": 2237, + "line": 2395, "description": "add several new input slots in this node", "itemtype": "method", "name": "addInputs", @@ -1288,7 +1336,7 @@ }, { "file": "../src/litegraph.js", - "line": 2262, + "line": 2420, "description": "remove an existing input slot", "itemtype": "method", "name": "removeInput", @@ -1303,7 +1351,7 @@ }, { "file": "../src/litegraph.js", - "line": 2276, + "line": 2434, "description": "add an special connection to this node (used for special kinds of graphs)", "itemtype": "method", "name": "addConnection", @@ -1333,7 +1381,7 @@ }, { "file": "../src/litegraph.js", - "line": 2297, + "line": 2455, "description": "computes the size of a node according to its inputs and output slots", "itemtype": "method", "name": "computeSize", @@ -1352,7 +1400,19 @@ }, { "file": "../src/litegraph.js", - "line": 2348, + "line": 2510, + "description": "Allows to pass", + "itemtype": "method", + "name": "addWidget", + "return": { + "description": "the total size", + "type": "Float32Array[4]" + }, + "class": "LGraphNode" + }, + { + "file": "../src/litegraph.js", + "line": 2538, "description": "returns the bounding of the object, used for rendering purposes\nbounding is: [topleft_cornerx, topleft_cornery, width, height]", "itemtype": "method", "name": "getBounding", @@ -1364,10 +1424,10 @@ }, { "file": "../src/litegraph.js", - "line": 2364, + "line": 2554, "description": "checks if a point is inside the shape of a node", "itemtype": "method", - "name": "isPointInsideNode", + "name": "isPointInside", "params": [ { "name": "x", @@ -1388,7 +1448,7 @@ }, { "file": "../src/litegraph.js", - "line": 2388, + "line": 2578, "description": "checks if a point is inside a node slot, and returns info about which slot", "itemtype": "method", "name": "getSlotInPosition", @@ -1412,7 +1472,7 @@ }, { "file": "../src/litegraph.js", - "line": 2419, + "line": 2609, "description": "returns the input slot with a given name (used for dynamic slots), -1 if not found", "itemtype": "method", "name": "findInputSlot", @@ -1431,7 +1491,7 @@ }, { "file": "../src/litegraph.js", - "line": 2435, + "line": 2625, "description": "returns the output slot with a given name (used for dynamic slots), -1 if not found", "itemtype": "method", "name": "findOutputSlot", @@ -1450,7 +1510,7 @@ }, { "file": "../src/litegraph.js", - "line": 2450, + "line": 2640, "description": "connect this node output to the input of another node", "itemtype": "method", "name": "connect", @@ -1479,7 +1539,7 @@ }, { "file": "../src/litegraph.js", - "line": 2568, + "line": 2768, "description": "disconnect one output to an specific node", "itemtype": "method", "name": "disconnectOutput", @@ -1503,7 +1563,7 @@ }, { "file": "../src/litegraph.js", - "line": 2658, + "line": 2870, "description": "disconnect one input", "itemtype": "method", "name": "disconnectInput", @@ -1522,7 +1582,7 @@ }, { "file": "../src/litegraph.js", - "line": 2726, + "line": 2939, "description": "returns the center of a connection point in canvas coords", "itemtype": "method", "name": "getConnectionPos", @@ -1546,7 +1606,7 @@ }, { "file": "../src/litegraph.js", - "line": 2860, + "line": 3072, "description": "Collapse the node to make it smaller on the canvas", "itemtype": "method", "name": "collapse", @@ -1554,7 +1614,7 @@ }, { "file": "../src/litegraph.js", - "line": 2873, + "line": 3088, "description": "Forces the node to do not move or realign on Z", "itemtype": "method", "name": "pin", @@ -1562,7 +1622,7 @@ }, { "file": "../src/litegraph.js", - "line": 2974, + "line": 3306, "description": "clears all the data inside", "itemtype": "method", "name": "clear", @@ -1570,7 +1630,7 @@ }, { "file": "../src/litegraph.js", - "line": 3013, + "line": 3348, "description": "assigns a graph, you can reasign graphs to the same canvas", "itemtype": "method", "name": "setGraph", @@ -1585,7 +1645,7 @@ }, { "file": "../src/litegraph.js", - "line": 3044, + "line": 3379, "description": "opens a graph contained inside a node in the current graph", "itemtype": "method", "name": "openSubgraph", @@ -1600,7 +1660,7 @@ }, { "file": "../src/litegraph.js", - "line": 3071, + "line": 3406, "description": "closes a subgraph contained inside a node", "itemtype": "method", "name": "closeSubgraph", @@ -1615,7 +1675,7 @@ }, { "file": "../src/litegraph.js", - "line": 3088, + "line": 3423, "description": "assigns a canvas", "itemtype": "method", "name": "setCanvas", @@ -1630,7 +1690,31 @@ }, { "file": "../src/litegraph.js", - "line": 3341, + "line": 3498, + "description": "binds mouse, keyboard, touch and drag events to the canvas", + "itemtype": "method", + "name": "bindEvents", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 3550, + "description": "unbinds mouse events from the canvas", + "itemtype": "method", + "name": "unbindEvents", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 3598, + "description": "this function allows to render the canvas using WebGL instead of Canvas2D\nthis is useful if you plant to render 3D objects inside your nodes, it uses litegl.js for webgl and canvas2DtoWebGL to emulate the Canvas2D calls in webGL", + "itemtype": "method", + "name": "enableWebGL", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 3640, "description": "Used to attach the canvas in a popup", "itemtype": "method", "name": "getCanvasWindow", @@ -1642,7 +1726,7 @@ }, { "file": "../src/litegraph.js", - "line": 3355, + "line": 3654, "description": "starts rendering the content of the canvas when needed", "itemtype": "method", "name": "startRendering", @@ -1650,11 +1734,275 @@ }, { "file": "../src/litegraph.js", - "line": 3379, + "line": 3678, "description": "stops rendering the content of the canvas (to save resources)", "itemtype": "method", "name": "stopRendering", "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 3921, + "description": "Called when a mouse move event has to be processed", + "itemtype": "method", + "name": "processMouseMove", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4112, + "description": "Called when a mouse up event has to be processed", + "itemtype": "method", + "name": "processMouseUp", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4263, + "description": "Called when a mouse wheel event has to be processed", + "itemtype": "method", + "name": "processMouseWheel", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4296, + "description": "retuns true if a position (in graph space) is on top of a node little corner box", + "itemtype": "method", + "name": "isOverNodeBox", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4308, + "description": "retuns true if a position (in graph space) is on top of a node input slot", + "itemtype": "method", + "name": "isOverNodeInput", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4332, + "description": "process a key event", + "itemtype": "method", + "name": "processKey", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4485, + "description": "process a item drop event on top the canvas", + "itemtype": "method", + "name": "processDrop", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4603, + "description": "selects a given node (or adds it to the current selection)", + "itemtype": "method", + "name": "selectNode", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4615, + "description": "selects several nodes (or adds them to the current selection)", + "itemtype": "method", + "name": "selectNodes", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4653, + "description": "removes a node from the current selection", + "itemtype": "method", + "name": "deselectNode", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4679, + "description": "removes all nodes from the current selection", + "itemtype": "method", + "name": "deselectAllNodes", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4702, + "description": "deletes all nodes in the current selection from the graph", + "itemtype": "method", + "name": "deleteSelectedNodes", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4719, + "description": "centers the camera on a given node", + "itemtype": "method", + "name": "centerOnNode", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4730, + "description": "adds some useful properties to a mouse event, like the position in graph coordinates", + "itemtype": "method", + "name": "adjustMouseEvent", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4758, + "description": "changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom", + "itemtype": "method", + "name": "setZoom", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4786, + "description": "converts a coordinate in canvas2D space to graphcanvas space (NAME IS CONFUSION, SHOULD BE THE OTHER WAY AROUND)", + "itemtype": "method", + "name": "convertOffsetToCanvas", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4798, + "description": "converts a coordinate in graphcanvas space to canvas2D space (NAME IS CONFUSION, SHOULD BE THE OTHER WAY AROUND)", + "itemtype": "method", + "name": "convertCanvasToOffset", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4816, + "description": "brings a node to front (above all other nodes)", + "itemtype": "method", + "name": "bringToFront", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4829, + "description": "sends a node to the back (below all other nodes)", + "itemtype": "method", + "name": "sendToBack", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4849, + "description": "checks which nodes are visible (inside the camera area)", + "itemtype": "method", + "name": "computeVisibleNodes", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4874, + "description": "renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes)", + "itemtype": "method", + "name": "draw", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 4905, + "description": "draws the front canvas (the one containing all the nodes)", + "itemtype": "method", + "name": "drawFrontCanvas", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 5038, + "description": "draws some useful stats in the corner of the canvas", + "itemtype": "method", + "name": "renderInfo", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 5064, + "description": "draws the back canvas (the one containing the background and the connections)", + "itemtype": "method", + "name": "drawBackCanvas", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 5196, + "description": "draws the given node inside the canvas", + "itemtype": "method", + "name": "drawNode", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 5467, + "description": "draws the shape of the given node in the canvas", + "itemtype": "method", + "name": "drawNodeShape", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 5639, + "description": "draws every connection visible in the canvas\nOPTIMIZE THIS: precatch connections position instead of recomputing them every time", + "itemtype": "method", + "name": "drawConnections", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 5694, + "description": "draws a link between two points", + "itemtype": "method", + "name": "renderLink", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 5815, + "description": "draws the widgets stored inside a node", + "itemtype": "method", + "name": "drawNodeWidgets", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 5911, + "description": "process an event on widgets", + "itemtype": "method", + "name": "processNodeWidgets", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 5990, + "description": "draws every group area in the background", + "itemtype": "method", + "name": "drawGroups", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 6035, + "description": "resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode", + "itemtype": "method", + "name": "resize", + "class": "LGraphCanvas" + }, + { + "file": "../src/litegraph.js", + "line": 6058, + "description": "switches to live mode (node shapes are not rendered, only the content)\nthis feature was designed when graphs where meant to create user interfaces", + "itemtype": "method", + "name": "switchLiveMode", + "class": "LGraphCanvas" } ], "warnings": [] diff --git a/doc/files/.._src_litegraph.js.html b/doc/files/.._src_litegraph.js.html index 079c1d3ef..e1f237abe 100644 --- a/doc/files/.._src_litegraph.js.html +++ b/doc/files/.._src_litegraph.js.html @@ -84,6597 +84,7620 @@
      -(function(global){
      -// *************************************************************
      -//   LiteGraph CLASS                                     *******
      -// *************************************************************
      -
      -/* FYI: links are stored in graph.links with this structure per object
      -{
      -	id: number
      -	type: string,
      -	origin_id: number,
      -	origin_slot: number,
      -	target_id: number,
      -	target_slot: number,
      -	data: *
      -};
      -*/
      -
      -/**
      -* The Global Scope. It contains all the registered node classes.
      -*
      -* @class LiteGraph
      -* @constructor
      -*/
      -
      -var LiteGraph = global.LiteGraph = {
      -
      -	NODE_TITLE_HEIGHT: 16,
      -	NODE_SLOT_HEIGHT: 15,
      -	NODE_WIDTH: 140,
      -	NODE_MIN_WIDTH: 50,
      -	NODE_COLLAPSED_RADIUS: 10,
      -	NODE_COLLAPSED_WIDTH: 80,
      -	CANVAS_GRID_SIZE: 10,
      -	NODE_TITLE_COLOR: "#222",
      -	NODE_DEFAULT_COLOR: "#999",
      -	NODE_DEFAULT_BGCOLOR: "#444",
      -	NODE_DEFAULT_BOXCOLOR: "#AEF",
      -	NODE_DEFAULT_SHAPE: "box",
      -	MAX_NUMBER_OF_NODES: 1000, //avoid infinite loops
      -	DEFAULT_POSITION: [100,100],//default node position
      -	node_images_path: "",
      -
      -	VALID_SHAPES: ["box","round"], //,"circle"
      -
      -	BOX_SHAPE: 1,
      -	ROUND_SHAPE: 2,
      -	CIRCLE_SHAPE: 3,
      -
      -	//enums
      -	INPUT: 1,
      -	OUTPUT: 2,
      -
      -	EVENT: -1, //for outputs
      -	ACTION: -1, //for inputs
      -
      -	ALWAYS: 0,
      -	ON_EVENT: 1,
      -	NEVER: 2,
      -	ON_TRIGGER: 3,
      -
      -	proxy: null, //used to redirect calls
      -
      -	debug: false,
      -	throw_errors: true,
      -	allow_scripts: true,
      -	registered_node_types: {}, //nodetypes by string
      -	node_types_by_file_extension: {}, //used for droping files in the canvas
      -	Nodes: {}, //node types by classname
      -
      -	/**
      -	* Register a node class so it can be listed when the user wants to create a new one
      -	* @method registerNodeType
      -	* @param {String} type name of the node and path
      -	* @param {Class} base_class class containing the structure of a node
      -	*/
      -
      -	registerNodeType: function(type, base_class)
      -	{
      -		if(!base_class.prototype)
      -			throw("Cannot register a simple object, it must be a class with a prototype");
      -		base_class.type = type;
      -
      -		if(LiteGraph.debug)
      -			console.log("Node registered: " + type);
      -
      -		var categories = type.split("/");
      -		var classname = base_class.name;
      -
      -		var pos = type.lastIndexOf("/");
      -		base_class.category = type.substr(0,pos);
      -
      -		if(!base_class.title)
      -			base_class.title = classname;
      -		//info.name = name.substr(pos+1,name.length - pos);
      -
      -		//extend class
      -		if(base_class.prototype) //is a class
      -			for(var i in LGraphNode.prototype)
      -				if(!base_class.prototype[i])
      -					base_class.prototype[i] = LGraphNode.prototype[i];
      -
      -		Object.defineProperty( base_class.prototype, "shape",{
      -			set: function(v) {
      -				switch(v)
      -				{
      -					case "box": this._shape = LiteGraph.BOX_SHAPE; break;
      -					case "round": this._shape = LiteGraph.ROUND_SHAPE; break;
      -					case "circle": this._shape = LiteGraph.CIRCLE_SHAPE; break;
      -					default:
      -						this._shape = v;
      -				}
      -			},
      -			get: function(v)
      -			{
      -				return this._shape;
      -			},
      -			enumerable: true
      -		});
      -
      -		this.registered_node_types[ type ] = base_class;
      -		if(base_class.constructor.name)
      -			this.Nodes[ classname ] = base_class;
      -
      -		//warnings
      -		if(base_class.prototype.onPropertyChange)
      -			console.warn("LiteGraph node class " + type + " has onPropertyChange method, it must be called onPropertyChanged with d at the end");
      -
      -		if( base_class.supported_extensions )
      -		{
      -			for(var i in base_class.supported_extensions )
      -				this.node_types_by_file_extension[ base_class.supported_extensions[i].toLowerCase() ] = base_class;
      -		}
      -	},
      -
      -	/**
      -	* Create a new node type by passing a function, it wraps it with a propper class and generates inputs according to the parameters of the function.
      -	* Useful to wrap simple methods that do not require properties, and that only process some input to generate an output.
      -	* @method wrapFunctionAsNode
      -	* @param {String} name node name with namespace (p.e.: 'math/sum')
      -	* @param {Function} func
      -	* @param {Array} param_types [optional] an array containing the type of every parameter, otherwise parameters will accept any type
      -	* @param {String} return_type [optional] string with the return type, otherwise it will be generic
      -	*/
      -	wrapFunctionAsNode: function( name, func, param_types, return_type )
      -	{
      -		var params = Array(func.length);
      -		var code = "";
      -		var names = LiteGraph.getParameterNames( func );
      -		for(var i = 0; i < names.length; ++i)
      -			code += "this.addInput('"+names[i]+"',"+(param_types && param_types[i] ? "'" + param_types[i] + "'" : "0") + ");\n";
      -		code += "this.addOutput('out',"+( return_type ? "'" + return_type + "'" : 0 )+");\n";
      -		var classobj = Function(code);
      -		classobj.title = name.split("/").pop();
      -		classobj.desc = "Generated from " + func.name;
      -		classobj.prototype.onExecute = function onExecute()
      -		{
      -			for(var i = 0; i < params.length; ++i)
      -				params[i] = this.getInputData(i);
      -			var r = func.apply( this, params );
      -			this.setOutputData(0,r);
      -		}
      -		this.registerNodeType( name, classobj );
      -	},
      -
      -	/**
      -	* Adds this method to all nodetypes, existing and to be created
      -	* (You can add it to LGraphNode.prototype but then existing node types wont have it)
      -	* @method addNodeMethod
      -	* @param {Function} func
      -	*/
      -	addNodeMethod: function( name, func )
      -	{
      -		LGraphNode.prototype[name] = func;
      -		for(var i in this.registered_node_types)
      -		{
      -			var type = this.registered_node_types[i];
      -			if(type.prototype[name])
      -				type.prototype["_" + name] = type.prototype[name]; //keep old in case of replacing
      -			type.prototype[name] = func;
      -		}
      -	},
      -
      -	/**
      -	* Create a node of a given type with a name. The node is not attached to any graph yet.
      -	* @method createNode
      -	* @param {String} type full name of the node class. p.e. "math/sin"
      -	* @param {String} name a name to distinguish from other nodes
      -	* @param {Object} options to set options
      -	*/
      -
      -	createNode: function( type, title, options )
      -	{
      -		var base_class = this.registered_node_types[type];
      -		if (!base_class)
      -		{
      -			if(LiteGraph.debug)
      -				console.log("GraphNode type \"" + type + "\" not registered.");
      -			return null;
      -		}
      -
      -		var prototype = base_class.prototype || base_class;
      -
      -		title = title || base_class.title || type;
      -
      -		var node = new base_class( title );
      -		node.type = type;
      -
      -		if(!node.title && title) node.title = title;
      -		if(!node.properties) node.properties = {};
      -		if(!node.properties_info) node.properties_info = [];
      -		if(!node.flags) node.flags = {};
      -		if(!node.size) node.size = node.computeSize();
      -		if(!node.pos) node.pos = LiteGraph.DEFAULT_POSITION.concat();
      -		if(!node.mode) node.mode = LiteGraph.ALWAYS;
      -
      -		//extra options
      -		if(options)
      -		{
      -			for(var i in options)
      -				node[i] = options[i];
      -		}
      -
      -		return node;
      -	},
      -
      -	/**
      -	* Returns a registered node type with a given name
      -	* @method getNodeType
      -	* @param {String} type full name of the node class. p.e. "math/sin"
      -	* @return {Class} the node class
      -	*/
      -
      -	getNodeType: function(type)
      -	{
      -		return this.registered_node_types[type];
      -	},
      -
      -
      -	/**
      -	* Returns a list of node types matching one category
      -	* @method getNodeType
      -	* @param {String} category category name
      -	* @return {Array} array with all the node classes
      -	*/
      -
      -	getNodeTypesInCategory: function(category)
      -	{
      -		var r = [];
      -		for(var i in this.registered_node_types)
      -			if(category == "")
      -			{
      -				if (this.registered_node_types[i].category == null)
      -					r.push(this.registered_node_types[i]);
      -			}
      -			else if (this.registered_node_types[i].category == category)
      -				r.push(this.registered_node_types[i]);
      -
      -		return r;
      -	},
      -
      -	/**
      -	* Returns a list with all the node type categories
      -	* @method getNodeTypesCategories
      -	* @return {Array} array with all the names of the categories
      -	*/
      -
      -	getNodeTypesCategories: function()
      -	{
      -		var categories = {"":1};
      -		for(var i in this.registered_node_types)
      -			if(this.registered_node_types[i].category && !this.registered_node_types[i].skip_list)
      -				categories[ this.registered_node_types[i].category ] = 1;
      -		var result = [];
      -		for(var i in categories)
      -			result.push(i);
      -		return result;
      -	},
      -
      -	//debug purposes: reloads all the js scripts that matches a wilcard
      -	reloadNodes: function (folder_wildcard)
      -	{
      -		var tmp = document.getElementsByTagName("script");
      -		//weird, this array changes by its own, so we use a copy
      -		var script_files = [];
      -		for(var i in tmp)
      -			script_files.push(tmp[i]);
      -
      -
      -		var docHeadObj = document.getElementsByTagName("head")[0];
      -		folder_wildcard = document.location.href + folder_wildcard;
      -
      -		for(var i in script_files)
      -		{
      -			var src = script_files[i].src;
      -			if( !src || src.substr(0,folder_wildcard.length ) != folder_wildcard)
      -				continue;
      -
      -			try
      -			{
      -				if(LiteGraph.debug)
      -					console.log("Reloading: " + src);
      -				var dynamicScript = document.createElement("script");
      -				dynamicScript.type = "text/javascript";
      -				dynamicScript.src = src;
      -				docHeadObj.appendChild(dynamicScript);
      -				docHeadObj.removeChild(script_files[i]);
      -			}
      -			catch (err)
      -			{
      -				if(LiteGraph.throw_errors)
      -					throw err;
      -				if(LiteGraph.debug)
      -					console.log("Error while reloading " + src);
      -			}
      -		}
      -
      -		if(LiteGraph.debug)
      -			console.log("Nodes reloaded");
      -	},
      -
      -	//separated just to improve if it doesnt work
      -	cloneObject: function(obj, target)
      -	{
      -		if(obj == null) return null;
      -		var r = JSON.parse( JSON.stringify( obj ) );
      -		if(!target) return r;
      -
      -		for(var i in r)
      -			target[i] = r[i];
      -		return target;
      -	},
      -
      -	isValidConnection: function( type_a, type_b )
      -	{
      -		if( !type_a ||  //generic output
      -			!type_b || //generic input
      -			type_a == type_b || //same type (is valid for triggers)
      -			type_a == LiteGraph.EVENT && type_b == LiteGraph.ACTION )
      -				return true;
      -
      -		type_a = type_a.toLowerCase();
      -		type_b = type_b.toLowerCase();
      -		if( type_a.indexOf(",") == -1 && type_b.indexOf(",") == -1 )
      -			return type_a == type_b;
      -
      -		var supported_types_a = type_a.split(",");
      -		var supported_types_b = type_b.split(",");
      -		for(var i = 0; i < supported_types_a.length; ++i)
      -			for(var j = 0; j < supported_types_b.length; ++j)
      -				if( supported_types_a[i] == supported_types_b[j] )
      -					return true;
      -		return false;
      -	}
      -};
      -
      -//timer that works everywhere
      -if(typeof(performance) != "undefined")
      -	LiteGraph.getTime = performance.now.bind(performance);
      -else if(typeof(Date) != "undefined" && Date.now)
      -	LiteGraph.getTime = Date.now.bind(Date);
      -else if(typeof(process) != "undefined")
      -	LiteGraph.getTime = function(){
      -		var t = process.hrtime();
      -		return t[0]*0.001 + t[1]*(1e-6);
      -	}
      -else
      -  LiteGraph.getTime = function getTime() { return (new Date).getTime(); }
      -
      -
      -
      -
      -
      -
      -//*********************************************************************************
      -// LGraph CLASS
      -//*********************************************************************************
      -
      -/**
      -* LGraph is the class that contain a full graph. We instantiate one and add nodes to it, and then we can run the execution loop.
      -*
      -* @class LGraph
      -* @constructor
      -*/
      -
      -function LGraph()
      -{
      -	if (LiteGraph.debug)
      -		console.log("Graph created");
      -	this.list_of_graphcanvas = null;
      -	this.clear();
      -}
      -
      -global.LGraph = LiteGraph.LGraph = LGraph;
      -
      -//default supported types
      -LGraph.supported_types = ["number","string","boolean"];
      -
      -//used to know which types of connections support this graph (some graphs do not allow certain types)
      -LGraph.prototype.getSupportedTypes = function() { return this.supported_types || LGraph.supported_types; }
      -
      -LGraph.STATUS_STOPPED = 1;
      -LGraph.STATUS_RUNNING = 2;
      -
      -/**
      -* Removes all nodes from this graph
      -* @method clear
      -*/
      -
      -LGraph.prototype.clear = function()
      -{
      -	this.stop();
      -	this.status = LGraph.STATUS_STOPPED;
      -	this.last_node_id = 0;
      -
      -	//nodes
      -	this._nodes = [];
      -	this._nodes_by_id = {};
      -	this._nodes_in_order = null; //nodes that are executable sorted in execution order
      -	this._nodes_executable = null; //nodes that contain onExecute
      -
      -	//links
      -	this.last_link_id = 0;
      -	this.links = {}; //container with all the links
      -
      -	//iterations
      -	this.iteration = 0;
      -
      -	this.config = {
      -	};
      -
      -	//timing
      -	this.globaltime = 0;
      -	this.runningtime = 0;
      -	this.fixedtime =  0;
      -	this.fixedtime_lapse = 0.01;
      -	this.elapsed_time = 0.01;
      -	this.starttime = 0;
      -
      -	this.catch_errors = true;
      -
      -	//subgraph_data
      -	this.global_inputs = {};
      -	this.global_outputs = {};
      -
      -	//this.graph = {};
      -	this.debug = true;
      -
      -	this.change();
      -
      -	this.sendActionToCanvas("clear");
      -}
      -
      -/**
      -* Attach Canvas to this graph
      -* @method attachCanvas
      -* @param {GraphCanvas} graph_canvas
      -*/
      -
      -LGraph.prototype.attachCanvas = function(graphcanvas)
      -{
      -	if(graphcanvas.constructor != LGraphCanvas)
      -		throw("attachCanvas expects a LGraphCanvas instance");
      -	if(graphcanvas.graph && graphcanvas.graph != this)
      -		graphcanvas.graph.detachCanvas( graphcanvas );
      -
      -	graphcanvas.graph = this;
      -	if(!this.list_of_graphcanvas)
      -		this.list_of_graphcanvas = [];
      -	this.list_of_graphcanvas.push(graphcanvas);
      -}
      -
      -/**
      -* Detach Canvas from this graph
      -* @method detachCanvas
      -* @param {GraphCanvas} graph_canvas
      -*/
      -
      -LGraph.prototype.detachCanvas = function(graphcanvas)
      -{
      -	if(!this.list_of_graphcanvas)
      -		return;
      -
      -	var pos = this.list_of_graphcanvas.indexOf( graphcanvas );
      -	if(pos == -1)
      -		return;
      -	graphcanvas.graph = null;
      -	this.list_of_graphcanvas.splice(pos,1);
      -}
      -
      -/**
      -* Starts running this graph every interval milliseconds.
      -* @method start
      -* @param {number} interval amount of milliseconds between executions, default is 1
      -*/
      -
      -LGraph.prototype.start = function(interval)
      -{
      -	if( this.status == LGraph.STATUS_RUNNING )
      -		return;
      -	this.status = LGraph.STATUS_RUNNING;
      -
      -	if(this.onPlayEvent)
      -		this.onPlayEvent();
      -
      -	this.sendEventToAllNodes("onStart");
      -
      -	//launch
      -	this.starttime = LiteGraph.getTime();
      -	interval = interval || 1;
      -	var that = this;
      -
      -	this.execution_timer_id = setInterval( function() {
      -		//execute
      -		that.runStep(1, !this.catch_errors );
      -	},interval);
      -}
      -
      -/**
      -* Stops the execution loop of the graph
      -* @method stop execution
      -*/
      -
      -LGraph.prototype.stop = function()
      -{
      -	if( this.status == LGraph.STATUS_STOPPED )
      -		return;
      -
      -	this.status = LGraph.STATUS_STOPPED;
      -
      -	if(this.onStopEvent)
      -		this.onStopEvent();
      -
      -	if(this.execution_timer_id != null)
      -		clearInterval(this.execution_timer_id);
      -	this.execution_timer_id = null;
      -
      -	this.sendEventToAllNodes("onStop");
      -}
      -
      -/**
      -* Run N steps (cycles) of the graph
      -* @method runStep
      -* @param {number} num number of steps to run, default is 1
      -*/
      -
      -LGraph.prototype.runStep = function( num, do_not_catch_errors )
      -{
      -	num = num || 1;
      -
      -	var start = LiteGraph.getTime();
      -	this.globaltime = 0.001 * (start - this.starttime);
      -
      -	var nodes = this._nodes_executable ? this._nodes_executable : this._nodes;
      -	if(!nodes)
      -		return;
      -
      -	if( do_not_catch_errors )
      -	{
      -		//iterations
      -		for(var i = 0; i < num; i++)
      -		{
      -			for( var j = 0, l = nodes.length; j < l; ++j )
      -			{
      -				var node = nodes[j];
      -				if( node.mode == LiteGraph.ALWAYS && node.onExecute )
      -					node.onExecute();
      -			}
      -
      -			this.fixedtime += this.fixedtime_lapse;
      -			if( this.onExecuteStep )
      -				this.onExecuteStep();
      -		}
      -
      -		if( this.onAfterExecute )
      -			this.onAfterExecute();
      -	}
      -	else
      -	{
      -		try
      -		{
      -			//iterations
      -			for(var i = 0; i < num; i++)
      -			{
      -				for( var j = 0, l = nodes.length; j < l; ++j )
      -				{
      -					var node = nodes[j];
      -					if( node.mode == LiteGraph.ALWAYS && node.onExecute )
      -						node.onExecute();
      -				}
      -
      -				this.fixedtime += this.fixedtime_lapse;
      -				if( this.onExecuteStep )
      -					this.onExecuteStep();
      -			}
      -
      -			if( this.onAfterExecute )
      -				this.onAfterExecute();
      -			this.errors_in_execution = false;
      -		}
      -		catch (err)
      -		{
      -			this.errors_in_execution = true;
      -			if(LiteGraph.throw_errors)
      -				throw err;
      -			if(LiteGraph.debug)
      -				console.log("Error during execution: " + err);
      -			this.stop();
      -		}
      -	}
      -
      -	var elapsed = LiteGraph.getTime() - start;
      -	if (elapsed == 0)
      -		elapsed = 1;
      -	this.elapsed_time = 0.001 * elapsed;
      -	this.globaltime += 0.001 * elapsed;
      -	this.iteration += 1;
      -}
      -
      -/**
      -* Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than
      -* nodes with only inputs.
      -* @method updateExecutionOrder
      -*/
      -LGraph.prototype.updateExecutionOrder = function()
      -{
      -	this._nodes_in_order = this.computeExecutionOrder( false );
      -	this._nodes_executable = [];
      -	for(var i = 0; i < this._nodes_in_order.length; ++i)
      -		if( this._nodes_in_order[i].onExecute )
      -			this._nodes_executable.push( this._nodes_in_order[i] );
      -}
      -
      -//This is more internal, it computes the order and returns it
      -LGraph.prototype.computeExecutionOrder = function( only_onExecute, set_level )
      -{
      -	var L = [];
      -	var S = [];
      -	var M = {};
      -	var visited_links = {}; //to avoid repeating links
      -	var remaining_links = {}; //to a
      -
      -	//search for the nodes without inputs (starting nodes)
      -	for (var i = 0, l = this._nodes.length; i < l; ++i)
      -	{
      -		var node = this._nodes[i];
      -		if( only_onExecute && !node.onExecute )
      -			continue;
      -
      -		M[node.id] = node; //add to pending nodes
      -
      -		var num = 0; //num of input connections
      -		if(node.inputs)
      -			for(var j = 0, l2 = node.inputs.length; j < l2; j++)
      -				if(node.inputs[j] && node.inputs[j].link != null)
      -					num += 1;
      -
      -		if(num == 0) //is a starting node
      -		{
      -			S.push(node);
      -			if(set_level)
      -				node._level = 1;
      -		}
      -		else //num of input links
      -		{
      -			if(set_level)
      -				node._level = 0;
      -			remaining_links[node.id] = num;
      -		}
      -	}
      -
      -	while(true)
      -	{
      -		if(S.length == 0)
      -			break;
      -
      -		//get an starting node
      -		var node = S.shift();
      -		L.push(node); //add to ordered list
      -		delete M[node.id]; //remove from the pending nodes
      -
      -		if(!node.outputs)
      -			continue;
      -
      -		//for every output
      -		for(var i = 0; i < node.outputs.length; i++)
      -		{
      -			var output = node.outputs[i];
      -			//not connected
      -			if(output == null || output.links == null || output.links.length == 0)
      -				continue;
      -
      -			//for every connection
      -			for(var j = 0; j < output.links.length; j++)
      -			{
      -				var link_id = output.links[j];
      -				var link = this.links[link_id];
      -				if(!link)
      -					continue;
      -
      -				//already visited link (ignore it)
      -				if(visited_links[ link.id ])
      -					continue;
      -
      -				var target_node = this.getNodeById( link.target_id );
      -				if(target_node == null)
      -				{
      -					visited_links[ link.id ] = true;
      -					continue;
      -				}
      -
      -				if(set_level && (!target_node._level || target_node._level <= node._level))
      -					target_node._level = node._level + 1;
      -
      -				visited_links[link.id] = true; //mark as visited
      -				remaining_links[target_node.id] -= 1; //reduce the number of links remaining
      -				if (remaining_links[ target_node.id ] == 0)
      -					S.push(target_node); //if no more links, then add to starters array
      -			}
      -		}
      -	}
      -
      -	//the remaining ones (loops)
      -	for(var i in M)
      -		L.push( M[i] );
      -
      -	if( L.length != this._nodes.length && LiteGraph.debug )
      -		console.warn("something went wrong, nodes missing");
      -
      -	//save order number in the node
      -	for(var i = 0; i < L.length; ++i)
      -		L[i].order = i;
      -
      -	return L;
      -}
      -
      -/**
      -* Positions every node in a more readable manner
      -* @method arrange
      -*/
      -LGraph.prototype.arrange = function( margin )
      -{
      -	margin = margin || 40;
      -
      -	var nodes = this.computeExecutionOrder( false, true );
      -	var columns = [];
      -	for(var i = 0; i < nodes.length; ++i)
      -	{
      -		var node = nodes[i];
      -		var col = node._level || 1;
      -		if(!columns[col])
      -			columns[col] = [];
      -		columns[col].push( node );
      -	}
      -
      -	var x = margin;
      -
      -	for(var i = 0; i < columns.length; ++i)
      -	{
      -		var column = columns[i];
      -		if(!column)
      -			continue;
      -		var max_size = 100;
      -		var y = margin;
      -		for(var j = 0; j < column.length; ++j)
      -		{
      -			var node = column[j];
      -			node.pos[0] = x;
      -			node.pos[1] = y;
      -			if(node.size[0] > max_size)
      -				max_size = node.size[0];
      -			y += node.size[1] + margin;
      -		}
      -		x += max_size + margin;
      -	}
      -
      -	this.setDirtyCanvas(true,true);
      -}
      -
      -
      -/**
      -* Returns the amount of time the graph has been running in milliseconds
      -* @method getTime
      -* @return {number} number of milliseconds the graph has been running
      -*/
      -LGraph.prototype.getTime = function()
      -{
      -	return this.globaltime;
      -}
      -
      -/**
      -* Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant
      -* @method getFixedTime
      -* @return {number} number of milliseconds the graph has been running
      -*/
      -
      -LGraph.prototype.getFixedTime = function()
      -{
      -	return this.fixedtime;
      -}
      -
      -/**
      -* Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct
      -* if the nodes are using graphical actions
      -* @method getElapsedTime
      -* @return {number} number of milliseconds it took the last cycle
      -*/
      -
      -LGraph.prototype.getElapsedTime = function()
      -{
      -	return this.elapsed_time;
      -}
      -
      -/**
      -* Sends an event to all the nodes, useful to trigger stuff
      -* @method sendEventToAllNodes
      -* @param {String} eventname the name of the event (function to be called)
      -* @param {Array} params parameters in array format
      -*/
      -
      -LGraph.prototype.sendEventToAllNodes = function( eventname, params, mode )
      -{
      -	mode = mode || LiteGraph.ALWAYS;
      -
      -	var nodes = this._nodes_in_order ? this._nodes_in_order : this._nodes;
      -	if(!nodes)
      -		return;
      -
      -	for( var j = 0, l = nodes.length; j < l; ++j )
      -	{
      -		var node = nodes[j];
      -		if(node[eventname] && node.mode == mode )
      -		{
      -			if(params === undefined)
      -				node[eventname]();
      -			else if(params && params.constructor === Array)
      -				node[eventname].apply( node, params );
      -			else
      -				node[eventname](params);
      -		}
      -	}
      -}
      -
      -LGraph.prototype.sendActionToCanvas = function(action, params)
      -{
      -	if(!this.list_of_graphcanvas)
      -		return;
      -
      -	for(var i = 0; i < this.list_of_graphcanvas.length; ++i)
      -	{
      -		var c = this.list_of_graphcanvas[i];
      -		if( c[action] )
      -			c[action].apply(c, params);
      -	}
      -}
      -
      -/**
      -* Adds a new node instasnce to this graph
      -* @method add
      -* @param {LGraphNode} node the instance of the node
      -*/
      -
      -LGraph.prototype.add = function(node, skip_compute_order)
      -{
      -	if(!node)
      -		return;
      -	if(node.id != -1 && this._nodes_by_id[node.id] != null)
      -	{
      -		console.warn("LiteGraph: there is already a node with this ID, changing it");
      -		node.id = ++this.last_node_id;
      -	}
      -
      -	if(this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES)
      -		throw("LiteGraph: max number of nodes in a graph reached");
      -
      -	//give him an id
      -	if(node.id == null || node.id == -1)
      -		node.id = ++this.last_node_id;
      -	else if (this.last_node_id < node.id)
      -		this.last_node_id = node.id;
      -
      -	node.graph = this;
      -
      -	this._nodes.push(node);
      -	this._nodes_by_id[node.id] = node;
      -
      -	/*
      -	// rendering stuf...
      -	if(node.bgImageUrl)
      -		node.bgImage = node.loadImage(node.bgImageUrl);
      -	*/
      -
      -	if(node.onAdded)
      -		node.onAdded( this );
      -
      -	if(this.config.align_to_grid)
      -		node.alignToGrid();
      -
      -	if(!skip_compute_order)
      -		this.updateExecutionOrder();
      -
      -	if(this.onNodeAdded)
      -		this.onNodeAdded(node);
      -
      -
      -	this.setDirtyCanvas(true);
      -
      -	this.change();
      -
      -	return node; //to chain actions
      -}
      -
      -/**
      -* Removes a node from the graph
      -* @method remove
      -* @param {LGraphNode} node the instance of the node
      -*/
      -
      -LGraph.prototype.remove = function(node)
      -{
      -	if(this._nodes_by_id[node.id] == null)
      -		return; //not found
      -
      -	if(node.ignore_remove)
      -		return; //cannot be removed
      -
      -	//disconnect inputs
      -	if(node.inputs)
      -		for(var i = 0; i < node.inputs.length; i++)
      -		{
      -			var slot = node.inputs[i];
      -			if(slot.link != null)
      -				node.disconnectInput(i);
      -		}
      -
      -	//disconnect outputs
      -	if(node.outputs)
      -		for(var i = 0; i < node.outputs.length; i++)
      -		{
      -			var slot = node.outputs[i];
      -			if(slot.links != null && slot.links.length)
      -				node.disconnectOutput(i);
      -		}
      -
      -	//node.id = -1; //why?
      -
      -	//callback
      -	if(node.onRemoved)
      -		node.onRemoved();
      -
      -	node.graph = null;
      -
      -	//remove from canvas render
      -	if(this.list_of_graphcanvas)
      -	{
      -		for(var i = 0; i < this.list_of_graphcanvas.length; ++i)
      -		{
      -			var canvas = this.list_of_graphcanvas[i];
      -			if(canvas.selected_nodes[node.id])
      -				delete canvas.selected_nodes[node.id];
      -			if(canvas.node_dragged == node)
      -				canvas.node_dragged = null;
      -		}
      -	}
      -
      -	//remove from containers
      -	var pos = this._nodes.indexOf(node);
      -	if(pos != -1)
      -		this._nodes.splice(pos,1);
      -	delete this._nodes_by_id[node.id];
      -
      -	if(this.onNodeRemoved)
      -		this.onNodeRemoved(node);
      -
      -	this.setDirtyCanvas(true,true);
      -
      -	this.change();
      -
      -	this.updateExecutionOrder();
      -}
      -
      -/**
      -* Returns a node by its id.
      -* @method getNodeById
      -* @param {Number} id
      -*/
      -
      -LGraph.prototype.getNodeById = function( id )
      -{
      -	if( id == null )
      -		return null;
      -	return this._nodes_by_id[ id ];
      -}
      -
      -/**
      -* Returns a list of nodes that matches a class
      -* @method findNodesByClass
      -* @param {Class} classObject the class itself (not an string)
      -* @return {Array} a list with all the nodes of this type
      -*/
      -
      -LGraph.prototype.findNodesByClass = function(classObject)
      -{
      -	var r = [];
      -	for(var i = 0, l = this._nodes.length; i < l; ++i)
      -		if(this._nodes[i].constructor === classObject)
      -			r.push(this._nodes[i]);
      -	return r;
      -}
      -
      -/**
      -* Returns a list of nodes that matches a type
      -* @method findNodesByType
      -* @param {String} type the name of the node type
      -* @return {Array} a list with all the nodes of this type
      -*/
      -
      -LGraph.prototype.findNodesByType = function(type)
      -{
      -	var type = type.toLowerCase();
      -	var r = [];
      -	for(var i = 0, l = this._nodes.length; i < l; ++i)
      -		if(this._nodes[i].type.toLowerCase() == type )
      -			r.push(this._nodes[i]);
      -	return r;
      -}
      -
      -/**
      -* Returns a list of nodes that matches a name
      -* @method findNodesByTitle
      -* @param {String} name the name of the node to search
      -* @return {Array} a list with all the nodes with this name
      -*/
      -
      -LGraph.prototype.findNodesByTitle = function(title)
      -{
      -	var result = [];
      -	for(var i = 0, l = this._nodes.length; i < l; ++i)
      -		if(this._nodes[i].title == title)
      -			result.push(this._nodes[i]);
      -	return result;
      -}
      -
      -/**
      -* Returns the top-most node in this position of the canvas
      -* @method getNodeOnPos
      -* @param {number} x the x coordinate in canvas space
      -* @param {number} y the y coordinate in canvas space
      -* @param {Array} nodes_list a list with all the nodes to search from, by default is all the nodes in the graph
      -* @return {Array} a list with all the nodes that intersect this coordinate
      -*/
      -LGraph.prototype.getNodeOnPos = function(x,y, nodes_list)
      -{
      -	nodes_list = nodes_list || this._nodes;
      -	for (var i = nodes_list.length - 1; i >= 0; i--)
      -	{
      -		var n = nodes_list[i];
      -		if(n.isPointInsideNode( x, y, 2 ))
      -			return n;
      -	}
      -	return null;
      -}
      -
      -// ********** GLOBALS *****************
      -
      -/**
      -* Tell this graph it has a global graph input of this type
      -* @method addGlobalInput
      -* @param {String} name
      -* @param {String} type
      -* @param {*} value [optional]
      -*/
      -LGraph.prototype.addGlobalInput = function(name, type, value)
      -{
      -	this.global_inputs[name] = { name: name, type: type, value: value };
      -
      -	if(this.onGlobalInputAdded)
      -		this.onGlobalInputAdded(name, type);
      -
      -	if(this.onGlobalsChange)
      -		this.onGlobalsChange();
      -}
      -
      -/**
      -* Assign a data to the global graph input
      -* @method setGlobalInputData
      -* @param {String} name
      -* @param {*} data
      -*/
      -LGraph.prototype.setGlobalInputData = function(name, data)
      -{
      -	var input = this.global_inputs[name];
      -	if (!input)
      -		return;
      -	input.value = data;
      -}
      -
      -/**
      -* Assign a data to the global graph input (same as setGlobalInputData)
      -* @method setInputData
      -* @param {String} name
      -* @param {*} data
      -*/
      -LGraph.prototype.setInputData = LGraph.prototype.setGlobalInputData;
      -
      -
      -/**
      -* Returns the current value of a global graph input
      -* @method getGlobalInputData
      -* @param {String} name
      -* @return {*} the data
      -*/
      -LGraph.prototype.getGlobalInputData = function(name)
      -{
      -	var input = this.global_inputs[name];
      -	if (!input)
      -		return null;
      -	return input.value;
      -}
      -
      -/**
      -* Changes the name of a global graph input
      -* @method renameGlobalInput
      -* @param {String} old_name
      -* @param {String} new_name
      -*/
      -LGraph.prototype.renameGlobalInput = function(old_name, name)
      -{
      -	if(name == old_name)
      -		return;
      -
      -	if(!this.global_inputs[old_name])
      -		return false;
      -
      -	if(this.global_inputs[name])
      -	{
      -		console.error("there is already one input with that name");
      -		return false;
      -	}
      -
      -	this.global_inputs[name] = this.global_inputs[old_name];
      -	delete this.global_inputs[old_name];
      -
      -	if(this.onGlobalInputRenamed)
      -		this.onGlobalInputRenamed(old_name, name);
      -
      -	if(this.onGlobalsChange)
      -		this.onGlobalsChange();
      -}
      -
      -/**
      -* Changes the type of a global graph input
      -* @method changeGlobalInputType
      -* @param {String} name
      -* @param {String} type
      -*/
      -LGraph.prototype.changeGlobalInputType = function(name, type)
      -{
      -	if(!this.global_inputs[name])
      -		return false;
      -
      -	if(this.global_inputs[name].type.toLowerCase() == type.toLowerCase() )
      -		return;
      -
      -	this.global_inputs[name].type = type;
      -	if(this.onGlobalInputTypeChanged)
      -		this.onGlobalInputTypeChanged(name, type);
      -}
      -
      -/**
      -* Removes a global graph input
      -* @method removeGlobalInput
      -* @param {String} name
      -* @param {String} type
      -*/
      -LGraph.prototype.removeGlobalInput = function(name)
      -{
      -	if(!this.global_inputs[name])
      -		return false;
      -
      -	delete this.global_inputs[name];
      -
      -	if(this.onGlobalInputRemoved)
      -		this.onGlobalInputRemoved(name);
      -
      -	if(this.onGlobalsChange)
      -		this.onGlobalsChange();
      -	return true;
      -}
      -
      -/**
      -* Creates a global graph output
      -* @method addGlobalOutput
      -* @param {String} name
      -* @param {String} type
      -* @param {*} value
      -*/
      -LGraph.prototype.addGlobalOutput = function(name, type, value)
      -{
      -	this.global_outputs[name] = { name: name, type: type, value: value };
      -
      -	if(this.onGlobalOutputAdded)
      -		this.onGlobalOutputAdded(name, type);
      -
      -	if(this.onGlobalsChange)
      -		this.onGlobalsChange();
      -}
      -
      -/**
      -* Assign a data to the global output
      -* @method setGlobalOutputData
      -* @param {String} name
      -* @param {String} value
      -*/
      -LGraph.prototype.setGlobalOutputData = function(name, value)
      -{
      -	var output = this.global_outputs[ name ];
      -	if (!output)
      -		return;
      -	output.value = value;
      -}
      -
      -/**
      -* Returns the current value of a global graph output
      -* @method getGlobalOutputData
      -* @param {String} name
      -* @return {*} the data
      -*/
      -LGraph.prototype.getGlobalOutputData = function(name)
      -{
      -	var output = this.global_outputs[name];
      -	if (!output)
      -		return null;
      -	return output.value;
      -}
      -
      -/**
      -* Returns the current value of a global graph output (sames as getGlobalOutputData)
      -* @method getOutputData
      -* @param {String} name
      -* @return {*} the data
      -*/
      -LGraph.prototype.getOutputData = LGraph.prototype.getGlobalOutputData;
      -
      -
      -/**
      -* Renames a global graph output
      -* @method renameGlobalOutput
      -* @param {String} old_name
      -* @param {String} new_name
      -*/
      -LGraph.prototype.renameGlobalOutput = function(old_name, name)
      -{
      -	if(!this.global_outputs[old_name])
      -		return false;
      -
      -	if(this.global_outputs[name])
      -	{
      -		console.error("there is already one output with that name");
      -		return false;
      -	}
      -
      -	this.global_outputs[name] = this.global_outputs[old_name];
      -	delete this.global_outputs[old_name];
      -
      -	if(this.onGlobalOutputRenamed)
      -		this.onGlobalOutputRenamed(old_name, name);
      -
      -	if(this.onGlobalsChange)
      -		this.onGlobalsChange();
      -}
      -
      -/**
      -* Changes the type of a global graph output
      -* @method changeGlobalOutputType
      -* @param {String} name
      -* @param {String} type
      -*/
      -LGraph.prototype.changeGlobalOutputType = function(name, type)
      -{
      -	if(!this.global_outputs[name])
      -		return false;
      -
      -	if(this.global_outputs[name].type.toLowerCase() == type.toLowerCase() )
      -		return;
      -
      -	this.global_outputs[name].type = type;
      -	if(this.onGlobalOutputTypeChanged)
      -		this.onGlobalOutputTypeChanged(name, type);
      -}
      -
      -/**
      -* Removes a global graph output
      -* @method removeGlobalOutput
      -* @param {String} name
      -*/
      -LGraph.prototype.removeGlobalOutput = function(name)
      -{
      -	if(!this.global_outputs[name])
      -		return false;
      -	delete this.global_outputs[name];
      -
      -	if(this.onGlobalOutputRemoved)
      -		this.onGlobalOutputRemoved(name);
      -
      -	if(this.onGlobalsChange)
      -		this.onGlobalsChange();
      -	return true;
      -}
      -
      -LGraph.prototype.triggerInput = function(name,value)
      -{
      -	var nodes = this.findNodesByTitle(name);
      -	for(var i = 0; i < nodes.length; ++i)
      -		nodes[i].onTrigger(value);
      -}
      -
      -LGraph.prototype.setCallback = function(name,func)
      -{
      -	var nodes = this.findNodesByTitle(name);
      -	for(var i = 0; i < nodes.length; ++i)
      -		nodes[i].setTrigger(func);
      -}
      -
      -
      -LGraph.prototype.connectionChange = function( node )
      -{
      -	this.updateExecutionOrder();
      -	if( this.onConnectionChange )
      -		this.onConnectionChange( node );
      -	this.sendActionToCanvas("onConnectionChange");
      -}
      -
      -/**
      -* returns if the graph is in live mode
      -* @method isLive
      -*/
      -
      -LGraph.prototype.isLive = function()
      -{
      -	if(!this.list_of_graphcanvas)
      -		return false;
      -
      -	for(var i = 0; i < this.list_of_graphcanvas.length; ++i)
      -	{
      -		var c = this.list_of_graphcanvas[i];
      -		if(c.live_mode)
      -			return true;
      -	}
      -	return false;
      -}
      -
      -/* Called when something visually changed */
      -LGraph.prototype.change = function()
      -{
      -	if(LiteGraph.debug)
      -		console.log("Graph changed");
      -
      -	this.sendActionToCanvas("setDirty",[true,true]);
      -
      -	if(this.on_change)
      -		this.on_change(this);
      -}
      -
      -LGraph.prototype.setDirtyCanvas = function(fg,bg)
      -{
      -	this.sendActionToCanvas("setDirty",[fg,bg]);
      -}
      -
      -//save and recover app state ***************************************
      -/**
      -* Creates a Object containing all the info about this graph, it can be serialized
      -* @method serialize
      -* @return {Object} value of the node
      -*/
      -LGraph.prototype.serialize = function()
      -{
      -	var nodes_info = [];
      -	for(var i = 0, l = this._nodes.length; i < l; ++i)
      -		nodes_info.push( this._nodes[i].serialize() );
      -
      -	//pack link info into a non-verbose format
      -	var links = [];
      -	for(var i in this.links) //links is an OBJECT
      -	{
      -		var link = this.links[i];
      -		links.push([ link.id, link.origin_id, link.origin_slot, link.target_id, link.target_slot, link.type ]);
      -	}
      -
      -	var data = {
      -		iteration: this.iteration,
      -		frame: this.frame,
      -		last_node_id: this.last_node_id,
      -		last_link_id: this.last_link_id,
      -		links: links, //LiteGraph.cloneObject( this.links ),
      -		config: this.config,
      -		nodes: nodes_info
      -	};
      -
      -	return data;
      -}
      -
      -
      -/**
      -* Configure a graph from a JSON string
      -* @method configure
      -* @param {String} str configure a graph from a JSON string
      -*/
      -LGraph.prototype.configure = function(data, keep_old)
      -{
      -	if(!keep_old)
      -		this.clear();
      -
      -	var nodes = data.nodes;
      -
      -	//decode links info (they are very verbose)
      -	if(data.links && data.links.constructor === Array)
      -	{
      -		var links = {};
      -		for(var i = 0; i < data.links.length; ++i)
      -		{
      -			var link = data.links[i];
      -			links[ link[0] ] = { id: link[0], origin_id: link[1], origin_slot: link[2], target_id: link[3], target_slot: link[4], type: link[5] };
      -		}
      -		data.links = links;
      -	}
      -
      -	//copy all stored fields
      -	for (var i in data)
      -		this[i] = data[i];
      -
      -	var error = false;
      -
      -	//create nodes
      -	this._nodes = [];
      -	for(var i = 0, l = nodes.length; i < l; ++i)
      -	{
      -		var n_info = nodes[i]; //stored info
      -		var node = LiteGraph.createNode( n_info.type, n_info.title );
      -		if(!node)
      -		{
      -			if(LiteGraph.debug)
      -				console.log("Node not found: " + n_info.type);
      -			error = true;
      -			continue;
      -		}
      -
      -		node.id = n_info.id; //id it or it will create a new id
      -		this.add(node, true); //add before configure, otherwise configure cannot create links
      -	}
      -
      -	//configure nodes afterwards so they can reach each other
      -	for(var i = 0, l = nodes.length; i < l; ++i)
      -	{
      -		var n_info = nodes[i];
      -		var node = this.getNodeById( n_info.id );
      -		if(node)
      -			node.configure( n_info );
      -	}
      -
      -	this.updateExecutionOrder();
      -	this.setDirtyCanvas(true,true);
      -	return error;
      -}
      -
      -LGraph.prototype.load = function(url)
      -{
      -	var that = this;
      -	var req = new XMLHttpRequest();
      -	req.open('GET', url, true);
      -	req.send(null);
      -	req.onload = function (oEvent) {
      -		if(req.status !== 200)
      -		{
      -			console.error("Error loading graph:",req.status,req.response);
      -			return;
      -		}
      -		var data = JSON.parse( req.response );
      -		that.configure(data);
      -	}
      -	req.onerror = function(err)
      -	{
      -		console.error("Error loading graph:",err);
      -	}
      -}
      -
      -LGraph.prototype.onNodeTrace = function(node, msg, color)
      -{
      -	//TODO
      -}
      -
      -// *************************************************************
      -//   Node CLASS                                          *******
      -// *************************************************************
      -
      -/*
      -	title: string
      -	pos: [x,y]
      -	size: [x,y]
      -
      -	input|output: every connection
      -		+  { name:string, type:string, pos: [x,y]=Optional, direction: "input"|"output", links: Array });
      -
      -	flags:
      -		+ skip_title_render
      -		+ clip_area
      -		+ unsafe_execution: not allowed for safe execution
      -		+ skip_repeated_outputs: when adding new outputs, it wont show if there is one already connected
      -
      -	supported callbacks:
      -		+ onAdded: when added to graph
      -		+ onRemoved: when removed from graph
      -		+ onStart:	when the graph starts playing
      -		+ onStop:	when the graph stops playing
      -		+ onDrawForeground: render the inside widgets inside the node
      -		+ onDrawBackground: render the background area inside the node (only in edit mode)
      -		+ onMouseDown
      -		+ onMouseMove
      -		+ onMouseUp
      -		+ onMouseEnter
      -		+ onMouseLeave
      -		+ onExecute: execute the node
      -		+ onPropertyChanged: when a property is changed in the panel (return true to skip default behaviour)
      -		+ onGetInputs: returns an array of possible inputs
      -		+ onGetOutputs: returns an array of possible outputs
      -		+ onDblClick
      -		+ onSerialize
      -		+ onSelected
      -		+ onDeselected
      -		+ onDropItem : DOM item dropped over the node
      -		+ onDropFile : file dropped over the node
      -		+ onConnectInput : if returns false the incoming connection will be canceled
      -		+ onConnectionsChange : a connection changed (new one or removed) (LiteGraph.INPUT or LiteGraph.OUTPUT, slot, true if connected, link_info, input_info )
      -*/
      -
      -/**
      -* Base Class for all the node type classes
      -* @class LGraphNode
      -* @param {String} name a name for the node
      -*/
      -
      -function LGraphNode(title)
      -{
      -	this._ctor(title);
      -}
      -
      -global.LGraphNode = LiteGraph.LGraphNode = LGraphNode;
      -
      -LGraphNode.prototype._ctor = function( title )
      -{
      -	this.title = title || "Unnamed";
      -	this.size = [LiteGraph.NODE_WIDTH,60];
      -	this.graph = null;
      -
      -	this._pos = new Float32Array(10,10);
      -
      -	Object.defineProperty( this, "pos", {
      -		set: function(v)
      -		{
      -			if(!v || !v.length < 2)
      -				return;
      -			this._pos[0] = v[0];
      -			this._pos[1] = v[1];
      -		},
      -		get: function()
      -		{
      -			return this._pos;
      -		},
      -		enumerable: true
      -	});
      -
      -	this.id = -1; //not know till not added
      -	this.type = null;
      -
      -	//inputs available: array of inputs
      -	this.inputs = [];
      -	this.outputs = [];
      -	this.connections = [];
      -
      -	//local data
      -	this.properties = {}; //for the values
      -	this.properties_info = []; //for the info
      -
      -	this.data = null; //persistent local data
      -	this.flags = {
      -		//skip_title_render: true,
      -		//unsafe_execution: false,
      -	};
      -}
      -
      -/**
      -* configure a node from an object containing the serialized info
      -* @method configure
      -*/
      -LGraphNode.prototype.configure = function(info)
      -{
      -	for (var j in info)
      -	{
      -		if(j == "console")
      -			continue;
      -
      -		if(j == "properties")
      -		{
      -			//i dont want to clone properties, I want to reuse the old container
      -			for(var k in info.properties)
      -			{
      -				this.properties[k] = info.properties[k];
      -				if(this.onPropertyChanged)
      -					this.onPropertyChanged(k,info.properties[k]);
      -			}
      -			continue;
      -		}
      -
      -		if(info[j] == null)
      -			continue;
      -
      -		else if (typeof(info[j]) == 'object') //object
      -		{
      -			if(this[j] && this[j].configure)
      -				this[j].configure( info[j] );
      -			else
      -				this[j] = LiteGraph.cloneObject(info[j], this[j]);
      -		}
      -		else //value
      -			this[j] = info[j];
      -	}
      -
      -	if(!info.title)
      -		this.title = this.constructor.title;
      -
      -	if(this.onConnectionsChange)
      -	{
      -		if(this.inputs)
      -		for(var i = 0; i < this.inputs.length; ++i)
      -		{
      -			var input = this.inputs[i];
      -			var link_info = this.graph.links[ input.link ];
      -			this.onConnectionsChange( LiteGraph.INPUT, i, true, link_info, input ); //link_info has been created now, so its updated
      -		}
      -
      -		if(this.outputs)
      -		for(var i = 0; i < this.outputs.length; ++i)
      -		{
      -			var output = this.outputs[i];
      -			if(!output.links)
      -				continue;
      -			for(var j = 0; j < output.links.length; ++j)
      -			{
      -				var link_info = this.graph.links[ output.links[j] ];
      -				this.onConnectionsChange( LiteGraph.OUTPUT, i, true, link_info, output ); //link_info has been created now, so its updated
      -			}
      -		}
      -	}
      -
      -	//FOR LEGACY, PLEASE REMOVE ON NEXT VERSION
      -	for(var i in this.inputs)
      -	{
      -		var input = this.inputs[i];
      -		if(!input.link || !input.link.length )
      -			continue;
      -		var link = input.link;
      -		if(typeof(link) != "object")
      -			continue;
      -		input.link = link[0];
      -		this.graph.links[ link[0] ] = {
      -			id: link[0],
      -			origin_id: link[1],
      -			origin_slot: link[2],
      -			target_id: link[3],
      -			target_slot: link[4]
      -		};
      -	}
      -	for(var i in this.outputs)
      -	{
      -		var output = this.outputs[i];
      -		if(!output.links || output.links.length == 0)
      -			continue;
      -		for(var j in output.links)
      -		{
      -			var link = output.links[j];
      -			if(typeof(link) != "object")
      -				continue;
      -			output.links[j] = link[0];
      -		}
      -	}
      -
      -	if( this.onConfigure )
      -		this.onConfigure( info );
      -}
      -
      -/**
      -* serialize the content
      -* @method serialize
      -*/
      -
      -LGraphNode.prototype.serialize = function()
      -{
      -	//create serialization object
      -	var o = {
      -		id: this.id,
      -		type: this.type,
      -		pos: this.pos,
      -		size: this.size,
      -		data: this.data,
      -		flags: LiteGraph.cloneObject(this.flags),
      -		mode: this.mode
      -	};
      -
      -	if(this.inputs)
      -		o.inputs = this.inputs;
      -
      -	if(this.outputs)
      -	{
      -		//clear outputs last data (because data in connections is never serialized but stored inside the outputs info)
      -		for(var i = 0; i < this.outputs.length; i++)
      -			delete this.outputs[i]._data;
      -		o.outputs = this.outputs;
      -	}
      -
      -	if( this.title && this.title != this.constructor.title )
      -		o.title = this.title;
      -
      -	if(this.properties)
      -		o.properties = LiteGraph.cloneObject(this.properties);
      -
      -	if(!o.type)
      -		o.type = this.constructor.type;
      -
      -	if(this.color)
      -		o.color = this.color;
      -	if(this.bgcolor)
      -		o.bgcolor = this.bgcolor;
      -	if(this.boxcolor)
      -		o.boxcolor = this.boxcolor;
      -	if(this.shape)
      -		o.shape = this.shape;
      -
      -	if(this.onSerialize)
      -		this.onSerialize(o);
      -
      -	return o;
      -}
      -
      -
      -/* Creates a clone of this node */
      -LGraphNode.prototype.clone = function()
      -{
      -	var node = LiteGraph.createNode(this.type);
      -
      -	//we clone it because serialize returns shared containers
      -	var data = LiteGraph.cloneObject( this.serialize() );
      -
      -	//remove links
      -	if(data.inputs)
      -		for(var i = 0; i < data.inputs.length; ++i)
      -			data.inputs[i].link = null;
      -
      -	if(data.outputs)
      -		for(var i = 0; i < data.outputs.length; ++i)
      -		{
      -			if(data.outputs[i].links)
      -				data.outputs[i].links.length = 0;
      -		}
      -
      -	delete data["id"];
      -	//remove links
      -	node.configure(data);
      -
      -	return node;
      -}
      -
      -
      -/**
      -* serialize and stringify
      -* @method toString
      -*/
      -
      -LGraphNode.prototype.toString = function()
      -{
      -	return JSON.stringify( this.serialize() );
      -}
      -//LGraphNode.prototype.unserialize = function(info) {} //this cannot be done from within, must be done in LiteGraph
      -
      -
      -/**
      -* get the title string
      -* @method getTitle
      -*/
      -
      -LGraphNode.prototype.getTitle = function()
      -{
      -	return this.title || this.constructor.title;
      -}
      -
      -
      -
      -// Execution *************************
      -/**
      -* sets the output data
      -* @method setOutputData
      -* @param {number} slot
      -* @param {*} data
      -*/
      -LGraphNode.prototype.setOutputData = function(slot, data)
      -{
      -	if(!this.outputs)
      -		return;
      -
      -	//this maybe slow and a niche case
      -	//if(slot && slot.constructor === String)
      -	//	slot = this.findOutputSlot(slot);
      -
      -	if(slot == -1 || slot >= this.outputs.length)
      -		return;
      -
      -	var output_info = this.outputs[slot];
      -	if(!output_info)
      -		return;
      -
      -	//store data in the output itself in case we want to debug
      -	output_info._data = data;
      -
      -	//if there are connections, pass the data to the connections
      -	if( this.outputs[slot].links )
      -	{
      -		for(var i = 0; i < this.outputs[slot].links.length; i++)
      -		{
      -			var link_id = this.outputs[slot].links[i];
      -			this.graph.links[ link_id ].data = data;
      -		}
      -	}
      -}
      -
      -/**
      -* Retrieves the input data (data traveling through the connection) from one slot
      -* @method getInputData
      -* @param {number} slot
      -* @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link
      -* @return {*} data or if it is not connected returns undefined
      -*/
      -LGraphNode.prototype.getInputData = function( slot, force_update )
      -{
      -	if(!this.inputs)
      -		return; //undefined;
      -
      -	if(slot >= this.inputs.length || this.inputs[slot].link == null)
      -		return;
      -
      -	var link_id = this.inputs[slot].link;
      -	var link = this.graph.links[ link_id ];
      -	if(!link) //bug: weird case but it happens sometimes
      -		return null;
      -
      -	if(!force_update)
      -		return link.data;
      -
      -	//special case: used to extract data from the incomming connection before the graph has been executed
      -	var node = this.graph.getNodeById( link.origin_id );
      -	if(!node)
      -		return link.data;
      -
      -	if(node.updateOutputData)
      -		node.updateOutputData( link.origin_slot );
      -	else if(node.onExecute)
      -		node.onExecute();
      -
      -	return link.data;
      -}
      -
      -/**
      -* Retrieves the input data from one slot using its name instead of slot number
      -* @method getInputDataByName
      -* @param {String} slot_name
      -* @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link
      -* @return {*} data or if it is not connected returns null
      -*/
      -LGraphNode.prototype.getInputDataByName = function( slot_name, force_update )
      -{
      -	var slot = this.findInputSlot( slot_name );
      -	if( slot == -1 )
      -		return null;
      -	return this.getInputData( slot, force_update );
      -}
      -
      -
      -/**
      -* tells you if there is a connection in one input slot
      -* @method isInputConnected
      -* @param {number} slot
      -* @return {boolean}
      -*/
      -LGraphNode.prototype.isInputConnected = function(slot)
      -{
      -	if(!this.inputs)
      -		return false;
      -	return (slot < this.inputs.length && this.inputs[slot].link != null);
      -}
      -
      -/**
      -* tells you info about an input connection (which node, type, etc)
      -* @method getInputInfo
      -* @param {number} slot
      -* @return {Object} object or null { link: id, name: string, type: string or 0 }
      -*/
      -LGraphNode.prototype.getInputInfo = function(slot)
      -{
      -	if(!this.inputs)
      -		return null;
      -	if(slot < this.inputs.length)
      -		return this.inputs[slot];
      -	return null;
      -}
      -
      -/**
      -* returns the node connected in the input slot
      -* @method getInputNode
      -* @param {number} slot
      -* @return {LGraphNode} node or null
      -*/
      -LGraphNode.prototype.getInputNode = function( slot )
      -{
      -	if(!this.inputs)
      -		return null;
      -	if(slot >= this.inputs.length)
      -		return null;
      -	var input = this.inputs[slot];
      -	if(!input || !input.link)
      -		return null;
      -	var link_info = this.graph.links[ input.link ];
      -	if(!link_info)
      -		return null;
      -	return this.graph.getNodeById( link_info.origin_id );
      -}
      -
      -
      -/**
      -* returns the value of an input with this name, otherwise checks if there is a property with that name
      -* @method getInputOrProperty
      -* @param {string} name
      -* @return {*} value
      -*/
      -LGraphNode.prototype.getInputOrProperty = function( name )
      -{
      -	if(!this.inputs || !this.inputs.length)
      -		return this.properties ? this.properties[name] : null;
      -
      -	for(var i = 0, l = this.inputs.length; i < l; ++i)
      -		if(name == this.inputs[i].name)
      -		{
      -			var link_id = this.inputs[i].link;
      -			var link = this.graph.links[ link_id ];
      -			return link ? link.data : null;
      -		}
      -	return this.properties[name];
      -}
      -
      -
      -
      -
      -/**
      -* tells you the last output data that went in that slot
      -* @method getOutputData
      -* @param {number} slot
      -* @return {Object}  object or null
      -*/
      -LGraphNode.prototype.getOutputData = function(slot)
      -{
      -	if(!this.outputs)
      -		return null;
      -	if(slot >= this.outputs.length)
      -		return null;
      -
      -	var info = this.outputs[slot];
      -	return info._data;
      -}
      -
      -
      -/**
      -* tells you info about an output connection (which node, type, etc)
      -* @method getOutputInfo
      -* @param {number} slot
      -* @return {Object}  object or null { name: string, type: string, links: [ ids of links in number ] }
      -*/
      -LGraphNode.prototype.getOutputInfo = function(slot)
      -{
      -	if(!this.outputs)
      -		return null;
      -	if(slot < this.outputs.length)
      -		return this.outputs[slot];
      -	return null;
      -}
      -
      -
      -/**
      -* tells you if there is a connection in one output slot
      -* @method isOutputConnected
      -* @param {number} slot
      -* @return {boolean}
      -*/
      -LGraphNode.prototype.isOutputConnected = function(slot)
      -{
      -	if(!this.outputs)
      -		return false;
      -	return (slot < this.outputs.length && this.outputs[slot].links && this.outputs[slot].links.length);
      -}
      -
      -/**
      -* tells you if there is any connection in the output slots
      -* @method isAnyOutputConnected
      -* @return {boolean}
      -*/
      -LGraphNode.prototype.isAnyOutputConnected = function()
      -{
      -	if(!this.outputs)
      -		return false;
      -	for(var i = 0; i < this.outputs.length; ++i)
      -		if( this.outputs[i].links && this.outputs[i].links.length )
      -			return true;
      -	return false;
      -}
      -
      -
      -/**
      -* retrieves all the nodes connected to this output slot
      -* @method getOutputNodes
      -* @param {number} slot
      -* @return {array}
      -*/
      -LGraphNode.prototype.getOutputNodes = function(slot)
      -{
      -	if(!this.outputs || this.outputs.length == 0)
      -		return null;
      -
      -	if(slot >= this.outputs.length)
      -		return null;
      -
      -	var output = this.outputs[slot];
      -	if(!output.links || output.links.length == 0)
      -		return null;
      -
      -	var r = [];
      -	for(var i = 0; i < output.links.length; i++)
      -	{
      -		var link_id = output.links[i];
      -		var link = this.graph.links[ link_id ];
      -		if(link)
      -		{
      -			var target_node = this.graph.getNodeById( link.target_id );
      -			if( target_node )
      -				r.push( target_node );
      -		}
      -	}
      -	return r;
      -}
      -
      -/**
      -* Triggers an event in this node, this will trigger any output with the same name
      -* @method trigger
      -* @param {String} event name ( "on_play", ... ) if action is equivalent to false then the event is send to all
      -* @param {*} param
      -*/
      -LGraphNode.prototype.trigger = function( action, param )
      -{
      -	if( !this.outputs || !this.outputs.length )
      -		return;
      -
      -	if(this.graph)
      -		this.graph._last_trigger_time = LiteGraph.getTime();
      -
      -	for(var i = 0; i < this.outputs.length; ++i)
      -	{
      -		var output = this.outputs[ i ];
      -		if(!output || output.type !== LiteGraph.EVENT || (action && output.name != action) )
      -			continue;
      -		this.triggerSlot( i, param );
      -	}
      -}
      -
      -/**
      -* Triggers an slot event in this node
      -* @method triggerSlot
      -* @param {Number} slot the index of the output slot
      -* @param {*} param
      -*/
      -LGraphNode.prototype.triggerSlot = function( slot, param )
      -{
      -	if( !this.outputs )
      -		return;
      -
      -	var output = this.outputs[ slot ];
      -	if( !output )
      -		return;
      -
      -	var links = output.links;
      -	if(!links || !links.length)
      -		return;
      -
      -	if(this.graph)
      -		this.graph._last_trigger_time = LiteGraph.getTime();
      -
      -	//for every link attached here
      -	for(var k = 0; k < links.length; ++k)
      -	{
      -		var link_info = this.graph.links[ links[k] ];
      -		if(!link_info) //not connected
      -			continue;
      -		var node = this.graph.getNodeById( link_info.target_id );
      -		if(!node) //node not found?
      -			continue;
      -
      -		//used to mark events in graph
      -		link_info._last_time = LiteGraph.getTime();
      -
      -		var target_connection = node.inputs[ link_info.target_slot ];
      -
      -		if(node.onAction)
      -			node.onAction( target_connection.name, param );
      -		else if(node.mode === LiteGraph.ON_TRIGGER)
      -		{
      -			if(node.onExecute)
      -				node.onExecute(param);
      -		}
      -	}
      -}
      -
      -/**
      -* add a new property to this node
      -* @method addProperty
      -* @param {string} name
      -* @param {*} default_value
      -* @param {string} type string defining the output type ("vec3","number",...)
      -* @param {Object} extra_info this can be used to have special properties of the property (like values, etc)
      -*/
      -LGraphNode.prototype.addProperty = function( name, default_value, type, extra_info )
      -{
      -	var o = { name: name, type: type, default_value: default_value };
      -	if(extra_info)
      -		for(var i in extra_info)
      -			o[i] = extra_info[i];
      -	if(!this.properties_info)
      -		this.properties_info = [];
      -	this.properties_info.push(o);
      -	if(!this.properties)
      -		this.properties = {};
      -	this.properties[ name ] = default_value;
      -	return o;
      -}
      -
      -
      -//connections
      -
      -/**
      -* add a new output slot to use in this node
      -* @method addOutput
      -* @param {string} name
      -* @param {string} type string defining the output type ("vec3","number",...)
      -* @param {Object} extra_info this can be used to have special properties of an output (label, special color, position, etc)
      -*/
      -LGraphNode.prototype.addOutput = function(name,type,extra_info)
      -{
      -	var o = { name: name, type: type, links: null };
      -	if(extra_info)
      -		for(var i in extra_info)
      -			o[i] = extra_info[i];
      -
      -	if(!this.outputs)
      -		this.outputs = [];
      -	this.outputs.push(o);
      -	if(this.onOutputAdded)
      -		this.onOutputAdded(o);
      -	this.size = this.computeSize();
      -	return o;
      -}
      -
      -/**
      -* add a new output slot to use in this node
      -* @method addOutputs
      -* @param {Array} array of triplets like [[name,type,extra_info],[...]]
      -*/
      -LGraphNode.prototype.addOutputs = function(array)
      -{
      -	for(var i = 0; i < array.length; ++i)
      -	{
      -		var info = array[i];
      -		var o = {name:info[0],type:info[1],link:null};
      -		if(array[2])
      -			for(var j in info[2])
      -				o[j] = info[2][j];
      -
      -		if(!this.outputs)
      -			this.outputs = [];
      -		this.outputs.push(o);
      -		if(this.onOutputAdded)
      -			this.onOutputAdded(o);
      -	}
      -
      -	this.size = this.computeSize();
      -}
      -
      -/**
      -* remove an existing output slot
      -* @method removeOutput
      -* @param {number} slot
      -*/
      -LGraphNode.prototype.removeOutput = function(slot)
      -{
      -	this.disconnectOutput(slot);
      -	this.outputs.splice(slot,1);
      -	this.size = this.computeSize();
      -	if(this.onOutputRemoved)
      -		this.onOutputRemoved(slot);
      -}
      -
      -/**
      -* add a new input slot to use in this node
      -* @method addInput
      -* @param {string} name
      -* @param {string} type string defining the input type ("vec3","number",...), it its a generic one use 0
      -* @param {Object} extra_info this can be used to have special properties of an input (label, color, position, etc)
      -*/
      -LGraphNode.prototype.addInput = function(name,type,extra_info)
      -{
      -	type = type || 0;
      -	var o = {name:name,type:type,link:null};
      -	if(extra_info)
      -		for(var i in extra_info)
      -			o[i] = extra_info[i];
      -
      -	if(!this.inputs)
      -		this.inputs = [];
      -	this.inputs.push(o);
      -	this.size = this.computeSize();
      -	if(this.onInputAdded)
      -		this.onInputAdded(o);
      -	return o;
      -}
      -
      -/**
      -* add several new input slots in this node
      -* @method addInputs
      -* @param {Array} array of triplets like [[name,type,extra_info],[...]]
      -*/
      -LGraphNode.prototype.addInputs = function(array)
      -{
      -	for(var i = 0; i < array.length; ++i)
      -	{
      -		var info = array[i];
      -		var o = {name:info[0], type:info[1], link:null};
      -		if(array[2])
      -			for(var j in info[2])
      -				o[j] = info[2][j];
      -
      -		if(!this.inputs)
      -			this.inputs = [];
      -		this.inputs.push(o);
      -		if(this.onInputAdded)
      -			this.onInputAdded(o);
      -	}
      -
      -	this.size = this.computeSize();
      -}
      -
      -/**
      -* remove an existing input slot
      -* @method removeInput
      -* @param {number} slot
      -*/
      -LGraphNode.prototype.removeInput = function(slot)
      -{
      -	this.disconnectInput(slot);
      -	this.inputs.splice(slot,1);
      -	this.size = this.computeSize();
      -	if(this.onInputRemoved)
      -		this.onInputRemoved(slot);
      -}
      -
      -/**
      -* add an special connection to this node (used for special kinds of graphs)
      -* @method addConnection
      -* @param {string} name
      -* @param {string} type string defining the input type ("vec3","number",...)
      -* @param {[x,y]} pos position of the connection inside the node
      -* @param {string} direction if is input or output
      -*/
      -LGraphNode.prototype.addConnection = function(name,type,pos,direction)
      -{
      -	var o = {
      -		name: name,
      -		type: type,
      -		pos: pos,
      -		direction: direction,
      -		links: null
      -	};
      -	this.connections.push( o );
      -	return o;
      -}
      -
      -/**
      -* computes the size of a node according to its inputs and output slots
      -* @method computeSize
      -* @param {number} minHeight
      -* @return {number} the total size
      -*/
      -LGraphNode.prototype.computeSize = function( minHeight, out )
      -{
      -	var rows = Math.max( this.inputs ? this.inputs.length : 1, this.outputs ? this.outputs.length : 1);
      -	var size = out || new Float32Array([0,0]);
      -	rows = Math.max(rows, 1);
      -	size[1] = rows * 14 + 6;
      -
      -	var font_size = 14;
      -	var title_width = compute_text_size( this.title );
      -	var input_width = 0;
      -	var output_width = 0;
      -
      -	if(this.inputs)
      -		for(var i = 0, l = this.inputs.length; i < l; ++i)
      -		{
      -			var input = this.inputs[i];
      -			var text = input.label || input.name || "";
      -			var text_width = compute_text_size( text );
      -			if(input_width < text_width)
      -				input_width = text_width;
      -		}
      -
      -	if(this.outputs)
      -		for(var i = 0, l = this.outputs.length; i < l; ++i)
      -		{
      -			var output = this.outputs[i];
      -			var text = output.label || output.name || "";
      -			var text_width = compute_text_size( text );
      -			if(output_width < text_width)
      -				output_width = text_width;
      -		}
      -
      -	size[0] = Math.max( input_width + output_width + 10, title_width );
      -	size[0] = Math.max( size[0], LiteGraph.NODE_WIDTH );
      -
      -	function compute_text_size( text )
      -	{
      -		if(!text)
      -			return 0;
      -		return font_size * text.length * 0.6;
      -	}
      -
      -	return size;
      -}
      -
      -/**
      -* returns the bounding of the object, used for rendering purposes
      -* bounding is: [topleft_cornerx, topleft_cornery, width, height]
      -* @method getBounding
      -* @return {Float32Array[4]} the total size
      -*/
      -LGraphNode.prototype.getBounding = function( out )
      -{
      -	out = out || new Float32Array(4);
      -	out[0] = this.pos[0] - 4;
      -	out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT;
      -	out[2] = this.size[0] + 4;
      -	out[3] = this.size[1] + LiteGraph.NODE_TITLE_HEIGHT;
      -	return out;
      -}
      -
      -/**
      -* checks if a point is inside the shape of a node
      -* @method isPointInsideNode
      -* @param {number} x
      -* @param {number} y
      -* @return {boolean}
      -*/
      -LGraphNode.prototype.isPointInsideNode = function(x,y, margin)
      -{
      -	margin = margin || 0;
      -
      -	var margin_top = this.graph && this.graph.isLive() ? 0 : 20;
      -	if(this.flags.collapsed)
      -	{
      -		//if ( distance([x,y], [this.pos[0] + this.size[0]*0.5, this.pos[1] + this.size[1]*0.5]) < LiteGraph.NODE_COLLAPSED_RADIUS)
      -		if( isInsideRectangle( x, y, this.pos[0] - margin, this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT - margin, LiteGraph.NODE_COLLAPSED_WIDTH + 2 * margin, LiteGraph.NODE_TITLE_HEIGHT + 2 * margin ) )
      -			return true;
      -	}
      -	else if ( (this.pos[0] - 4 - margin) < x && (this.pos[0] + this.size[0] + 4 + margin) > x
      -		&& (this.pos[1] - margin_top - margin) < y && (this.pos[1] + this.size[1] + margin) > y)
      -		return true;
      -	return false;
      -}
      -
      -/**
      -* checks if a point is inside a node slot, and returns info about which slot
      -* @method getSlotInPosition
      -* @param {number} x
      -* @param {number} y
      -* @return {Object} if found the object contains { input|output: slot object, slot: number, link_pos: [x,y] }
      -*/
      -LGraphNode.prototype.getSlotInPosition = function( x, y )
      -{
      -	//search for inputs
      -	if(this.inputs)
      -		for(var i = 0, l = this.inputs.length; i < l; ++i)
      -		{
      -			var input = this.inputs[i];
      -			var link_pos = this.getConnectionPos( true,i );
      -			if( isInsideRectangle(x, y, link_pos[0] - 10, link_pos[1] - 5, 20,10) )
      -				return { input: input, slot: i, link_pos: link_pos, locked: input.locked };
      -		}
      -
      -	if(this.outputs)
      -		for(var i = 0, l = this.outputs.length; i < l; ++i)
      -		{
      -			var output = this.outputs[i];
      -			var link_pos = this.getConnectionPos(false,i);
      -			if( isInsideRectangle(x, y, link_pos[0] - 10, link_pos[1] - 5, 20,10) )
      -				return { output: output, slot: i, link_pos: link_pos, locked: output.locked };
      -		}
      -
      -	return null;
      -}
      -
      -/**
      -* returns the input slot with a given name (used for dynamic slots), -1 if not found
      -* @method findInputSlot
      -* @param {string} name the name of the slot
      -* @return {number} the slot (-1 if not found)
      -*/
      -LGraphNode.prototype.findInputSlot = function(name)
      -{
      -	if(!this.inputs)
      -		return -1;
      -	for(var i = 0, l = this.inputs.length; i < l; ++i)
      -		if(name == this.inputs[i].name)
      -			return i;
      -	return -1;
      -}
      -
      -/**
      -* returns the output slot with a given name (used for dynamic slots), -1 if not found
      -* @method findOutputSlot
      -* @param {string} name the name of the slot
      -* @return {number} the slot (-1 if not found)
      -*/
      -LGraphNode.prototype.findOutputSlot = function(name)
      -{
      -	if(!this.outputs) return -1;
      -	for(var i = 0, l = this.outputs.length; i < l; ++i)
      -		if(name == this.outputs[i].name)
      -			return i;
      -	return -1;
      -}
      -
      -/**
      -* connect this node output to the input of another node
      -* @method connect
      -* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
      -* @param {LGraphNode} node the target node
      -* @param {number_or_string} target_slot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger)
      -* @return {boolean} if it was connected succesfully
      -*/
      -LGraphNode.prototype.connect = function( slot, target_node, target_slot )
      -{
      -	target_slot = target_slot || 0;
      -
      -	//seek for the output slot
      -	if( slot.constructor === String )
      -	{
      -		slot = this.findOutputSlot(slot);
      -		if(slot == -1)
      -		{
      -			if(LiteGraph.debug)
      -				console.log("Connect: Error, no slot of name " + slot);
      -			return false;
      -		}
      -	}
      -	else if(!this.outputs || slot >= this.outputs.length)
      -	{
      -		if(LiteGraph.debug)
      -			console.log("Connect: Error, slot number not found");
      -		return false;
      -	}
      -
      -	if(target_node && target_node.constructor === Number)
      -		target_node = this.graph.getNodeById( target_node );
      -	if(!target_node)
      -		throw("Node not found");
      -
      -	//avoid loopback
      -	if(target_node == this)
      -		return false;
      -
      -	//you can specify the slot by name
      -	if(target_slot.constructor === String)
      -	{
      -		target_slot = target_node.findInputSlot( target_slot );
      -		if(target_slot == -1)
      -		{
      -			if(LiteGraph.debug)
      -				console.log("Connect: Error, no slot of name " + target_slot);
      -			return false;
      -		}
      -	}
      -	else if( target_slot === LiteGraph.EVENT )
      -	{
      -		//search for first slot with event?
      -		/*
      -		//create input for trigger
      -		var input = target_node.addInput("onTrigger", LiteGraph.EVENT );
      -		target_slot = target_node.inputs.length - 1; //last one is the one created
      -		target_node.mode = LiteGraph.ON_TRIGGER;
      -		*/
      -		return false;
      -	}
      -	else if( !target_node.inputs || target_slot >= target_node.inputs.length )
      -	{
      -		if(LiteGraph.debug)
      -			console.log("Connect: Error, slot number not found");
      -		return false;
      -	}
      -
      -	//if there is something already plugged there, disconnect
      -	if(target_node.inputs[ target_slot ].link != null )
      -		target_node.disconnectInput( target_slot );
      -
      -	//why here??
      -	this.setDirtyCanvas(false,true);
      -	this.graph.connectionChange( this );
      -
      -	var output = this.outputs[slot];
      -
      -	//allows nodes to block connection
      -	if(target_node.onConnectInput)
      -		if( target_node.onConnectInput( target_slot, output.type, output ) === false)
      -			return false;
      -
      -	var input = target_node.inputs[target_slot];
      -
      -	if( LiteGraph.isValidConnection( output.type, input.type ) )
      -	{
      -		var link_info = {
      -			id: this.graph.last_link_id++,
      -			type: input.type,
      -			origin_id: this.id,
      -			origin_slot: slot,
      -			target_id: target_node.id,
      -			target_slot: target_slot
      -		};
      -
      -		//add to graph links list
      -		this.graph.links[ link_info.id ] = link_info;
      -
      -		//connect in output
      -		if( output.links == null )
      -			output.links = [];
      -		output.links.push( link_info.id );
      -		//connect in input
      -		target_node.inputs[target_slot].link = link_info.id;
      -
      -		if(this.onConnectionsChange)
      -			this.onConnectionsChange( LiteGraph.OUTPUT, slot, true, link_info, output ); //link_info has been created now, so its updated
      -		if(target_node.onConnectionsChange)
      -			target_node.onConnectionsChange( LiteGraph.INPUT, target_slot, true, link_info, input );
      -	}
      -
      -	this.setDirtyCanvas(false,true);
      -	this.graph.connectionChange( this );
      -
      -	return true;
      -}
      -
      -/**
      -* disconnect one output to an specific node
      -* @method disconnectOutput
      -* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
      -* @param {LGraphNode} target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected]
      -* @return {boolean} if it was disconnected succesfully
      -*/
      -LGraphNode.prototype.disconnectOutput = function( slot, target_node )
      -{
      -	if( slot.constructor === String )
      -	{
      -		slot = this.findOutputSlot(slot);
      -		if(slot == -1)
      -		{
      -			if(LiteGraph.debug)
      -				console.log("Connect: Error, no slot of name " + slot);
      -			return false;
      -		}
      -	}
      -	else if(!this.outputs || slot >= this.outputs.length)
      -	{
      -		if(LiteGraph.debug)
      -			console.log("Connect: Error, slot number not found");
      -		return false;
      -	}
      -
      -	//get output slot
      -	var output = this.outputs[slot];
      -	if(!output.links || output.links.length == 0)
      -		return false;
      -
      -	//one of the links
      -	if(target_node)
      -	{
      -		if(target_node.constructor === Number)
      -			target_node = this.graph.getNodeById( target_node );
      -		if(!target_node)
      -			throw("Target Node not found");
      -
      -		for(var i = 0, l = output.links.length; i < l; i++)
      -		{
      -			var link_id = output.links[i];
      -			var link_info = this.graph.links[ link_id ];
      -
      -			//is the link we are searching for...
      -			if( link_info.target_id == target_node.id )
      -			{
      -				output.links.splice(i,1); //remove here
      -				var input = target_node.inputs[ link_info.target_slot ];
      -				input.link = null; //remove there
      -				delete this.graph.links[ link_id ]; //remove the link from the links pool
      -				if(target_node.onConnectionsChange)
      -					target_node.onConnectionsChange( LiteGraph.INPUT, link_info.target_slot, false, link_info, input ); //link_info hasnt been modified so its ok
      -				if(this.onConnectionsChange)
      -					this.onConnectionsChange( LiteGraph.OUTPUT, slot, false, link_info, output );
      -				break;
      -			}
      -		}
      -	}
      -	else //all the links
      -	{
      -		for(var i = 0, l = output.links.length; i < l; i++)
      -		{
      -			var link_id = output.links[i];
      -			var link_info = this.graph.links[ link_id ];
      -			if(!link_info) //bug: it happens sometimes
      -				continue;
      -
      -			var target_node = this.graph.getNodeById( link_info.target_id );
      -			var input = null;
      -			if(target_node)
      -			{
      -				input = target_node.inputs[ link_info.target_slot ];
      -				input.link = null; //remove other side link
      -				if(target_node.onConnectionsChange)
      -					target_node.onConnectionsChange( LiteGraph.INPUT, link_info.target_slot, false, link_info, input ); //link_info hasnt been modified so its ok
      -			}
      -			delete this.graph.links[ link_id ]; //remove the link from the links pool
      -			if(this.onConnectionsChange)
      -				this.onConnectionsChange( LiteGraph.OUTPUT, slot, false, link_info, output );
      -		}
      -		output.links = null;
      -	}
      -
      -
      -	this.setDirtyCanvas(false,true);
      -	this.graph.connectionChange( this );
      -	return true;
      -}
      -
      -/**
      -* disconnect one input
      -* @method disconnectInput
      -* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
      -* @return {boolean} if it was disconnected succesfully
      -*/
      -LGraphNode.prototype.disconnectInput = function( slot )
      -{
      -	//seek for the output slot
      -	if( slot.constructor === String )
      -	{
      -		slot = this.findInputSlot(slot);
      -		if(slot == -1)
      -		{
      -			if(LiteGraph.debug)
      -				console.log("Connect: Error, no slot of name " + slot);
      -			return false;
      -		}
      -	}
      -	else if(!this.inputs || slot >= this.inputs.length)
      -	{
      -		if(LiteGraph.debug)
      -			console.log("Connect: Error, slot number not found");
      -		return false;
      -	}
      -
      -	var input = this.inputs[slot];
      -	if(!input)
      -		return false;
      -
      -	var link_id = this.inputs[slot].link;
      -	this.inputs[slot].link = null;
      -
      -	//remove other side
      -	var link_info = this.graph.links[ link_id ];
      -	if( link_info )
      -	{
      -		var target_node = this.graph.getNodeById( link_info.origin_id );
      -		if(!target_node)
      -			return false;
      -
      -		var output = target_node.outputs[ link_info.origin_slot ];
      -		if(!output || !output.links || output.links.length == 0)
      -			return false;
      -
      -		//search in the inputs list for this link
      -		for(var i = 0, l = output.links.length; i < l; i++)
      -		{
      -			if( output.links[i] == link_id )
      -			{
      -				output.links.splice(i,1);
      -				break;
      -			}
      -		}
      -
      -		delete this.graph.links[ link_id ]; //remove from the pool
      -
      -		if( this.onConnectionsChange )
      -			this.onConnectionsChange( LiteGraph.INPUT, slot, false, link_info, input );
      -		if( target_node.onConnectionsChange )
      -			target_node.onConnectionsChange( LiteGraph.OUTPUT, i, false, link_info, output );
      -	}
      -
      -	this.setDirtyCanvas(false,true);
      -	this.graph.connectionChange( this );
      -	return true;
      -}
      -
      -/**
      -* returns the center of a connection point in canvas coords
      -* @method getConnectionPos
      -* @param {boolean} is_input true if if a input slot, false if it is an output
      -* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
      -* @return {[x,y]} the position
      -**/
      -LGraphNode.prototype.getConnectionPos = function(is_input, slot_number)
      -{
      -	if(this.flags.collapsed)
      -	{
      -		if(is_input)
      -			return [this.pos[0], this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5];
      -		else
      -			return [this.pos[0] + LiteGraph.NODE_COLLAPSED_WIDTH, this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5];
      -		//return [this.pos[0] + this.size[0] * 0.5, this.pos[1] + this.size[1] * 0.5];
      -	}
      -
      -	if(is_input && slot_number == -1)
      -	{
      -		return [this.pos[0] + 10, this.pos[1] + 10];
      -	}
      -
      -	if(is_input && this.inputs.length > slot_number && this.inputs[slot_number].pos)
      -		return [this.pos[0] + this.inputs[slot_number].pos[0],this.pos[1] + this.inputs[slot_number].pos[1]];
      -	else if(!is_input && this.outputs.length > slot_number && this.outputs[slot_number].pos)
      -		return [this.pos[0] + this.outputs[slot_number].pos[0],this.pos[1] + this.outputs[slot_number].pos[1]];
      -
      -	if(!is_input) //output
      -		return [this.pos[0] + this.size[0] + 1, this.pos[1] + 10 + slot_number * LiteGraph.NODE_SLOT_HEIGHT];
      -	return [this.pos[0] , this.pos[1] + 10 + slot_number * LiteGraph.NODE_SLOT_HEIGHT];
      -}
      -
      -/* Force align to grid */
      -LGraphNode.prototype.alignToGrid = function()
      -{
      -	this.pos[0] = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.pos[0] / LiteGraph.CANVAS_GRID_SIZE);
      -	this.pos[1] = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.pos[1] / LiteGraph.CANVAS_GRID_SIZE);
      -}
      -
      -
      -/* Console output */
      -LGraphNode.prototype.trace = function(msg)
      -{
      -	if(!this.console)
      -		this.console = [];
      -	this.console.push(msg);
      -	if(this.console.length > LGraphNode.MAX_CONSOLE)
      -		this.console.shift();
      -
      -	this.graph.onNodeTrace(this,msg);
      -}
      -
      -/* Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */
      -LGraphNode.prototype.setDirtyCanvas = function(dirty_foreground, dirty_background)
      -{
      -	if(!this.graph)
      -		return;
      -	this.graph.sendActionToCanvas("setDirty",[dirty_foreground, dirty_background]);
      -}
      -
      -LGraphNode.prototype.loadImage = function(url)
      -{
      -	var img = new Image();
      -	img.src = LiteGraph.node_images_path + url;
      -	img.ready = false;
      -
      -	var that = this;
      -	img.onload = function() {
      -		this.ready = true;
      -		that.setDirtyCanvas(true);
      -	}
      -	return img;
      -}
      -
      -//safe LGraphNode action execution (not sure if safe)
      -/*
      -LGraphNode.prototype.executeAction = function(action)
      -{
      -	if(action == "") return false;
      -
      -	if( action.indexOf(";") != -1 || action.indexOf("}") != -1)
      -	{
      -		this.trace("Error: Action contains unsafe characters");
      -		return false;
      -	}
      -
      -	var tokens = action.split("(");
      -	var func_name = tokens[0];
      -	if( typeof(this[func_name]) != "function")
      -	{
      -		this.trace("Error: Action not found on node: " + func_name);
      -		return false;
      -	}
      -
      -	var code = action;
      -
      -	try
      -	{
      -		var _foo = eval;
      -		eval = null;
      -		(new Function("with(this) { " + code + "}")).call(this);
      -		eval = _foo;
      -	}
      -	catch (err)
      -	{
      -		this.trace("Error executing action {" + action + "} :" + err);
      -		return false;
      -	}
      -
      -	return true;
      -}
      -*/
      -
      -/* Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */
      -LGraphNode.prototype.captureInput = function(v)
      -{
      -	if(!this.graph || !this.graph.list_of_graphcanvas)
      -		return;
      -
      -	var list = this.graph.list_of_graphcanvas;
      -
      -	for(var i = 0; i < list.length; ++i)
      -	{
      -		var c = list[i];
      -		//releasing somebody elses capture?!
      -		if(!v && c.node_capturing_input != this)
      -			continue;
      -
      -		//change
      -		c.node_capturing_input = v ? this : null;
      -	}
      -}
      -
      -/**
      -* Collapse the node to make it smaller on the canvas
      -* @method collapse
      -**/
      -LGraphNode.prototype.collapse = function()
      -{
      -	if(!this.flags.collapsed)
      -		this.flags.collapsed = true;
      -	else
      -		this.flags.collapsed = false;
      -	this.setDirtyCanvas(true,true);
      -}
      -
      -/**
      -* Forces the node to do not move or realign on Z
      -* @method pin
      -**/
      -
      -LGraphNode.prototype.pin = function(v)
      -{
      -	if(v === undefined)
      -		this.flags.pinned = !this.flags.pinned;
      -	else
      -		this.flags.pinned = v;
      -}
      -
      -LGraphNode.prototype.localToScreen = function(x,y, graphcanvas)
      -{
      -	return [(x + this.pos[0]) * graphcanvas.scale + graphcanvas.offset[0],
      -		(y + this.pos[1]) * graphcanvas.scale + graphcanvas.offset[1]];
      -}
      -
      -
      -
      -//*********************************************************************************
      -// LGraphCanvas: LGraph renderer CLASS
      -//*********************************************************************************
      -
      -/**
      -* The Global Scope. It contains all the registered node classes.
      -* Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked
      -*
      -* @class LGraphCanvas
      -* @constructor
      -* @param {HTMLCanvas} canvas the canvas where you want to render (it accepts a selector in string format or the canvas element itself)
      -* @param {LGraph} graph [optional]
      -* @param {Object} options [optional] { skip_rendering, autoresize }
      -*/
      -function LGraphCanvas( canvas, graph, options )
      -{
      -	options = options || {};
      -
      -	//if(graph === undefined)
      -  //	throw ("No graph assigned");
      -	this.background_image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII='
      -
      -	if(canvas && canvas.constructor === String )
      -		canvas = document.querySelector( canvas );
      -
      -	this.max_zoom = 10;
      -	this.min_zoom = 0.1;
      -
      -	this.title_text_font = "bold 14px Arial";
      -	this.inner_text_font = "normal 12px Arial";
      -	this.default_link_color = "#AAC";
      -	this.default_connection_color = {
      -		input_off: "#AAB",
      -		input_on: "#7F7",
      -		output_off: "#AAB",
      -		output_on: "#7F7"
      -	};
      -
      -	this.highquality_render = true;
      -	this.editor_alpha = 1; //used for transition
      -	this.pause_rendering = false;
      -	this.render_shadows = true;
      -	this.clear_background = true;
      -
      -	this.render_only_selected = true;
      -	this.live_mode = false;
      -	this.show_info = true;
      -	this.allow_dragcanvas = true;
      -	this.allow_dragnodes = true;
      -	this.allow_interaction = true; //allow to control widgets, buttons, collapse, etc
      -	this.drag_mode = false;
      -	this.dragging_rectangle = null;
      -
      -	this.always_render_background = false;
      -	this.render_canvas_area = true;
      -	this.render_connections_shadows = false; //too much cpu
      -	this.render_connections_border = true;
      -	this.render_curved_connections = true;
      -	this.render_connection_arrows = true;
      -
      -	this.connections_width = 3;
      -
      -	//link canvas and graph
      -	if(graph)
      -		graph.attachCanvas(this);
      -
      -	this.setCanvas( canvas );
      -	this.clear();
      -
      -	if(!options.skip_render)
      -		this.startRendering();
      -
      -	this.autoresize = options.autoresize;
      -}
      -
      -global.LGraphCanvas = LiteGraph.LGraphCanvas = LGraphCanvas;
      -
      -LGraphCanvas.link_type_colors = {"-1":"#F85",'number':"#AAC","node":"#DCA"};
      -
      -
      -/**
      -* clears all the data inside
      -*
      -* @method clear
      -*/
      -LGraphCanvas.prototype.clear = function()
      -{
      -	this.frame = 0;
      -	this.last_draw_time = 0;
      -	this.render_time = 0;
      -	this.fps = 0;
      -
      -	this.scale = 1;
      -	this.offset = [0,0];
      -
      -	this.dragging_rectangle = null;
      -
      -	this.selected_nodes = {};
      -	this.visible_nodes = [];
      -	this.node_dragged = null;
      -	this.node_over = null;
      -	this.node_capturing_input = null;
      -	this.connecting_node = null;
      -	this.highlighted_links = {};
      -
      -	this.dirty_canvas = true;
      -	this.dirty_bgcanvas = true;
      -	this.dirty_area = null;
      -
      -	this.node_in_panel = null;
      -
      -	this.last_mouse = [0,0];
      -	this.last_mouseclick = 0;
      -
      -	if(this.onClear)
      -		this.onClear();
      -	//this.UIinit();
      -}
      -
      -/**
      -* assigns a graph, you can reasign graphs to the same canvas
      -*
      -* @method setGraph
      -* @param {LGraph} graph
      -*/
      -LGraphCanvas.prototype.setGraph = function( graph, skip_clear )
      -{
      -	if(this.graph == graph)
      -		return;
      -
      -	if(!skip_clear)
      -		this.clear();
      -
      -	if(!graph && this.graph)
      -	{
      -		this.graph.detachCanvas(this);
      -		return;
      -	}
      -
      -	/*
      -	if(this.graph)
      -		this.graph.canvas = null; //remove old graph link to the canvas
      -	this.graph = graph;
      -	if(this.graph)
      -		this.graph.canvas = this;
      -	*/
      -	graph.attachCanvas(this);
      -	this.setDirty(true,true);
      -}
      -
      -/**
      -* opens a graph contained inside a node in the current graph
      -*
      -* @method openSubgraph
      -* @param {LGraph} graph
      -*/
      -LGraphCanvas.prototype.openSubgraph = function(graph)
      -{
      -	if(!graph)
      -		throw("graph cannot be null");
      -
      -	if(this.graph == graph)
      -		throw("graph cannot be the same");
      -
      -	this.clear();
      -
      -	if(this.graph)
      -	{
      -		if(!this._graph_stack)
      -			this._graph_stack = [];
      -		this._graph_stack.push(this.graph);
      -	}
      -
      -	graph.attachCanvas(this);
      -	this.setDirty(true,true);
      -}
      -
      -/**
      -* closes a subgraph contained inside a node
      -*
      -* @method closeSubgraph
      -* @param {LGraph} assigns a graph
      -*/
      -LGraphCanvas.prototype.closeSubgraph = function()
      -{
      -	if(!this._graph_stack || this._graph_stack.length == 0)
      -		return;
      -	var graph = this._graph_stack.pop();
      -	this.selected_nodes = {};
      -	this.highlighted_links = {};
      -	graph.attachCanvas(this);
      -	this.setDirty(true,true);
      -}
      -
      -/**
      -* assigns a canvas
      -*
      -* @method setCanvas
      -* @param {Canvas} assigns a canvas (also accepts the ID of the element (not a selector)
      -*/
      -LGraphCanvas.prototype.setCanvas = function( canvas, skip_events )
      -{
      -	var that = this;
      -
      -	if(canvas)
      -	{
      -		if( canvas.constructor === String )
      -		{
      -			canvas = document.getElementById(canvas);
      -			if(!canvas)
      -				throw("Error creating LiteGraph canvas: Canvas not found");
      -		}
      -	}
      -
      -	if(canvas === this.canvas)
      -		return;
      -
      -	if(!canvas && this.canvas)
      -	{
      -		//maybe detach events from old_canvas
      -		if(!skip_events)
      -			this.unbindEvents();
      -	}
      -
      -	this.canvas = canvas;
      -
      -	if(!canvas)
      -		return;
      -
      -	//this.canvas.tabindex = "1000";
      -	canvas.className += " lgraphcanvas";
      -	canvas.data = this;
      -
      -	//bg canvas: used for non changing stuff
      -	this.bgcanvas = null;
      -	if(!this.bgcanvas)
      -	{
      -		this.bgcanvas = document.createElement("canvas");
      -		this.bgcanvas.width = this.canvas.width;
      -		this.bgcanvas.height = this.canvas.height;
      -	}
      -
      -	if(canvas.getContext == null)
      -	{
      -		if( canvas.localName != "canvas" )
      -			throw("Element supplied for LGraphCanvas must be a <canvas> element, you passed a " + canvas.localName );
      -		throw("This browser doesnt support Canvas");
      -	}
      -
      -	var ctx = this.ctx = canvas.getContext("2d");
      -	if(ctx == null)
      -	{
      -		console.warn("This canvas seems to be WebGL, enabling WebGL renderer");
      -		this.enableWebGL();
      -	}
      -
      -	//input:  (move and up could be unbinded)
      -	this._mousemove_callback = this.processMouseMove.bind(this);
      -	this._mouseup_callback = this.processMouseUp.bind(this);
      -
      -	if(!skip_events)
      -		this.bindEvents();
      -}
      -
      -//used in some events to capture them
      -LGraphCanvas.prototype._doNothing = function doNothing(e) { e.preventDefault(); return false; };
      -LGraphCanvas.prototype._doReturnTrue = function doNothing(e) { e.preventDefault(); return true; };
      -
      -LGraphCanvas.prototype.bindEvents = function()
      -{
      -	if(	this._events_binded )
      -	{
      -		console.warn("LGraphCanvas: events already binded");
      -		return;
      -	}
      -
      -	var canvas = this.canvas;
      -	var ref_window = this.getCanvasWindow();
      -	var document = ref_window.document; //hack used when moving canvas between windows
      -
      -	this._mousedown_callback = this.processMouseDown.bind(this);
      -	this._mousewheel_callback = this.processMouseWheel.bind(this);
      -
      -	canvas.addEventListener("mousedown", this._mousedown_callback, true ); //down do not need to store the binded
      -	canvas.addEventListener("mousemove", this._mousemove_callback );
      -	canvas.addEventListener("mousewheel", this._mousewheel_callback, false);
      -
      -	canvas.addEventListener("contextmenu", this._doNothing );
      -	canvas.addEventListener("DOMMouseScroll", this._mousewheel_callback, false);
      -
      -	//touch events
      -	//if( 'touchstart' in document.documentElement )
      -	{
      -		canvas.addEventListener("touchstart", this.touchHandler, true);
      -		canvas.addEventListener("touchmove", this.touchHandler, true);
      -		canvas.addEventListener("touchend", this.touchHandler, true);
      -		canvas.addEventListener("touchcancel", this.touchHandler, true);
      -	}
      -
      -	//Keyboard ******************
      -	this._key_callback = this.processKey.bind(this);
      -
      -	canvas.addEventListener("keydown", this._key_callback, true );
      -	document.addEventListener("keyup", this._key_callback, true ); //in document, otherwise it doesnt fire keyup
      -
      -	//Droping Stuff over nodes ************************************
      -	this._ondrop_callback = this.processDrop.bind(this);
      -
      -	canvas.addEventListener("dragover", this._doNothing, false );
      -	canvas.addEventListener("dragend", this._doNothing, false );
      -	canvas.addEventListener("drop", this._ondrop_callback, false );
      -	canvas.addEventListener("dragenter", this._doReturnTrue, false );
      -
      -	this._events_binded = true;
      -}
      -
      -LGraphCanvas.prototype.unbindEvents = function()
      -{
      -	if(	!this._events_binded )
      -	{
      -		console.warn("LGraphCanvas: no events binded");
      -		return;
      -	}
      -
      -	var ref_window = this.getCanvasWindow();
      -	var document = ref_window.document;
      -
      -	this.canvas.removeEventListener( "mousedown", this._mousedown_callback );
      -	this.canvas.removeEventListener( "mousewheel", this._mousewheel_callback );
      -	this.canvas.removeEventListener( "DOMMouseScroll", this._mousewheel_callback );
      -	this.canvas.removeEventListener( "keydown", this._key_callback );
      -	document.removeEventListener( "keyup", this._key_callback );
      -	this.canvas.removeEventListener( "contextmenu", this._doNothing );
      -	this.canvas.removeEventListener( "drop", this._ondrop_callback );
      -	this.canvas.removeEventListener( "dragenter", this._doReturnTrue );
      -
      -	this.canvas.removeEventListener("touchstart", this.touchHandler );
      -	this.canvas.removeEventListener("touchmove", this.touchHandler );
      -	this.canvas.removeEventListener("touchend", this.touchHandler );
      -	this.canvas.removeEventListener("touchcancel", this.touchHandler );
      -
      -	this._mousedown_callback = null;
      -	this._mousewheel_callback = null;
      -	this._key_callback = null;
      -	this._ondrop_callback = null;
      -
      -	this._events_binded = false;
      -}
      -
      -LGraphCanvas.getFileExtension = function (url)
      -{
      -	var question = url.indexOf("?");
      -	if(question != -1)
      -		url = url.substr(0,question);
      -	var point = url.lastIndexOf(".");
      -	if(point == -1)
      -		return "";
      -	return url.substr(point+1).toLowerCase();
      -}
      -
      -//this file allows to render the canvas using WebGL instead of Canvas2D
      -//this is useful if you plant to render 3D objects inside your nodes
      -LGraphCanvas.prototype.enableWebGL = function()
      -{
      -	if(typeof(GL) === undefined)
      -		throw("litegl.js must be included to use a WebGL canvas");
      -	if(typeof(enableWebGLCanvas) === undefined)
      -		throw("webglCanvas.js must be included to use this feature");
      -
      -	this.gl = this.ctx = enableWebGLCanvas(this.canvas);
      -	this.ctx.webgl = true;
      -	this.bgcanvas = this.canvas;
      -	this.bgctx = this.gl;
      -
      -	/*
      -	GL.create({ canvas: this.bgcanvas });
      -	this.bgctx = enableWebGLCanvas( this.bgcanvas );
      -	window.gl = this.gl;
      -	*/
      -}
      -
      -
      -/*
      -LGraphCanvas.prototype.UIinit = function()
      -{
      -	var that = this;
      -	$("#node-console input").change(function(e)
      -	{
      -		if(e.target.value == "")
      -			return;
      -
      -		var node = that.node_in_panel;
      -		if(!node)
      -			return;
      -
      -		node.trace("] " + e.target.value, "#333");
      -		if(node.onConsoleCommand)
      -		{
      -			if(!node.onConsoleCommand(e.target.value))
      -				node.trace("command not found", "#A33");
      -		}
      -		else if (e.target.value == "info")
      -		{
      -			node.trace("Special methods:");
      -			for(var i in node)
      -			{
      -				if(typeof(node[i]) == "function" && LGraphNode.prototype[i] == null && i.substr(0,2) != "on" && i[0] != "_")
      -					node.trace(" + " + i);
      -			}
      -		}
      -		else
      -		{
      -			try
      -			{
      -				eval("var _foo = function() { return ("+e.target.value+"); }");
      -				var result = _foo.call(node);
      -				if(result)
      -					node.trace(result.toString());
      -				delete window._foo;
      -			}
      -			catch(err)
      -			{
      -				node.trace("error: " + err, "#A33");
      -			}
      -		}
      -
      -		this.value = "";
      -	});
      -}
      -*/
      -
      -/**
      -* marks as dirty the canvas, this way it will be rendered again
      -*
      -* @class LGraphCanvas
      -* @method setDirty
      -* @param {bool} fgcanvas if the foreground canvas is dirty (the one containing the nodes)
      -* @param {bool} bgcanvas if the background canvas is dirty (the one containing the wires)
      -*/
      -LGraphCanvas.prototype.setDirty = function(fgcanvas,bgcanvas)
      -{
      -	if(fgcanvas)
      -		this.dirty_canvas = true;
      -	if(bgcanvas)
      -		this.dirty_bgcanvas = true;
      -}
      -
      -/**
      -* Used to attach the canvas in a popup
      -*
      -* @method getCanvasWindow
      -* @return {window} returns the window where the canvas is attached (the DOM root node)
      -*/
      -LGraphCanvas.prototype.getCanvasWindow = function()
      -{
      -	if(!this.canvas)
      -		return window;
      -	var doc = this.canvas.ownerDocument;
      -	return doc.defaultView || doc.parentWindow;
      -}
      -
      -/**
      -* starts rendering the content of the canvas when needed
      -*
      -* @method startRendering
      -*/
      -LGraphCanvas.prototype.startRendering = function()
      -{
      -	if(this.is_rendering)
      -		return; //already rendering
      -
      -	this.is_rendering = true;
      -	renderFrame.call(this);
      -
      -	function renderFrame()
      -	{
      -		if(!this.pause_rendering)
      -			this.draw();
      -
      -		var window = this.getCanvasWindow();
      -		if(this.is_rendering)
      -			window.requestAnimationFrame( renderFrame.bind(this) );
      -	}
      -}
      -
      -/**
      -* stops rendering the content of the canvas (to save resources)
      -*
      -* @method stopRendering
      -*/
      -LGraphCanvas.prototype.stopRendering = function()
      -{
      -	this.is_rendering = false;
      -	/*
      -	if(this.rendering_timer_id)
      -	{
      -		clearInterval(this.rendering_timer_id);
      -		this.rendering_timer_id = null;
      -	}
      -	*/
      -}
      -
      -/* LiteGraphCanvas input */
      -
      -LGraphCanvas.prototype.processMouseDown = function(e)
      -{
      -	if(!this.graph)
      -		return;
      -
      -	this.adjustMouseEvent(e);
      -
      -	var ref_window = this.getCanvasWindow();
      -	var document = ref_window.document;
      -	LGraphCanvas.active_canvas = this;
      -
      -	//move mouse move event to the window in case it drags outside of the canvas
      -	this.canvas.removeEventListener("mousemove", this._mousemove_callback );
      -	ref_window.document.addEventListener("mousemove", this._mousemove_callback, true ); //catch for the entire window
      -	ref_window.document.addEventListener("mouseup", this._mouseup_callback, true );
      -
      -	var n = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes );
      -	var skip_dragging = false;
      -	var skip_action = false;
      -
      -    LiteGraph.closeAllContextMenus( ref_window );
      -
      -	if(e.which == 1) //left button mouse
      -	{
      -		if( e.ctrlKey )
      -		{
      -			this.dragging_rectangle = new Float32Array(4);
      -			this.dragging_rectangle[0] = e.canvasX;
      -			this.dragging_rectangle[1] = e.canvasY;
      -			this.dragging_rectangle[2] = 1;
      -			this.dragging_rectangle[3] = 1;
      -			skip_action = true;
      -		}
      -
      -		var clicking_canvas_bg = false;
      -
      -		//when clicked on top of a node
      -		//and it is not interactive
      -		if( n && this.allow_interaction && !skip_action )
      -		{
      -			if(!this.live_mode && !n.flags.pinned)
      -				this.bringToFront(n); //if it wasnt selected?
      -
      -			//not dragging mouse to connect two slots
      -			if(!this.connecting_node && !n.flags.collapsed && !this.live_mode)
      -			{
      -				//search for outputs
      -				if(n.outputs)
      -					for(var i = 0, l = n.outputs.length; i < l; ++i)
      -					{
      -						var output = n.outputs[i];
      -						var link_pos = n.getConnectionPos(false,i);
      -						if( isInsideRectangle(e.canvasX, e.canvasY, link_pos[0] - 10, link_pos[1] - 5, 20,10) )
      -						{
      -							this.connecting_node = n;
      -							this.connecting_output = output;
      -							this.connecting_pos = n.getConnectionPos(false,i);
      -							this.connecting_slot = i;
      -
      -							skip_action = true;
      -							break;
      -						}
      -					}
      -
      -				//search for inputs
      -				if(n.inputs)
      -					for(var i = 0, l = n.inputs.length; i < l; ++i)
      -					{
      -						var input = n.inputs[i];
      -						var link_pos = n.getConnectionPos(true,i);
      -						if( isInsideRectangle(e.canvasX, e.canvasY, link_pos[0] - 10, link_pos[1] - 5, 20,10) )
      -						{
      -							if(input.link !== null)
      -							{
      -								n.disconnectInput(i);
      -								this.dirty_bgcanvas = true;
      -								skip_action = true;
      -							}
      -						}
      -					}
      -
      -				//Search for corner
      -				if( !skip_action && isInsideRectangle(e.canvasX, e.canvasY, n.pos[0] + n.size[0] - 5, n.pos[1] + n.size[1] - 5 ,5,5 ))
      -				{
      -					this.resizing_node = n;
      -					this.canvas.style.cursor = "se-resize";
      -					skip_action = true;
      -				}
      -			}
      -
      -			//Search for corner
      -			if( !skip_action && isInsideRectangle(e.canvasX, e.canvasY, n.pos[0], n.pos[1] - LiteGraph.NODE_TITLE_HEIGHT ,LiteGraph.NODE_TITLE_HEIGHT, LiteGraph.NODE_TITLE_HEIGHT ))
      -			{
      -				n.collapse();
      -				skip_action = true;
      -			}
      -
      -			//it wasnt clicked on the links boxes
      -			if(!skip_action)
      -			{
      -				var block_drag_node = false;
      -
      -				//double clicking
      -				var now = LiteGraph.getTime();
      -				if ((now - this.last_mouseclick) < 300 && this.selected_nodes[n.id])
      -				{
      -					//double click node
      -					if( n.onDblClick)
      -						n.onDblClick(e);
      -					this.processNodeDblClicked(n);
      -					block_drag_node = true;
      -				}
      -
      -				//if do not capture mouse
      -
      -				if( n.onMouseDown && n.onMouseDown(e, [e.canvasX - n.pos[0], e.canvasY - n.pos[1]] ) )
      -					block_drag_node = true;
      -				else if(this.live_mode)
      -				{
      -					clicking_canvas_bg = true;
      -					block_drag_node = true;
      -				}
      -
      -				if(!block_drag_node)
      -				{
      -					if(this.allow_dragnodes)
      -						this.node_dragged = n;
      -
      -					if(!this.selected_nodes[n.id])
      -						this.processNodeSelected(n,e);
      -				}
      -
      -				this.dirty_canvas = true;
      -			}
      -		}
      -		else
      -			clicking_canvas_bg = true;
      -
      -		if(!skip_action && clicking_canvas_bg && this.allow_dragcanvas)
      -		{
      -			this.dragging_canvas = true;
      -		}
      -	}
      -	else if (e.which == 2) //middle button
      -	{
      -
      -	}
      -	else if (e.which == 3) //right button
      -	{
      -		this.processContextMenu(n,e);
      -	}
      -
      -	//TODO
      -	//if(this.node_selected != prev_selected)
      -	//	this.onNodeSelectionChange(this.node_selected);
      -
      -	this.last_mouse[0] = e.localX;
      -	this.last_mouse[1] = e.localY;
      -	this.last_mouseclick = LiteGraph.getTime();
      -	this.canvas_mouse = [e.canvasX, e.canvasY];
      -
      -	/*
      -	if( (this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null)
      -		this.draw();
      -	*/
      -
      -	this.graph.change();
      -
      -	//this is to ensure to defocus(blur) if a text input element is on focus
      -	if(!ref_window.document.activeElement || (ref_window.document.activeElement.nodeName.toLowerCase() != "input" && ref_window.document.activeElement.nodeName.toLowerCase() != "textarea"))
      -		e.preventDefault();
      -	e.stopPropagation();
      -
      -	if(this.onMouseDown)
      -		this.onMouseDown(e);
      -
      -	return false;
      -}
      -
      -LGraphCanvas.prototype.processMouseMove = function(e)
      -{
      -	if(this.autoresize)
      -		this.resize();
      -
      -	if(!this.graph)
      -		return;
      -
      -	LGraphCanvas.active_canvas = this;
      -	this.adjustMouseEvent(e);
      -	var mouse = [e.localX, e.localY];
      -	var delta = [mouse[0] - this.last_mouse[0], mouse[1] - this.last_mouse[1]];
      -	this.last_mouse = mouse;
      -	this.canvas_mouse = [e.canvasX, e.canvasY];
      -
      -	if( this.dragging_rectangle )
      -	{
      -		this.dragging_rectangle[2] = e.canvasX - this.dragging_rectangle[0];
      -		this.dragging_rectangle[3] = e.canvasY - this.dragging_rectangle[1];
      -		this.dirty_canvas = true;
      -	}
      -	else if(this.dragging_canvas)
      -	{
      -		this.offset[0] += delta[0] / this.scale;
      -		this.offset[1] += delta[1] / this.scale;
      -		this.dirty_canvas = true;
      -		this.dirty_bgcanvas = true;
      -	}
      -	else if(this.allow_interaction)
      -	{
      -		if(this.connecting_node)
      -			this.dirty_canvas = true;
      -
      -		//get node over
      -		var n = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes );
      -
      -		//remove mouseover flag
      -		for(var i = 0, l = this.graph._nodes.length; i < l; ++i)
      -		{
      -			if(this.graph._nodes[i].mouseOver && n != this.graph._nodes[i])
      -			{
      -				//mouse leave
      -				this.graph._nodes[i].mouseOver = false;
      -				if(this.node_over && this.node_over.onMouseLeave)
      -					this.node_over.onMouseLeave(e);
      -				this.node_over = null;
      -				this.dirty_canvas = true;
      -			}
      -		}
      -
      -		//mouse over a node
      -		if(n)
      -		{
      -			//this.canvas.style.cursor = "move";
      -			if(!n.mouseOver)
      -			{
      -				//mouse enter
      -				n.mouseOver = true;
      -				this.node_over = n;
      -				this.dirty_canvas = true;
      -
      -				if(n.onMouseEnter) n.onMouseEnter(e);
      -			}
      -
      -			if(n.onMouseMove) n.onMouseMove(e);
      -
      -			//on top of input
      -			if(this.connecting_node)
      -			{
      -				var pos = this._highlight_input || [0,0]; //to store the output of isOverNodeInput
      -
      -				if( this.isOverNodeBox( n, e.canvasX, e.canvasY ) )
      -				{
      -					//mouse on top of the corner box, dont know what to do
      -				}
      -				else
      -				{
      -					var slot = this.isOverNodeInput( n, e.canvasX, e.canvasY, pos );
      -					if(slot != -1 && n.inputs[slot])
      -					{
      -						var slot_type = n.inputs[slot].type;
      -						if( LiteGraph.isValidConnection( this.connecting_output.type, slot_type ) )
      -							this._highlight_input = pos;
      -					}
      -					else
      -						this._highlight_input = null;
      -				}
      -			}
      -
      -			//Search for corner
      -			if( isInsideRectangle(e.canvasX, e.canvasY, n.pos[0] + n.size[0] - 5, n.pos[1] + n.size[1] - 5 ,5,5 ))
      -				this.canvas.style.cursor = "se-resize";
      -			else
      -				this.canvas.style.cursor = null;
      -		}
      -		else
      -			this.canvas.style.cursor = null;
      -
      -		if(this.node_capturing_input && this.node_capturing_input != n && this.node_capturing_input.onMouseMove)
      -		{
      -			this.node_capturing_input.onMouseMove(e);
      -		}
      -
      -
      -		if(this.node_dragged && !this.live_mode)
      -		{
      -			/*
      -			this.node_dragged.pos[0] += delta[0] / this.scale;
      -			this.node_dragged.pos[1] += delta[1] / this.scale;
      -			this.node_dragged.pos[0] = Math.round(this.node_dragged.pos[0]);
      -			this.node_dragged.pos[1] = Math.round(this.node_dragged.pos[1]);
      -			*/
      -
      -			for(var i in this.selected_nodes)
      -			{
      -				var n = this.selected_nodes[i];
      -
      -				n.pos[0] += delta[0] / this.scale;
      -				n.pos[1] += delta[1] / this.scale;
      -				//n.pos[0] = Math.round(n.pos[0]);
      -				//n.pos[1] = Math.round(n.pos[1]);
      -			}
      -
      -			this.dirty_canvas = true;
      -			this.dirty_bgcanvas = true;
      -		}
      -
      -		if(this.resizing_node && !this.live_mode)
      -		{
      -			this.resizing_node.size[0] += delta[0] / this.scale;
      -			this.resizing_node.size[1] += delta[1] / this.scale;
      -			var max_slots = Math.max( this.resizing_node.inputs ? this.resizing_node.inputs.length : 0, this.resizing_node.outputs ? this.resizing_node.outputs.length : 0);
      -			if(this.resizing_node.size[1] < max_slots * LiteGraph.NODE_SLOT_HEIGHT + 4)
      -				this.resizing_node.size[1] = max_slots * LiteGraph.NODE_SLOT_HEIGHT + 4;
      -			if(this.resizing_node.size[0] < LiteGraph.NODE_MIN_WIDTH)
      -				this.resizing_node.size[0] = LiteGraph.NODE_MIN_WIDTH;
      -
      -			this.canvas.style.cursor = "se-resize";
      -			this.dirty_canvas = true;
      -			this.dirty_bgcanvas = true;
      -		}
      -	}
      -
      -	/*
      -	if((this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null)
      -		this.draw();
      -	*/
      -
      -	e.preventDefault();
      -	//e.stopPropagation();
      -	return false;
      -	//this is not really optimal
      -	//this.graph.change();
      -}
      -
      -LGraphCanvas.prototype.processMouseUp = function(e)
      -{
      -	if(!this.graph)
      -		return;
      -
      -	var window = this.getCanvasWindow();
      -	var document = window.document;
      -	LGraphCanvas.active_canvas = this;
      -
      -	//restore the mousemove event back to the canvas
      -	document.removeEventListener("mousemove", this._mousemove_callback, true );
      -	this.canvas.addEventListener("mousemove", this._mousemove_callback, true);
      -	document.removeEventListener("mouseup", this._mouseup_callback, true );
      -
      -	this.adjustMouseEvent(e);
      -
      -	if (e.which == 1) //left button
      -	{
      -		if( this.dragging_rectangle )
      -		{
      -			if(this.graph)
      -			{
      -				var nodes = this.graph._nodes;
      -				var node_bounding = new Float32Array(4);
      -				this.deselectAllNodes();
      -				if( this.dragging_rectangle[2] < 0 ) //flip if negative width
      -					this.dragging_rectangle[0] += this.dragging_rectangle[2];
      -				if( this.dragging_rectangle[3] < 0 ) //flip if negative height
      -					this.dragging_rectangle[1] += this.dragging_rectangle[3];
      -				this.dragging_rectangle[2] = Math.abs( this.dragging_rectangle[2] * this.scale ); //abs to convert negative width
      -				this.dragging_rectangle[3] = Math.abs( this.dragging_rectangle[3] * this.scale ); //abs to convert negative height
      -
      -				for(var i = 0; i < nodes.length; ++i)
      -				{
      -					var node = nodes[i];
      -					node.getBounding( node_bounding );
      -					if(!overlapBounding( this.dragging_rectangle, node_bounding ))
      -						continue; //out of the visible area
      -					this.selectNode( node, true );
      -				}
      -			}
      -			this.dragging_rectangle = null;
      -		}
      -		else if(this.connecting_node) //dragging a connection
      -		{
      -			this.dirty_canvas = true;
      -			this.dirty_bgcanvas = true;
      -
      -			var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes );
      -
      -			//node below mouse
      -			if(node)
      -			{
      -				if( this.connecting_output.type == LiteGraph.EVENT && this.isOverNodeBox( node, e.canvasX, e.canvasY ) )
      -				{
      -					this.connecting_node.connect( this.connecting_slot, node, LiteGraph.EVENT );
      -				}
      -				else
      -				{
      -					//slot below mouse? connect
      -					var slot = this.isOverNodeInput(node, e.canvasX, e.canvasY);
      -					if(slot != -1)
      -					{
      -						this.connecting_node.connect(this.connecting_slot, node, slot);
      -					}
      -					else
      -					{ //not on top of an input
      -						var input = node.getInputInfo(0);
      -						//auto connect
      -						if(this.connecting_output.type == LiteGraph.EVENT)
      -							this.connecting_node.connect( this.connecting_slot, node, LiteGraph.EVENT );
      -						else
      -							if(input && !input.link && LiteGraph.isValidConnection( input.type && this.connecting_output.type ) )
      -								this.connecting_node.connect( this.connecting_slot, node, 0 );
      -					}
      -				}
      -			}
      -
      -			this.connecting_output = null;
      -			this.connecting_pos = null;
      -			this.connecting_node = null;
      -			this.connecting_slot = -1;
      -
      -		}//not dragging connection
      -		else if(this.resizing_node)
      -		{
      -			this.dirty_canvas = true;
      -			this.dirty_bgcanvas = true;
      -			this.resizing_node = null;
      -		}
      -		else if(this.node_dragged) //node being dragged?
      -		{
      -			this.dirty_canvas = true;
      -			this.dirty_bgcanvas = true;
      -			this.node_dragged.pos[0] = Math.round(this.node_dragged.pos[0]);
      -			this.node_dragged.pos[1] = Math.round(this.node_dragged.pos[1]);
      -			if(this.graph.config.align_to_grid)
      -				this.node_dragged.alignToGrid();
      -			this.node_dragged = null;
      -		}
      -		else //no node being dragged
      -		{
      -			//get node over
      -			var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes );
      -
      -			var now = LiteGraph.getTime();
      -			if ( !node && (now - this.last_mouseclick) < 300 )
      -				this.deselectAllNodes();
      -
      -			this.dirty_canvas = true;
      -			this.dragging_canvas = false;
      -
      -			if( this.node_over && this.node_over.onMouseUp )
      -				this.node_over.onMouseUp(e, [e.canvasX - this.node_over.pos[0], e.canvasY - this.node_over.pos[1]] );
      -			if( this.node_capturing_input && this.node_capturing_input.onMouseUp )
      -				this.node_capturing_input.onMouseUp(e, [e.canvasX - this.node_capturing_input.pos[0], e.canvasY - this.node_capturing_input.pos[1]] );
      -		}
      -	}
      -	else if (e.which == 2) //middle button
      -	{
      -		//trace("middle");
      -		this.dirty_canvas = true;
      -		this.dragging_canvas = false;
      -	}
      -	else if (e.which == 3) //right button
      -	{
      -		//trace("right");
      -		this.dirty_canvas = true;
      -		this.dragging_canvas = false;
      -	}
      -
      -	/*
      -	if((this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null)
      -		this.draw();
      -	*/
      -
      -	this.graph.change();
      -
      -	e.stopPropagation();
      -	e.preventDefault();
      -	return false;
      -}
      -
      -
      -LGraphCanvas.prototype.processMouseWheel = function(e)
      -{
      -	if(!this.graph || !this.allow_dragcanvas)
      -		return;
      -
      -	var delta = (e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60);
      -
      -	this.adjustMouseEvent(e);
      -
      -	var zoom = this.scale;
      -
      -	if (delta > 0)
      -		zoom *= 1.1;
      -	else if (delta < 0)
      -		zoom *= 1/(1.1);
      -
      -	this.setZoom( zoom, [ e.localX, e.localY ] );
      -
      -	/*
      -	if(this.rendering_timer_id == null)
      -		this.draw();
      -	*/
      -
      -	this.graph.change();
      -
      -	e.preventDefault();
      -	return false; // prevent default
      -}
      -
      -LGraphCanvas.prototype.isOverNodeBox = function( node, canvasx, canvasy )
      -{
      -	var title_height = LiteGraph.NODE_TITLE_HEIGHT;
      -	if( isInsideRectangle( canvasx, canvasy, node.pos[0] + 2, node.pos[1] + 2 - title_height, title_height - 4,title_height - 4) )
      -		return true;
      -	return false;
      -}
      -
      -LGraphCanvas.prototype.isOverNodeInput = function(node, canvasx, canvasy, slot_pos )
      -{
      -	if(node.inputs)
      -		for(var i = 0, l = node.inputs.length; i < l; ++i)
      -		{
      -			var input = node.inputs[i];
      -			var link_pos = node.getConnectionPos(true,i);
      -			if( isInsideRectangle(canvasx, canvasy, link_pos[0] - 10, link_pos[1] - 5, 20,10) )
      -			{
      -				if(slot_pos)
      -				{
      -					slot_pos[0] = link_pos[0];
      -					slot_pos[1] = link_pos[1];
      -				}
      -				return i;
      -			}
      -		}
      -	return -1;
      -}
      -
      -LGraphCanvas.prototype.processKey = function(e)
      -{
      -	if(!this.graph)
      -		return;
      -
      -	var block_default = false;
      -	//console.log(e); //debug
      -
      -	if(e.target.localName == "input")
      -		return;
      -
      -	if(e.type == "keydown")
      -	{
      -		if(e.keyCode == 32)
      -		{
      -			this.dragging_canvas = true;
      -			block_default = true;
      -		}
      -
      -		//select all Control A
      -		if(e.keyCode == 65 && e.ctrlKey)
      -		{
      -			this.selectNodes();
      -			block_default = true;
      -		}
      -
      -		if(e.code == "KeyC" && (e.metaKey || e.ctrlKey) && !e.shiftKey ) //copy
      -		{
      -			if(this.selected_nodes)
      -			{
      -				this.copyToClipboard();
      -				block_default = true;
      -			}
      -		}
      -
      -		if(e.code == "KeyV" && (e.metaKey || e.ctrlKey) && !e.shiftKey ) //paste
      -		{
      -			this.pasteFromClipboard();
      -		}
      -
      -		//delete or backspace
      -		if(e.keyCode == 46 || e.keyCode == 8)
      -		{
      -			this.deleteSelectedNodes();
      -			block_default = true;
      -		}
      -
      -		//collapse
      -		//...
      -
      -		//TODO
      -		if(this.selected_nodes)
      -			for (var i in this.selected_nodes)
      -				if(this.selected_nodes[i].onKeyDown)
      -					this.selected_nodes[i].onKeyDown(e);
      -	}
      -	else if( e.type == "keyup" )
      -	{
      -		if(e.keyCode == 32)
      -			this.dragging_canvas = false;
      -
      -		if(this.selected_nodes)
      -			for (var i in this.selected_nodes)
      -				if(this.selected_nodes[i].onKeyUp)
      -					this.selected_nodes[i].onKeyUp(e);
      -	}
      -
      -	this.graph.change();
      -
      -	if(block_default)
      -	{
      -		e.preventDefault();
      -		return false;
      -	}
      -}
      -
      -LGraphCanvas.prototype.copyToClipboard = function()
      -{
      -	var clipboard_info = {
      -		nodes: [],
      -		links: []
      -	};
      -	var index = 0;
      -	var selected_nodes_array = [];
      -	for(var i in this.selected_nodes)
      -	{
      -		var node = this.selected_nodes[i];
      -		node._relative_id = index;
      -		selected_nodes_array.push( node );
      -		index += 1;
      -	}
      -
      -	for(var i = 0; i < selected_nodes_array.length; ++i)
      -	{
      -		var node = selected_nodes_array[i];
      -		clipboard_info.nodes.push( node.clone().serialize() );
      -		if(node.inputs && node.inputs.length)
      -			for(var j = 0; j < node.inputs.length; ++j)
      -			{
      -				var input = node.inputs[j];
      -				if(!input || input.link == null)
      -					continue;
      -				var link_info = this.graph.links[ input.link ];
      -				if(!link_info)
      -					continue;
      -				var target_node = this.graph.getNodeById( link_info.origin_id );
      -				if(!target_node || !this.selected_nodes[ target_node.id ] ) //improve this by allowing connections to non-selected nodes
      -					continue; //not selected
      -				clipboard_info.links.push([ target_node._relative_id, j, node._relative_id, link_info.target_slot ]);
      -			}
      -	}
      -	localStorage.setItem( "litegrapheditor_clipboard", JSON.stringify( clipboard_info ) );
      -}
      -
      -LGraphCanvas.prototype.pasteFromClipboard = function()
      -{
      -	var data = localStorage.getItem( "litegrapheditor_clipboard" );
      -	if(!data)
      -		return;
      -
      -	//create nodes
      -	var clipboard_info = JSON.parse(data);
      -	var nodes = [];
      -	for(var i = 0; i < clipboard_info.nodes.length; ++i)
      -	{
      -		var node_data = clipboard_info.nodes[i];
      -		var node = LiteGraph.createNode( node_data.type );
      -		if(node)
      -		{
      -			node.configure(node_data);
      -			node.pos[0] += 5;
      -			node.pos[1] += 5;
      -			this.graph.add( node );
      -			nodes.push( node );
      -		}
      -	}
      -
      -	//create links
      -	for(var i = 0; i < clipboard_info.links.length; ++i)
      -	{
      -		var link_info = clipboard_info.links[i];
      -		var origin_node = nodes[ link_info[0] ];
      -		var target_node = nodes[ link_info[2] ];
      -		origin_node.connect( link_info[1], target_node, link_info[3] );
      -	}
      -
      -	this.selectNodes( nodes );
      -}
      -
      -LGraphCanvas.prototype.processDrop = function(e)
      -{
      -	e.preventDefault();
      -	this.adjustMouseEvent(e);
      -
      -
      -	var pos = [e.canvasX,e.canvasY];
      -	var node = this.graph.getNodeOnPos(pos[0],pos[1]);
      -
      -	if(!node)
      -	{
      -		var r = null;
      -		if(this.onDropItem)
      -			r = this.onDropItem( event );
      -		if(!r)
      -			this.checkDropItem(e);
      -		return;
      -	}
      -
      -	if( node.onDropFile || node.onDropData )
      -	{
      -		var files = e.dataTransfer.files;
      -		if(files && files.length)
      -		{
      -			for(var i=0; i < files.length; i++)
      -			{
      -				var file = e.dataTransfer.files[0];
      -				var filename = file.name;
      -				var ext = LGraphCanvas.getFileExtension( filename );
      -				//console.log(file);
      -
      -				if(node.onDropFile)
      -					node.onDropFile(file);
      -
      -				if(node.onDropData)
      -				{
      -					//prepare reader
      -					var reader = new FileReader();
      -					reader.onload = function (event) {
      -						//console.log(event.target);
      -						var data = event.target.result;
      -						node.onDropData( data, filename, file );
      -					};
      -
      -					//read data
      -					var type = file.type.split("/")[0];
      -					if(type == "text" || type == "")
      -						reader.readAsText(file);
      -					else if (type == "image")
      -						reader.readAsDataURL(file);
      -					else
      -						reader.readAsArrayBuffer(file);
      -				}
      -			}
      -		}
      -	}
      -
      -	if(node.onDropItem)
      -	{
      -		if( node.onDropItem( event ) )
      -			return true;
      -	}
      -
      -	if(this.onDropItem)
      -		return this.onDropItem( event );
      -
      -	return false;
      -}
      -
      -//called if the graph doesnt have a default drop item behaviour
      -LGraphCanvas.prototype.checkDropItem = function(e)
      -{
      -	if(e.dataTransfer.files.length)
      -	{
      -		var file = e.dataTransfer.files[0];
      -		var ext = LGraphCanvas.getFileExtension( file.name ).toLowerCase();
      -		var nodetype = LiteGraph.node_types_by_file_extension[ext];
      -		if(nodetype)
      -		{
      -			var node = LiteGraph.createNode( nodetype.type );
      -			node.pos = [e.canvasX, e.canvasY];
      -			this.graph.add( node );
      -			if( node.onDropFile )
      -				node.onDropFile( file );
      -		}
      -	}
      -}
      -
      -
      -LGraphCanvas.prototype.processNodeDblClicked = function(n)
      -{
      -	if(this.onShowNodePanel)
      -		this.onShowNodePanel(n);
      -
      -	if(this.onNodeDblClicked)
      -		this.onNodeDblClicked(n);
      -
      -	this.setDirty(true);
      -}
      -
      -LGraphCanvas.prototype.processNodeSelected = function(node,e)
      -{
      -	this.selectNode( node, e && e.shiftKey );
      -	if(this.onNodeSelected)
      -		this.onNodeSelected(node);
      -}
      -
      -LGraphCanvas.prototype.processNodeDeselected = function(node)
      -{
      -	this.deselectNode(node);
      -	if(this.onNodeDeselected)
      -		this.onNodeDeselected(node);
      -}
      -
      -LGraphCanvas.prototype.selectNode = function( node, add_to_current_selection )
      -{
      -	if(node == null)
      -		this.deselectAllNodes();
      -	else
      -		this.selectNodes([node], add_to_current_selection );
      -}
      -
      -LGraphCanvas.prototype.selectNodes = function( nodes, add_to_current_selection )
      -{
      -	if(!add_to_current_selection)
      -		this.deselectAllNodes();
      -
      -	nodes = nodes || this.graph._nodes;
      -	for(var i = 0; i < nodes.length; ++i)
      -	{
      -		var node = nodes[i];
      -		if(node.selected)
      -			continue;
      -
      -		if( !node.selected && node.onSelected )
      -			node.onSelected();
      -		node.selected = true;
      -		this.selected_nodes[ node.id ] = node;
      -
      -		if(node.inputs)
      -			for(var i = 0; i < node.inputs.length; ++i)
      -				this.highlighted_links[ node.inputs[i].link ] = true;
      -		if(node.outputs)
      -			for(var i = 0; i < node.outputs.length; ++i)
      -			{
      -				var out = node.outputs[i];
      -				if( out.links )
      -					for(var j = 0; j < out.links.length; ++j)
      -						this.highlighted_links[ out.links[j] ] = true;
      -			}
      -
      -	}
      -
      -	this.setDirty(true);
      -}
      -
      -LGraphCanvas.prototype.deselectNode = function( node )
      -{
      -	if(!node.selected)
      -		return;
      -	if(node.onDeselected)
      -		node.onDeselected();
      -	node.selected = false;
      -
      -	//remove highlighted
      -	if(node.inputs)
      -		for(var i = 0; i < node.inputs.length; ++i)
      -			delete this.highlighted_links[ node.inputs[i].link ];
      -	if(node.outputs)
      -		for(var i = 0; i < node.outputs.length; ++i)
      -		{
      -			var out = node.outputs[i];
      -			if( out.links )
      -				for(var j = 0; j < out.links.length; ++j)
      -					delete this.highlighted_links[ out.links[j] ];
      -		}
      -}
      -
      -LGraphCanvas.prototype.deselectAllNodes = function()
      -{
      -	if(!this.graph)
      -		return;
      -	var nodes = this.graph._nodes;
      -	for(var i = 0, l = nodes.length; i < l; ++i)
      -	{
      -		var node = nodes[i];
      -		if(!node.selected)
      -			continue;
      -		if(node.onDeselected)
      -			node.onDeselected();
      -		node.selected = false;
      -	}
      -	this.selected_nodes = {};
      -	this.highlighted_links = {};
      -	this.setDirty(true);
      -}
      -
      -LGraphCanvas.prototype.deleteSelectedNodes = function()
      -{
      -	for(var i in this.selected_nodes)
      -	{
      -		var m = this.selected_nodes[i];
      -		//if(m == this.node_in_panel) this.showNodePanel(null);
      -		this.graph.remove(m);
      -	}
      -	this.selected_nodes = {};
      -	this.highlighted_links = {};
      -	this.setDirty(true);
      -}
      -
      -LGraphCanvas.prototype.centerOnNode = function(node)
      -{
      -	this.offset[0] = -node.pos[0] - node.size[0] * 0.5 + (this.canvas.width * 0.5 / this.scale);
      -	this.offset[1] = -node.pos[1] - node.size[1] * 0.5 + (this.canvas.height * 0.5 / this.scale);
      -	this.setDirty(true,true);
      -}
      -
      -LGraphCanvas.prototype.adjustMouseEvent = function(e)
      -{
      -	var b = this.canvas.getBoundingClientRect();
      -	e.localX = e.pageX - b.left;
      -	e.localY = e.pageY - b.top;
      -
      -	e.canvasX = e.localX / this.scale - this.offset[0];
      -	e.canvasY = e.localY / this.scale - this.offset[1];
      -}
      -
      -LGraphCanvas.prototype.setZoom = function(value, zooming_center)
      -{
      -	if(!zooming_center)
      -		zooming_center = [this.canvas.width * 0.5,this.canvas.height * 0.5];
      -
      -	var center = this.convertOffsetToCanvas( zooming_center );
      -
      -	this.scale = value;
      -
      -	if(this.scale > this.max_zoom)
      -		this.scale = this.max_zoom;
      -	else if(this.scale < this.min_zoom)
      -		this.scale = this.min_zoom;
      -
      -	var new_center = this.convertOffsetToCanvas( zooming_center );
      -	var delta_offset = [new_center[0] - center[0], new_center[1] - center[1]];
      -
      -	this.offset[0] += delta_offset[0];
      -	this.offset[1] += delta_offset[1];
      -
      -	this.dirty_canvas = true;
      -	this.dirty_bgcanvas = true;
      -}
      -
      -LGraphCanvas.prototype.convertOffsetToCanvas = function( pos, out )
      -{
      -	out = out || [];
      -	out[0] = pos[0] / this.scale - this.offset[0];
      -	out[1] = pos[1] / this.scale - this.offset[1];
      -	return out;
      -}
      -
      -LGraphCanvas.prototype.convertCanvasToOffset = function( pos, out )
      -{
      -	out = out || [];
      -	out[0] = (pos[0] + this.offset[0]) * this.scale;
      -	out[1] = (pos[1] + this.offset[1]) * this.scale;
      -	return out;
      -}
      -
      -LGraphCanvas.prototype.convertEventToCanvas = function(e)
      -{
      -	var rect = this.canvas.getBoundingClientRect();
      -	return this.convertOffsetToCanvas([e.pageX - rect.left,e.pageY - rect.top]);
      -}
      -
      -LGraphCanvas.prototype.bringToFront = function(n)
      -{
      -	var i = this.graph._nodes.indexOf(n);
      -	if(i == -1) return;
      -
      -	this.graph._nodes.splice(i,1);
      -	this.graph._nodes.push(n);
      -}
      -
      -LGraphCanvas.prototype.sendToBack = function(n)
      -{
      -	var i = this.graph._nodes.indexOf(n);
      -	if(i == -1) return;
      -
      -	this.graph._nodes.splice(i,1);
      -	this.graph._nodes.unshift(n);
      -}
      -
      -/* Interaction */
      -
      -
      -
      -/* LGraphCanvas render */
      -var temp = new Float32Array(4);
      -
      -LGraphCanvas.prototype.computeVisibleNodes = function( nodes, out )
      -{
      -	var visible_nodes = out || [];
      -	visible_nodes.length = 0;
      -	nodes = nodes || this.graph._nodes;
      -	for(var i = 0, l = nodes.length; i < l; ++i)
      -	{
      -		var n = nodes[i];
      -
      -		//skip rendering nodes in live mode
      -		if(this.live_mode && !n.onDrawBackground && !n.onDrawForeground)
      -			continue;
      -
      -		if(!overlapBounding( this.visible_area, n.getBounding( temp ) ))
      -			continue; //out of the visible area
      -
      -		visible_nodes.push(n);
      -	}
      -	return visible_nodes;
      -}
      -
      -LGraphCanvas.prototype.draw = function(force_canvas, force_bgcanvas)
      -{
      -	if(!this.canvas)
      -		return;
      -
      -	//fps counting
      -	var now = LiteGraph.getTime();
      -	this.render_time = (now - this.last_draw_time)*0.001;
      -	this.last_draw_time = now;
      -
      -	if(this.graph)
      -	{
      -		var start = [-this.offset[0], -this.offset[1] ];
      -		var end = [start[0] + this.canvas.width / this.scale, start[1] + this.canvas.height / this.scale];
      -		this.visible_area = new Float32Array([ start[0], start[1], end[0] - start[0], end[1] - start[1] ]);
      -	}
      -
      -	if(this.dirty_bgcanvas || force_bgcanvas || this.always_render_background || (this.graph && this.graph._last_trigger_time && (now - this.graph._last_trigger_time) < 1000) )
      -		this.drawBackCanvas();
      -
      -	if(this.dirty_canvas || force_canvas)
      -		this.drawFrontCanvas();
      -
      -	this.fps = this.render_time ? (1.0 / this.render_time) : 0;
      -	this.frame += 1;
      -}
      -
      -LGraphCanvas.prototype.drawFrontCanvas = function()
      -{
      -	if(!this.ctx)
      -		this.ctx = this.bgcanvas.getContext("2d");
      -	var ctx = this.ctx;
      -	if(!ctx) //maybe is using webgl...
      -		return;
      -
      -	if(ctx.start2D)
      -		ctx.start2D();
      -
      -	var canvas = this.canvas;
      -
      -	//reset in case of error
      -	ctx.restore();
      -	ctx.setTransform(1, 0, 0, 1, 0, 0);
      -
      -	//clip dirty area if there is one, otherwise work in full canvas
      -	if(this.dirty_area)
      -	{
      -		ctx.save();
      -		ctx.beginPath();
      -		ctx.rect(this.dirty_area[0],this.dirty_area[1],this.dirty_area[2],this.dirty_area[3]);
      -		ctx.clip();
      -	}
      -
      -	//clear
      -	//canvas.width = canvas.width;
      -	if(this.clear_background)
      -		ctx.clearRect(0,0,canvas.width, canvas.height);
      -
      -	//draw bg canvas
      -	if(this.bgcanvas == this.canvas)
      -		this.drawBackCanvas();
      -	else
      -		ctx.drawImage(this.bgcanvas,0,0);
      -
      -	//rendering
      -	if(this.onRender)
      -		this.onRender(canvas, ctx);
      -
      -	//info widget
      -	if(this.show_info)
      -		this.renderInfo(ctx);
      -
      -	if(this.graph)
      -	{
      -		//apply transformations
      -		ctx.save();
      -		ctx.scale(this.scale,this.scale);
      -		ctx.translate(this.offset[0],this.offset[1]);
      -
      -		//draw nodes
      -		var drawn_nodes = 0;
      -		var visible_nodes = this.computeVisibleNodes( null, this.visible_nodes );
      -
      -		for (var i = 0; i < visible_nodes.length; ++i)
      -		{
      -			var node = visible_nodes[i];
      -
      -			//transform coords system
      -			ctx.save();
      -			ctx.translate( node.pos[0], node.pos[1] );
      -
      -			//Draw
      -			this.drawNode(node, ctx );
      -			drawn_nodes += 1;
      -
      -			//Restore
      -			ctx.restore();
      -		}
      -
      -		//connections ontop?
      -		if(this.graph.config.links_ontop)
      -			if(!this.live_mode)
      -				this.drawConnections(ctx);
      -
      -		//current connection
      -		if(this.connecting_pos != null)
      -		{
      -			ctx.lineWidth = this.connections_width;
      -			var link_color = null;
      -			switch( this.connecting_output.type )
      -			{
      -				case LiteGraph.EVENT: link_color = "#F85"; break;
      -				default:
      -					link_color = "#AFA";
      -			}
      -			//the connection being dragged by the mouse
      -			this.renderLink( ctx, this.connecting_pos, [this.canvas_mouse[0],this.canvas_mouse[1]], null, false, null, link_color );
      -
      -			ctx.beginPath();
      -				if( this.connecting_output.type === LiteGraph.EVENT )
      -					ctx.rect( (this.connecting_pos[0] - 6) + 0.5, (this.connecting_pos[1] - 5) + 0.5,14,10);
      -				else
      -					ctx.arc( this.connecting_pos[0], this.connecting_pos[1],4,0,Math.PI*2);
      -			ctx.fill();
      -
      -			ctx.fillStyle = "#ffcc00";
      -			if(this._highlight_input)
      -			{
      -				ctx.beginPath();
      -					ctx.arc( this._highlight_input[0], this._highlight_input[1],6,0,Math.PI*2);
      -				ctx.fill();
      -			}
      -		}
      -
      -		if( this.dragging_rectangle )
      -		{
      -			ctx.strokeStyle = "#FFF";
      -			ctx.strokeRect( this.dragging_rectangle[0], this.dragging_rectangle[1], this.dragging_rectangle[2], this.dragging_rectangle[3] );
      -		}
      -
      -
      -		ctx.restore();
      -	}
      -
      -	if(this.dirty_area)
      -	{
      -		ctx.restore();
      -		//this.dirty_area = null;
      -	}
      -
      -	if(ctx.finish2D) //this is a function I use in webgl renderer
      -		ctx.finish2D();
      -
      -	this.dirty_canvas = false;
      -}
      -
      -LGraphCanvas.prototype.renderInfo = function( ctx, x, y )
      -{
      -	x = x || 0;
      -	y = y || 0;
      -
      -	ctx.save();
      -	ctx.translate( x, y );
      -
      -	ctx.font = "10px Arial";
      -	ctx.fillStyle = "#888";
      -	if(this.graph)
      -	{
      -		ctx.fillText( "T: " + this.graph.globaltime.toFixed(2)+"s",5,13*1 );
      -		ctx.fillText( "I: " + this.graph.iteration,5,13*2 );
      -		ctx.fillText( "F: " + this.frame,5,13*3 );
      -		ctx.fillText( "FPS:" + this.fps.toFixed(2),5,13*4 );
      -	}
      -	else
      -		ctx.fillText( "No graph selected",5,13*1 );
      -	ctx.restore();
      -}
      -
      -LGraphCanvas.prototype.drawBackCanvas = function()
      -{
      -	var canvas = this.bgcanvas;
      -	if(canvas.width != this.canvas.width ||
      -		canvas.height != this.canvas.height)
      -	{
      -		canvas.width = this.canvas.width;
      -		canvas.height = this.canvas.height;
      -	}
      -
      -	if(!this.bgctx)
      -		this.bgctx = this.bgcanvas.getContext("2d");
      -	var ctx = this.bgctx;
      -	if(ctx.start)
      -		ctx.start();
      -
      -	//clear
      -	if(this.clear_background)
      -		ctx.clearRect(0,0,canvas.width, canvas.height);
      -
      -	if(this._graph_stack && this._graph_stack.length)
      -	{
      -		ctx.strokeStyle = this._graph_stack[ this._graph_stack.length - 1].bgcolor;
      -		ctx.lineWidth = 10;
      -		ctx.strokeRect(1,1,canvas.width-2,canvas.height-2);
      -		ctx.lineWidth = 1;
      -	}
      -
      -	//reset in case of error
      -	ctx.restore();
      -	ctx.setTransform(1, 0, 0, 1, 0, 0);
      -
      -	if(this.graph)
      -	{
      -		//apply transformations
      -		ctx.save();
      -		ctx.scale(this.scale,this.scale);
      -		ctx.translate(this.offset[0],this.offset[1]);
      -
      -		//render BG
      -		if(this.background_image && this.scale > 0.5)
      -		{
      -			ctx.globalAlpha = (1.0 - 0.5 / this.scale) * this.editor_alpha;
      -			ctx.imageSmoothingEnabled = ctx.mozImageSmoothingEnabled = ctx.imageSmoothingEnabled = false;
      -			if(!this._bg_img || this._bg_img.name != this.background_image)
      -			{
      -				this._bg_img = new Image();
      -				this._bg_img.name = this.background_image;
      -				this._bg_img.src = this.background_image;
      -				var that = this;
      -				this._bg_img.onload = function() {
      -					that.draw(true,true);
      -				}
      -			}
      -
      -			var pattern = null;
      -			if(this._pattern == null && this._bg_img.width > 0)
      -			{
      -				pattern = ctx.createPattern( this._bg_img, 'repeat' );
      -				this._pattern_img = this._bg_img;
      -				this._pattern = pattern;
      -			}
      -			else
      -				pattern = this._pattern;
      -			if(pattern)
      -			{
      -				ctx.fillStyle = pattern;
      -				ctx.fillRect(this.visible_area[0],this.visible_area[1],this.visible_area[2],this.visible_area[3]);
      -				ctx.fillStyle = "transparent";
      -			}
      -
      -			ctx.globalAlpha = 1.0;
      -			ctx.imageSmoothingEnabled = ctx.mozImageSmoothingEnabled = ctx.imageSmoothingEnabled = true;
      -		}
      -
      -		if(this.onBackgroundRender)
      -			this.onBackgroundRender(canvas, ctx);
      -
      -		//DEBUG: show clipping area
      -		//ctx.fillStyle = "red";
      -		//ctx.fillRect( this.visible_area[0] + 10, this.visible_area[1] + 10, this.visible_area[2] - 20, this.visible_area[3] - 20);
      -
      -		//bg
      -		if (this.render_canvas_area) {
      -			ctx.strokeStyle = "#235";
      -			ctx.strokeRect(0,0,canvas.width,canvas.height);
      -		}
      -
      -		if(this.render_connections_shadows)
      -		{
      -			ctx.shadowColor = "#000";
      -			ctx.shadowOffsetX = 0;
      -			ctx.shadowOffsetY = 0;
      -			ctx.shadowBlur = 6;
      -		}
      -		else
      -			ctx.shadowColor = "rgba(0,0,0,0)";
      -
      -		//draw connections
      -		if(!this.live_mode)
      -			this.drawConnections(ctx);
      -
      -		ctx.shadowColor = "rgba(0,0,0,0)";
      -
      -		//restore state
      -		ctx.restore();
      -	}
      -
      -	if(ctx.finish)
      -		ctx.finish();
      -
      -	this.dirty_bgcanvas = false;
      -	this.dirty_canvas = true; //to force to repaint the front canvas with the bgcanvas
      -}
      -
      -var temp_vec2 = new Float32Array(2);
      -
      -/* Renders the LGraphNode on the canvas */
      -LGraphCanvas.prototype.drawNode = function(node, ctx )
      -{
      -	var glow = false;
      -
      -	var color = node.color || LiteGraph.NODE_DEFAULT_COLOR;
      -	//if (this.selected) color = "#88F";
      -
      -	var render_title = true;
      -	if(node.flags.skip_title_render || node.graph.isLive())
      -		render_title = false;
      -	if(node.mouseOver)
      -		render_title = true;
      -
      -	//shadow and glow
      -	if (node.mouseOver) glow = true;
      -
      -	if(node.selected)
      -	{
      -		/*
      -		ctx.shadowColor = "#EEEEFF";//glow ? "#AAF" : "#000";
      -		ctx.shadowOffsetX = 0;
      -		ctx.shadowOffsetY = 0;
      -		ctx.shadowBlur = 1;
      -		*/
      -	}
      -	else if(this.render_shadows)
      -	{
      -		ctx.shadowColor = "rgba(0,0,0,0.5)";
      -		ctx.shadowOffsetX = 2;
      -		ctx.shadowOffsetY = 2;
      -		ctx.shadowBlur = 3;
      -	}
      -	else
      -		ctx.shadowColor = "transparent";
      -
      -	//only render if it forces it to do it
      -	if(this.live_mode)
      -	{
      -		if(!node.flags.collapsed)
      -		{
      -			ctx.shadowColor = "transparent";
      -			//if(node.onDrawBackground)
      -			//	node.onDrawBackground(ctx);
      -			if(node.onDrawForeground)
      -				node.onDrawForeground(ctx);
      -		}
      -
      -		return;
      -	}
      -
      -	//draw in collapsed form
      -	/*
      -	if(node.flags.collapsed)
      -	{
      -		if(!node.onDrawCollapsed || node.onDrawCollapsed(ctx) == false)
      -			this.drawNodeCollapsed(node, ctx, color, node.bgcolor);
      -		return;
      -	}
      -	*/
      -
      -	var editor_alpha = this.editor_alpha;
      -	ctx.globalAlpha = editor_alpha;
      -
      -	//clip if required (mask)
      -	var shape = node._shape || LiteGraph.BOX_SHAPE;
      -	var size = temp_vec2;
      -	temp_vec2.set( node.size );
      -	if(node.flags.collapsed)
      -	{
      -		size[0] = LiteGraph.NODE_COLLAPSED_WIDTH;
      -		size[1] = 0;
      -	}
      -
      -	//Start clipping
      -	if(node.flags.clip_area)
      -	{
      -		ctx.save();
      -		if(shape == LiteGraph.BOX_SHAPE)
      -		{
      -			ctx.beginPath();
      -			ctx.rect(0,0,size[0], size[1]);
      -		}
      -		else if (shape == LiteGraph.ROUND_SHAPE)
      -		{
      -			ctx.roundRect(0,0,size[0], size[1],10);
      -		}
      -		else if (shape == LiteGraph.CIRCLE_SHAPE)
      -		{
      -			ctx.beginPath();
      -			ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5, 0, Math.PI*2);
      -		}
      -		ctx.clip();
      -	}
      -
      -	//draw shape
      -	this.drawNodeShape(node, ctx, size, color, node.bgcolor, !render_title, node.selected );
      -	ctx.shadowColor = "transparent";
      -
      -	//connection slots
      -	ctx.textAlign = "left";
      -	ctx.font = this.inner_text_font;
      -
      -	var render_text = this.scale > 0.6;
      -
      -	var out_slot = this.connecting_output;
      -
      -	//render inputs and outputs
      -	if(!node.flags.collapsed)
      -	{
      -		//input connection slots
      -		if(node.inputs)
      -			for(var i = 0; i < node.inputs.length; i++)
      -			{
      -				var slot = node.inputs[i];
      -
      -				ctx.globalAlpha = editor_alpha;
      -				//change opacity of incompatible slots
      -				if ( this.connecting_node && LiteGraph.isValidConnection( slot.type && out_slot.type ) )
      -					ctx.globalAlpha = 0.4 * editor_alpha;
      -
      -				ctx.fillStyle = slot.link != null ? this.default_connection_color.input_on : this.default_connection_color.input_off;
      -
      -				var pos = node.getConnectionPos(true,i);
      -				pos[0] -= node.pos[0];
      -				pos[1] -= node.pos[1];
      -
      -				ctx.beginPath();
      -
      -				if (slot.type === LiteGraph.EVENT)
      -					ctx.rect((pos[0] - 6) + 0.5, (pos[1] - 5) + 0.5,14,10);
      -				else
      -					ctx.arc(pos[0],pos[1],4,0,Math.PI*2);
      -
      -				ctx.fill();
      -
      -				//render name
      -				if(render_text)
      -				{
      -					var text = slot.label != null ? slot.label : slot.name;
      -					if(text)
      -					{
      -						ctx.fillStyle = color;
      -						ctx.fillText(text,pos[0] + 10,pos[1] + 5);
      -					}
      -				}
      -			}
      -
      -		//output connection slots
      -		if(this.connecting_node)
      -			ctx.globalAlpha = 0.4 * editor_alpha;
      -
      -		ctx.lineWidth = 1;
      -
      -		ctx.textAlign = "right";
      -		ctx.strokeStyle = "black";
      -		if(node.outputs)
      -			for(var i = 0; i < node.outputs.length; i++)
      -			{
      -				var slot = node.outputs[i];
      -
      -				var pos = node.getConnectionPos(false,i);
      -				pos[0] -= node.pos[0];
      -				pos[1] -= node.pos[1];
      -
      -				ctx.fillStyle = slot.links && slot.links.length ? this.default_connection_color.output_on : this.default_connection_color.output_off;
      -				ctx.beginPath();
      -				//ctx.rect( node.size[0] - 14,i*14,10,10);
      -
      -				if (slot.type === LiteGraph.EVENT)
      -					ctx.rect((pos[0] - 6) + 0.5,(pos[1] - 5) + 0.5,14,10);
      -				else
      -					ctx.arc( pos[0],pos[1],4,0, Math.PI*2 );
      -
      -				//trigger
      -				//if(slot.node_id != null && slot.slot == -1)
      -				//	ctx.fillStyle = "#F85";
      -
      -				//if(slot.links != null && slot.links.length)
      -				ctx.fill();
      -				ctx.stroke();
      -
      -				//render output name
      -				if(render_text)
      -				{
      -					var text = slot.label != null ? slot.label : slot.name;
      -					if(text)
      -					{
      -						ctx.fillStyle = color;
      -						ctx.fillText(text, pos[0] - 10,pos[1] + 5);
      -					}
      -				}
      -			}
      -
      -		ctx.textAlign = "left";
      -		ctx.globalAlpha = 1;
      -
      -		if(node.onDrawForeground)
      -			node.onDrawForeground(ctx);
      -	}//!collapsed
      -
      -	if(node.flags.clip_area)
      -		ctx.restore();
      -
      -	ctx.globalAlpha = 1.0;
      -}
      -
      -/* Renders the node shape */
      -LGraphCanvas.prototype.drawNodeShape = function(node, ctx, size, fgcolor, bgcolor, no_title, selected )
      -{
      -	//bg rect
      -	ctx.strokeStyle = fgcolor || LiteGraph.NODE_DEFAULT_COLOR;
      -	ctx.fillStyle = bgcolor || LiteGraph.NODE_DEFAULT_BGCOLOR;
      -
      -	/* gradient test
      -	var grad = ctx.createLinearGradient(0,0,0,node.size[1]);
      -	grad.addColorStop(0, "#AAA");
      -	grad.addColorStop(0.5, fgcolor || LiteGraph.NODE_DEFAULT_COLOR);
      -	grad.addColorStop(1, bgcolor || LiteGraph.NODE_DEFAULT_BGCOLOR);
      -	ctx.fillStyle = grad;
      -	//*/
      -
      -	var title_height = LiteGraph.NODE_TITLE_HEIGHT;
      -
      -	//render depending on shape
      -	var shape = node._shape || LiteGraph.BOX_SHAPE;
      -	if(shape == LiteGraph.BOX_SHAPE)
      -	{
      -		ctx.beginPath();
      -		ctx.rect(0,no_title ? 0 : -title_height, size[0]+1, no_title ? size[1] : size[1] + title_height);
      -		ctx.fill();
      -		ctx.shadowColor = "transparent";
      -
      -		if(selected)
      -		{
      -			ctx.strokeStyle = "#CCC";
      -			ctx.strokeRect(-0.5,no_title ? -0.5 : -title_height + -0.5, size[0]+2, no_title ? (size[1]+2) : (size[1] + title_height+2) - 1);
      -			ctx.strokeStyle = fgcolor;
      -		}
      -	}
      -	else if (shape == LiteGraph.ROUND_SHAPE)
      -	{
      -		ctx.roundRect(0,no_title ? 0 : -title_height,size[0], no_title ? size[1] : size[1] + title_height, 10);
      -		ctx.fill();
      -	}
      -	else if (shape == LiteGraph.CIRCLE_SHAPE)
      -	{
      -		ctx.beginPath();
      -		ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5, 0, Math.PI*2);
      -		ctx.fill();
      -	}
      -
      -	ctx.shadowColor = "transparent";
      -
      -	//ctx.stroke();
      -
      -	//image
      -	if (node.bgImage && node.bgImage.width)
      -		ctx.drawImage( node.bgImage, (size[0] - node.bgImage.width) * 0.5 , (size[1] - node.bgImage.height) * 0.5);
      -
      -	if(node.bgImageUrl && !node.bgImage)
      -		node.bgImage = node.loadImage(node.bgImageUrl);
      -
      -	if(node.onDrawBackground)
      -		node.onDrawBackground(ctx);
      -
      -	//title bg (remember, it is rendered ABOVE the node
      -	if(!no_title)
      -	{
      -		ctx.fillStyle = fgcolor || LiteGraph.NODE_DEFAULT_COLOR;
      -		var old_alpha = ctx.globalAlpha;
      -		ctx.globalAlpha = 0.5 * old_alpha;
      -		if(shape == LiteGraph.BOX_SHAPE)
      -		{
      -			ctx.beginPath();
      -			ctx.rect(0, -title_height, size[0]+1, title_height);
      -			ctx.fill()
      -			//ctx.stroke();
      -		}
      -		else if (shape == LiteGraph.ROUND_SHAPE)
      -		{
      -			ctx.roundRect(0,-title_height,size[0], title_height,10,0);
      -			//ctx.fillRect(0,8,size[0],NODE_TITLE_HEIGHT - 12);
      -			ctx.fill();
      -			//ctx.stroke();
      -		}
      -		/*
      -		else if (shape == LiteGraph.CIRCLE_SHAPE)
      -		{
      -			ctx.beginPath();
      -			ctx.arc(title_height *0.5, title_height * -0.5, (title_height - 6) *0.5,0,Math.PI*2);
      -			ctx.fill();
      -		}
      -		*/
      -
      -		//title box
      -		ctx.fillStyle = node.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR;
      -		ctx.beginPath();
      -		if (shape == LiteGraph.ROUND_SHAPE || shape == LiteGraph.CIRCLE_SHAPE)
      -			ctx.arc(title_height *0.5, title_height * -0.5, (title_height - 6) *0.5,0,Math.PI*2);
      -		else
      -			ctx.rect(3,-title_height + 3,title_height - 6,title_height - 6);
      -		ctx.fill();
      -		ctx.globalAlpha = old_alpha;
      -
      -		//title text
      -		ctx.font = this.title_text_font;
      -		var title = node.getTitle();
      -		if(title && this.scale > 0.5)
      -		{
      -			ctx.fillStyle = LiteGraph.NODE_TITLE_COLOR;
      -			ctx.fillText( title, 16, 13 - title_height );
      -		}
      -	}
      -}
      -
      -/* Renders the node when collapsed */
      -LGraphCanvas.prototype.drawNodeCollapsed = function(node, ctx, fgcolor, bgcolor)
      -{
      -	//draw default collapsed shape
      -	ctx.strokeStyle = fgcolor || LiteGraph.NODE_DEFAULT_COLOR;
      -	ctx.fillStyle = bgcolor || LiteGraph.NODE_DEFAULT_BGCOLOR;
      -
      -	var collapsed_radius = LiteGraph.NODE_COLLAPSED_RADIUS;
      -
      -	//circle shape
      -	var shape = node._shape || LiteGraph.BOX_SHAPE;
      -	if(shape == LiteGraph.CIRCLE_SHAPE)
      -	{
      -		ctx.beginPath();
      -		ctx.arc(node.size[0] * 0.5, node.size[1] * 0.5, collapsed_radius,0,Math.PI * 2);
      -		ctx.fill();
      -		ctx.shadowColor = "rgba(0,0,0,0)";
      -		ctx.stroke();
      -
      -		ctx.fillStyle = node.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR;
      -		ctx.beginPath();
      -		ctx.arc(node.size[0] * 0.5, node.size[1] * 0.5, collapsed_radius * 0.5,0,Math.PI * 2);
      -		ctx.fill();
      -	}
      -	else if(shape == LiteGraph.ROUND_SHAPE) //rounded box
      -	{
      -		ctx.beginPath();
      -		ctx.roundRect(node.size[0] * 0.5 - collapsed_radius, node.size[1] * 0.5 - collapsed_radius, 2*collapsed_radius,2*collapsed_radius,5);
      -		ctx.fill();
      -		ctx.shadowColor = "rgba(0,0,0,0)";
      -		ctx.stroke();
      -
      -		ctx.fillStyle = node.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR;
      -		ctx.beginPath();
      -		ctx.roundRect(node.size[0] * 0.5 - collapsed_radius*0.5, node.size[1] * 0.5 - collapsed_radius*0.5, collapsed_radius,collapsed_radius,2);
      -		ctx.fill();
      -	}
      -	else //flat box
      -	{
      -		ctx.beginPath();
      -		//ctx.rect(node.size[0] * 0.5 - collapsed_radius, node.size[1] * 0.5 - collapsed_radius, 2*collapsed_radius, 2*collapsed_radius);
      -		ctx.rect(0, 0, node.size[0], collapsed_radius * 2 );
      -		ctx.fill();
      -		ctx.shadowColor = "rgba(0,0,0,0)";
      -		ctx.stroke();
      -
      -		ctx.fillStyle = node.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR;
      -		ctx.beginPath();
      -		//ctx.rect(node.size[0] * 0.5 - collapsed_radius*0.5, node.size[1] * 0.5 - collapsed_radius*0.5, collapsed_radius,collapsed_radius);
      -		ctx.rect(collapsed_radius*0.5, collapsed_radius*0.5, collapsed_radius, collapsed_radius);
      -		ctx.fill();
      -	}
      -}
      -
      -//OPTIMIZE THIS: precatch connections position instead of recomputing them every time
      -LGraphCanvas.prototype.drawConnections = function(ctx)
      -{
      -	var now = LiteGraph.getTime();
      -
      -	//draw connections
      -	ctx.lineWidth = this.connections_width;
      -
      -	ctx.fillStyle = "#AAA";
      -	ctx.strokeStyle = "#AAA";
      -	ctx.globalAlpha = this.editor_alpha;
      -	//for every node
      -	for (var n = 0, l = this.graph._nodes.length; n < l; ++n)
      -	{
      -		var node = this.graph._nodes[n];
      -		//for every input (we render just inputs because it is easier as every slot can only have one input)
      -		if(node.inputs && node.inputs.length)
      -			for(var i = 0; i < node.inputs.length; ++i)
      -			{
      -				var input = node.inputs[i];
      -				if(!input || input.link == null)
      -					continue;
      -				var link_id = input.link;
      -				var link = this.graph.links[ link_id ];
      -				if(!link)
      -					continue;
      -
      -				var start_node = this.graph.getNodeById( link.origin_id );
      -				if(start_node == null) continue;
      -				var start_node_slot = link.origin_slot;
      -				var start_node_slotpos = null;
      -
      -				if(start_node_slot == -1)
      -					start_node_slotpos = [start_node.pos[0] + 10, start_node.pos[1] + 10];
      -				else
      -					start_node_slotpos = start_node.getConnectionPos(false, start_node_slot);
      -
      -				this.renderLink( ctx, start_node_slotpos, node.getConnectionPos(true,i), link );
      -
      -				//event triggered rendered on top
      -				if(link && link._last_time && (now - link._last_time) < 1000 )
      -				{
      -					var f = 2.0 - (now - link._last_time) * 0.002;
      -					var color = "rgba(255,255,255, " + f.toFixed(2) + ")";
      -					this.renderLink( ctx, start_node_slotpos, node.getConnectionPos(true,i), link, true, f, color );
      -				}
      -			}
      -	}
      -	ctx.globalAlpha = 1;
      -}
      -
      -LGraphCanvas.prototype.renderLink = function( ctx, a, b, link, skip_border, flow, color )
      -{
      -	if(!this.highquality_render)
      -	{
      -		ctx.beginPath();
      -		ctx.moveTo(a[0],a[1]);
      -		ctx.lineTo(b[0],b[1]);
      -		ctx.stroke();
      -		return;
      -	}
      -
      -	var dist = distance(a,b);
      -
      -	if(this.render_connections_border && this.scale > 0.6)
      -		ctx.lineWidth = this.connections_width + 4;
      -
      -	//choose color
      -	if( !color && link )
      -		color = LGraphCanvas.link_type_colors[ link.type ];
      -	if(!color)
      -		color = this.default_link_color;
      -
      -	if( link != null && this.highlighted_links[ link.id ] )
      -		color = "#FFF";
      -
      -	//begin line shape
      -	ctx.beginPath();
      -
      -	if(this.render_curved_connections) //splines
      -	{
      -		ctx.moveTo(a[0],a[1]);
      -		ctx.bezierCurveTo(a[0] + dist*0.25, a[1],
      -							b[0] - dist*0.25 , b[1],
      -							b[0] ,b[1] );
      -	}
      -	else //lines
      -	{
      -		ctx.moveTo(a[0]+10,a[1]);
      -		ctx.lineTo(((a[0]+10) + (b[0]-10))*0.5,a[1]);
      -		ctx.lineTo(((a[0]+10) + (b[0]-10))*0.5,b[1]);
      -		ctx.lineTo(b[0]-10,b[1]);
      -	}
      -
      -	//rendering the outline of the connection can be a little bit slow
      -	if(this.render_connections_border && this.scale > 0.6 && !skip_border)
      -	{
      -		ctx.strokeStyle = "rgba(0,0,0,0.5)";
      -		ctx.stroke();
      -	}
      -
      -	ctx.lineWidth = this.connections_width;
      -	ctx.fillStyle = ctx.strokeStyle = color;
      -	ctx.stroke();
      -	//end line shape
      -
      -	//render arrow in the middle
      -	if( this.render_connection_arrows && this.scale >= 0.6 )
      -	{
      -		//render arrow
      -		if(this.render_connection_arrows && this.scale > 0.6)
      -		{
      -			//compute two points in the connection
      -			var pos = this.computeConnectionPoint(a,b,0.5);
      -			var pos2 = this.computeConnectionPoint(a,b,0.51);
      -
      -			//compute the angle between them so the arrow points in the right direction
      -			var angle = 0;
      -			if(this.render_curved_connections)
      -				angle = -Math.atan2( pos2[0] - pos[0], pos2[1] - pos[1]);
      -			else
      -				angle = b[1] > a[1] ? 0 : Math.PI;
      -
      -			//render arrow
      -			ctx.save();
      -			ctx.translate(pos[0],pos[1]);
      -			ctx.rotate(angle);
      -			ctx.beginPath();
      -			ctx.moveTo(-5,-5);
      -			ctx.lineTo(0,+5);
      -			ctx.lineTo(+5,-5);
      -			ctx.fill();
      -			ctx.restore();
      -		}
      -	}
      -
      -	//render flowing points
      -	if(flow)
      -	{
      -		for(var i = 0; i < 5; ++i)
      -		{
      -			var f = (LiteGraph.getTime() * 0.001 + (i * 0.2)) % 1;
      -			var pos = this.computeConnectionPoint(a,b,f);
      -			ctx.beginPath();
      -			ctx.arc(pos[0],pos[1],5,0,2*Math.PI);
      -			ctx.fill();
      -		}
      -	}
      -}
      -
      -LGraphCanvas.prototype.computeConnectionPoint = function(a,b,t)
      -{
      -	var dist = distance(a,b);
      -	var p0 = a;
      -	var p1 = [ a[0] + dist*0.25, a[1] ];
      -	var p2 = [ b[0] - dist*0.25, b[1] ];
      -	var p3 = b;
      -
      -	var c1 = (1-t)*(1-t)*(1-t);
      -	var c2 = 3*((1-t)*(1-t))*t;
      -	var c3 = 3*(1-t)*(t*t);
      -	var c4 = t*t*t;
      -
      -	var x = c1*p0[0] + c2*p1[0] + c3*p2[0] + c4*p3[0];
      -	var y = c1*p0[1] + c2*p1[1] + c3*p2[1] + c4*p3[1];
      -	return [x,y];
      -}
      -
      -/*
      -LGraphCanvas.prototype.resizeCanvas = function(width,height)
      -{
      -	this.canvas.width = width;
      -	if(height)
      -		this.canvas.height = height;
      -
      -	this.bgcanvas.width = this.canvas.width;
      -	this.bgcanvas.height = this.canvas.height;
      -	this.draw(true,true);
      -}
      -*/
      -
      -LGraphCanvas.prototype.resize = function(width, height)
      -{
      -	if(!width && !height)
      -	{
      -		var parent = this.canvas.parentNode;
      -		width = parent.offsetWidth;
      -		height = parent.offsetHeight;
      -	}
      -
      -	if(this.canvas.width == width && this.canvas.height == height)
      -		return;
      -
      -	this.canvas.width = width;
      -	this.canvas.height = height;
      -	this.bgcanvas.width = this.canvas.width;
      -	this.bgcanvas.height = this.canvas.height;
      -	this.setDirty(true,true);
      -}
      -
      -
      -LGraphCanvas.prototype.switchLiveMode = function(transition)
      -{
      -	if(!transition)
      -	{
      -		this.live_mode = !this.live_mode;
      -		this.dirty_canvas = true;
      -		this.dirty_bgcanvas = true;
      -		return;
      -	}
      -
      -	var self = this;
      -	var delta = this.live_mode ? 1.1 : 0.9;
      -	if(this.live_mode)
      -	{
      -		this.live_mode = false;
      -		this.editor_alpha = 0.1;
      -	}
      -
      -	var t = setInterval(function() {
      -		self.editor_alpha *= delta;
      -		self.dirty_canvas = true;
      -		self.dirty_bgcanvas = true;
      -
      -		if(delta < 1  && self.editor_alpha < 0.01)
      -		{
      -			clearInterval(t);
      -			if(delta < 1)
      -				self.live_mode = true;
      -		}
      -		if(delta > 1 && self.editor_alpha > 0.99)
      -		{
      -			clearInterval(t);
      -			self.editor_alpha = 1;
      -		}
      -	},1);
      -}
      -
      -LGraphCanvas.prototype.onNodeSelectionChange = function(node)
      -{
      -	return; //disabled
      -	//if(this.node_in_panel) this.showNodePanel(node);
      -}
      -
      -LGraphCanvas.prototype.touchHandler = function(event)
      -{
      -	//alert("foo");
      -    var touches = event.changedTouches,
      -        first = touches[0],
      -        type = "";
      -
      -         switch(event.type)
      -    {
      -        case "touchstart": type = "mousedown"; break;
      -        case "touchmove":  type="mousemove"; break;
      -        case "touchend":   type="mouseup"; break;
      -        default: return;
      -    }
      -
      -             //initMouseEvent(type, canBubble, cancelable, view, clickCount,
      -    //           screenX, screenY, clientX, clientY, ctrlKey,
      -    //           altKey, shiftKey, metaKey, button, relatedTarget);
      -
      -	var window = this.getCanvasWindow();
      -	var document = window.document;
      -
      -    var simulatedEvent = document.createEvent("MouseEvent");
      -    simulatedEvent.initMouseEvent(type, true, true, window, 1,
      -                              first.screenX, first.screenY,
      -                              first.clientX, first.clientY, false,
      -                              false, false, false, 0/*left*/, null);
      -	first.target.dispatchEvent(simulatedEvent);
      -    event.preventDefault();
      -}
      -
      -/* CONTEXT MENU ********************/
      -
      -LGraphCanvas.onMenuAdd = function( node, options, e, prev_menu )
      -{
      -	var canvas = LGraphCanvas.active_canvas;
      -	var ref_window = canvas.getCanvasWindow();
      -
      -	var values = LiteGraph.getNodeTypesCategories();
      -	var entries = [];
      -	for(var i in values)
      -		if(values[i])
      -			entries.push({ value: values[i], content: values[i], has_submenu: true });
      -
      -	var menu = new LiteGraph.ContextMenu( entries, { event: e, callback: inner_clicked, parentMenu: prev_menu }, ref_window);
      -
      -	function inner_clicked( v, option, e )
      -	{
      -		var category = v.value;
      -		var node_types = LiteGraph.getNodeTypesInCategory(category);
      -		var values = [];
      -		for(var i in node_types)
      -			values.push( { content: node_types[i].title, value: node_types[i].type });
      -
      -		new LiteGraph.ContextMenu( values, {event: e, callback: inner_create, parentMenu: menu }, ref_window);
      -		return false;
      -	}
      -
      -	function inner_create( v, e )
      -	{
      -		var first_event = prev_menu.getFirstEvent();
      -		var node = LiteGraph.createNode( v.value );
      -		if(node)
      -		{
      -			node.pos = canvas.convertEventToCanvas( first_event );
      -			canvas.graph.add( node );
      -		}
      -	}
      -
      -	return false;
      -}
      -
      -LGraphCanvas.onMenuCollapseAll = function()
      -{
      -
      -}
      -
      -
      -LGraphCanvas.onMenuNodeEdit = function()
      -{
      -
      -}
      -
      -LGraphCanvas.showMenuNodeOptionalInputs = function( v, options, e, prev_menu, node )
      -{
      -	if(!node)
      -		return;
      -
      -	var that = this;
      -	var canvas = LGraphCanvas.active_canvas;
      -	var ref_window = canvas.getCanvasWindow();
      -
      -	var options = node.optional_inputs;
      -	if(node.onGetInputs)
      -		options = node.onGetInputs();
      -
      -	var entries = [];
      -	if(options)
      -		for (var i in options)
      -		{
      -			var entry = options[i];
      -			if(!entry)
      -			{
      -				entries.push(null);
      -				continue;
      -			}
      -			var label = entry[0];
      -			if(entry[2] && entry[2].label)
      -				label = entry[2].label;
      -			var data = {content: label, value: entry};
      -			if(entry[1] == LiteGraph.ACTION)
      -				data.className = "event";
      -			entries.push(data);
      -		}
      -
      -	if(this.onMenuNodeInputs)
      -		entries = this.onMenuNodeInputs( entries );
      -
      -	if(!entries.length)
      -		return;
      -
      -	var menu = new LiteGraph.ContextMenu(entries, { event: e, callback: inner_clicked, parentMenu: prev_menu, node: node }, ref_window);
      -
      -	function inner_clicked(v, e, prev)
      -	{
      -		if(!node)
      -			return;
      -
      -		if(v.callback)
      -			v.callback.call( that, node, v, e, prev );
      -
      -		if(v.value)
      -		{
      -			node.addInput(v.value[0],v.value[1], v.value[2]);
      -			node.setDirtyCanvas(true,true);
      -		}
      -	}
      -
      -	return false;
      -}
      -
      -LGraphCanvas.showMenuNodeOptionalOutputs = function( v, options, e, prev_menu, node )
      -{
      -	if(!node)
      -		return;
      -
      -	var that = this;
      -	var canvas = LGraphCanvas.active_canvas;
      -	var ref_window = canvas.getCanvasWindow();
      -
      -	var options = node.optional_outputs;
      -	if(node.onGetOutputs)
      -		options = node.onGetOutputs();
      -
      -	var entries = [];
      -	if(options)
      -		for (var i in options)
      -		{
      -			var entry = options[i];
      -			if(!entry) //separator?
      -			{
      -				entries.push(null);
      -				continue;
      -			}
      -
      -			if(node.flags && node.flags.skip_repeated_outputs && node.findOutputSlot(entry[0]) != -1)
      -				continue; //skip the ones already on
      -			var label = entry[0];
      -			if(entry[2] && entry[2].label)
      -				label = entry[2].label;
      -			var data = {content: label, value: entry};
      -			if(entry[1] == LiteGraph.EVENT)
      -				data.className = "event";
      -			entries.push(data);
      -		}
      -
      -	if(this.onMenuNodeOutputs)
      -		entries = this.onMenuNodeOutputs( entries );
      -
      -	if(!entries.length)
      -		return;
      -
      -	var menu = new LiteGraph.ContextMenu(entries, {event: e, callback: inner_clicked, parentMenu: prev_menu, node: node }, ref_window);
      -
      -	function inner_clicked( v, e, prev )
      -	{
      -		if(!node)
      -			return;
      -
      -		if(v.callback)
      -			v.callback.call( that, node, v, e, prev );
      -
      -		if(!v.value)
      -			return;
      -
      -		var value = v.value[1];
      -
      -		if(value && (value.constructor === Object || value.constructor === Array)) //submenu why?
      -		{
      -			var entries = [];
      -			for(var i in value)
      -				entries.push({ content: i, value: value[i]});
      -			new LiteGraph.ContextMenu( entries, { event: e, callback: inner_clicked, parentMenu: prev_menu, node: node });
      -			return false;
      -		}
      -		else
      -		{
      -			node.addOutput( v.value[0], v.value[1], v.value[2]);
      -			node.setDirtyCanvas(true,true);
      -		}
      -
      -	}
      -
      -	return false;
      -}
      -
      -LGraphCanvas.onShowMenuNodeProperties = function( value, options, e, prev_menu, node )
      -{
      -	if(!node || !node.properties)
      -		return;
      -
      -	var that = this;
      -	var canvas = LGraphCanvas.active_canvas;
      -	var ref_window = canvas.getCanvasWindow();
      -
      -	var entries = [];
      -		for (var i in node.properties)
      -		{
      -			var value = node.properties[i] !== undefined ? node.properties[i] : " ";
      -			//value could contain invalid html characters, clean that
      -			value = LGraphCanvas.decodeHTML(value);
      -			entries.push({content: "<span class='property_name'>" + i + "</span>" + "<span class='property_value'>" + value + "</span>", value: i});
      -		}
      -	if(!entries.length)
      -		return;
      -
      -	var menu = new LiteGraph.ContextMenu(entries, {event: e, callback: inner_clicked, parentMenu: prev_menu, allow_html: true, node: node },ref_window);
      -
      -	function inner_clicked( v, options, e, prev )
      -	{
      -		if(!node)
      -			return;
      -		var rect = this.getBoundingClientRect();
      -		canvas.showEditPropertyValue( node, v.value, { position: [rect.left, rect.top] });
      -	}
      -
      -	return false;
      -}
      -
      -LGraphCanvas.decodeHTML = function( str )
      -{
      -	var e = document.createElement("div");
      -	e.innerText = str;
      -	return e.innerHTML;
      -}
      -
      -LGraphCanvas.onResizeNode = function( value, options, e, menu, node )
      -{
      -	if(!node)
      -		return;
      -	node.size = node.computeSize();
      -	node.setDirtyCanvas(true,true);
      -}
      -
      -
      -LGraphCanvas.onShowTitleEditor = function( value, options, e, menu, node )
      -{
      -	var input_html = "";
      -
      -	var dialog = document.createElement("div");
      -	dialog.className = "graphdialog";
      -	dialog.innerHTML = "<span class='name'>Title</span><input autofocus type='text' class='value'/><button>OK</button>";
      -	var input = dialog.querySelector("input");
      -	if(input)
      -	{
      -		input.value = node.title;
      -		input.addEventListener("keydown", function(e){
      -			if(e.keyCode != 13)
      -				return;
      -			inner();
      -			e.preventDefault();
      -			e.stopPropagation();
      -		});
      -	}
      -
      -	var graphcanvas = LGraphCanvas.active_canvas;
      -	var canvas = graphcanvas.canvas;
      -
      -	var rect = canvas.getBoundingClientRect();
      -	var offsetx = -20;
      -	var offsety = -20;
      -	if(rect)
      -	{
      -		offsetx -= rect.left;
      -		offsety -= rect.top;
      -	}
      -
      -	if( event )
      -	{
      -		dialog.style.left = (event.pageX + offsetx) + "px";
      -		dialog.style.top = (event.pageY + offsety)+ "px";
      -	}
      -	else
      -	{
      -		dialog.style.left = (canvas.width * 0.5 + offsetx) + "px";
      -		dialog.style.top = (canvas.height * 0.5 + offsety) + "px";
      -	}
      -
      -	var button = dialog.querySelector("button");
      -	button.addEventListener("click", inner );
      -	canvas.parentNode.appendChild( dialog );
      -
      -	function inner()
      -	{
      -		setValue( input.value );
      -	}
      -
      -	function setValue(value)
      -	{
      -		node.title = value;
      -		dialog.parentNode.removeChild( dialog );
      -		node.setDirtyCanvas(true,true);
      -	}
      -}
      -
      -LGraphCanvas.prototype.showEditPropertyValue = function( node, property, options )
      -{
      -	if(!node || node.properties[ property ] === undefined )
      -		return;
      -
      -	options = options || {};
      -	var that = this;
      -
      -	var type = "string";
      -
      -	if(node.properties[ property ] !== null)
      -		type = typeof(node.properties[ property ]);
      -
      -	var info = null;
      -	if(node.getPropertyInfo)
      -		info = node.getPropertyInfo(property);
      -	if(node.properties_info)
      -	{
      -		for(var i = 0; i < node.properties_info.length; ++i)
      -		{
      -			if( node.properties_info[i].name == property )
      -			{
      -				info = node.properties_info[i];
      -				break;
      -			}
      -		}
      -	}
      -
      -	if(info !== undefined && info !== null && info.type )
      -		type = info.type;
      -
      -	var input_html = "";
      -
      -	if(type == "string" || type == "number")
      -		input_html = "<input autofocus type='text' class='value'/>";
      -	else if(type == "enum" && info.values)
      -	{
      -		input_html = "<select autofocus type='text' class='value'>";
      -		for(var i in info.values)
      -		{
      -			var v = info.values.constructor === Array ? info.values[i] : i;
      -			input_html += "<option value='"+v+"' "+(v == node.properties[property] ? "selected" : "")+">"+info.values[i]+"</option>";
      -		}
      -		input_html += "</select>";
      -	}
      -	else if(type == "boolean")
      -	{
      -		input_html = "<input autofocus type='checkbox' class='value' "+(node.properties[property] ? "checked" : "")+"/>";
      -	}
      -
      -	var dialog = this.createDialog( "<span class='name'>" + property + "</span>"+input_html+"<button>OK</button>" , options );
      -
      -	if(type == "enum" && info.values)
      -	{
      -		var input = dialog.querySelector("select");
      -		input.addEventListener("change", function(e){
      -			setValue( e.target.value );
      -			//var index = e.target.value;
      -			//setValue( e.options[e.selectedIndex].value );
      -		});
      -	}
      -	else if(type == "boolean")
      -	{
      -		var input = dialog.querySelector("input");
      -		if(input)
      -		{
      -			input.addEventListener("click", function(e){
      -				setValue( !!input.checked );
      -			});
      -		}
      -	}
      -	else
      -	{
      -		var input = dialog.querySelector("input");
      -		if(input)
      -		{
      -			input.value = node.properties[ property ] !== undefined ? node.properties[ property ] : "";
      -			input.addEventListener("keydown", function(e){
      -				if(e.keyCode != 13)
      -					return;
      -				inner();
      -				e.preventDefault();
      -				e.stopPropagation();
      -			});
      -		}
      -	}
      -
      -	var button = dialog.querySelector("button");
      -	button.addEventListener("click", inner );
      -
      -	function inner()
      -	{
      -		setValue( input.value );
      -	}
      -
      -	function setValue(value)
      -	{
      -		if(typeof( node.properties[ property ] ) == "number")
      -			value = Number(value);
      -		node.properties[ property ] = value;
      -
      -		if(node.onPropertyChanged)
      -			node.onPropertyChanged( property, value );
      -		dialog.close();
      -		node.setDirtyCanvas(true,true);
      -	}
      -}
      -
      -LGraphCanvas.prototype.createDialog = function( html, options )
      -{
      -	options = options || {};
      -
      -	var dialog = document.createElement("div");
      -	dialog.className = "graphdialog";
      -	dialog.innerHTML = html;
      -
      -	var rect = this.canvas.getBoundingClientRect();
      -	var offsetx = -20;
      -	var offsety = -20;
      -	if(rect)
      -	{
      -		offsetx -= rect.left;
      -		offsety -= rect.top;
      -	}
      -
      -	if( options.position )
      -	{
      -		offsetx += options.position[0];
      -		offsety += options.position[1];
      -	}
      -	else if( options.event )
      -	{
      -		offsetx += options.event.pageX;
      -		offsety += options.event.pageY;
      -	}
      -	else //centered
      -	{
      -		offsetx += this.canvas.width * 0.5;
      -		offsety += this.canvas.height * 0.5;
      -	}
      -
      -	dialog.style.left = offsetx + "px";
      -	dialog.style.top = offsety + "px";
      -
      -	this.canvas.parentNode.appendChild( dialog );
      -
      -	dialog.close = function()
      -	{
      -		if(this.parentNode)
      -			this.parentNode.removeChild( this );
      -	}
      -
      -	return dialog;
      -}
      -
      -LGraphCanvas.onMenuNodeCollapse = function( value, options, e, menu, node )
      -{
      -	node.flags.collapsed = !node.flags.collapsed;
      -	node.setDirtyCanvas(true,true);
      -}
      -
      -LGraphCanvas.onMenuNodePin = function( value, options, e, menu, node )
      -{
      -	node.pin();
      -}
      -
      -LGraphCanvas.onMenuNodeMode = function( value, options, e, menu, node )
      -{
      -	new LiteGraph.ContextMenu(["Always","On Event","On Trigger","Never"], {event: e, callback: inner_clicked, parentMenu: menu, node: node });
      -
      -	function inner_clicked(v)
      -	{
      -		if(!node)
      -			return;
      -		switch(v)
      -		{
      -			case "On Event": node.mode = LiteGraph.ON_EVENT; break;
      -			case "On Trigger": node.mode = LiteGraph.ON_TRIGGER; break;
      -			case "Never": node.mode = LiteGraph.NEVER; break;
      -			case "Always":
      -			default:
      -				node.mode = LiteGraph.ALWAYS; break;
      -		}
      -	}
      -
      -	return false;
      -}
      -
      -LGraphCanvas.onMenuNodeColors = function( value, options, e, menu, node )
      -{
      -	if(!node)
      -		throw("no node for color");
      -
      -	var values = [];
      -	for(var i in LGraphCanvas.node_colors)
      -	{
      -		var color = LGraphCanvas.node_colors[i];
      -		var value = {value:i, content:"<span style='display: block; color:"+color.color+"; background-color:"+color.bgcolor+"'>"+i+"</span>"};
      -		values.push(value);
      -	}
      -	new LiteGraph.ContextMenu( values, { event: e, callback: inner_clicked, parentMenu: menu, node: node });
      -
      -	function inner_clicked(v)
      -	{
      -		if(!node)
      -			return;
      -
      -		var color = LGraphCanvas.node_colors[ v.value ];
      -		if(color)
      -		{
      -			node.color = color.color;
      -			node.bgcolor = color.bgcolor;
      -			node.setDirtyCanvas(true);
      -		}
      -	}
      -
      -	return false;
      -}
      -
      -LGraphCanvas.onMenuNodeShapes = function( value, options, e, menu, node )
      -{
      -	if(!node)
      -		throw("no node passed");
      -
      -	new LiteGraph.ContextMenu( LiteGraph.VALID_SHAPES, { event: e, callback: inner_clicked, parentMenu: menu, node: node });
      -
      -	function inner_clicked(v)
      -	{
      -		if(!node)
      -			return;
      -		node.shape = v;
      -		node.setDirtyCanvas(true);
      -	}
      -
      -	return false;
      -}
      -
      -LGraphCanvas.onMenuNodeRemove = function( value, options, e, menu, node )
      -{
      -	if(!node)
      -		throw("no node passed");
      -
      -	if(node.removable == false)
      -		return;
      -	node.graph.remove(node);
      -	node.setDirtyCanvas(true,true);
      -}
      -
      -LGraphCanvas.onMenuNodeClone = function( value, options, e, menu, node )
      -{
      -	if(node.clonable == false) return;
      -	var newnode = node.clone();
      -	if(!newnode)
      -		return;
      -	newnode.pos = [node.pos[0]+5,node.pos[1]+5];
      -	node.graph.add(newnode);
      -	node.setDirtyCanvas(true,true);
      -}
      -
      -LGraphCanvas.node_colors = {
      -	"red": { color:"#FAA", bgcolor:"#944" },
      -	"green": { color:"#AFA", bgcolor:"#494" },
      -	"blue": { color:"#AAF", bgcolor:"#449" },
      -	"cyan": { color:"#AFF", bgcolor:"#499" },
      -	"purple": { color:"#FAF", bgcolor:"#949" },
      -	"yellow": { color:"#FFA", bgcolor:"#994" },
      -	"black": { color:"#777", bgcolor:"#000" },
      -	"white": { color:"#FFF", bgcolor:"#AAA" }
      -};
      -
      -LGraphCanvas.prototype.getCanvasMenuOptions = function()
      -{
      -	var options = null;
      -	if(this.getMenuOptions)
      -		options = this.getMenuOptions();
      -	else
      -	{
      -		options = [
      -			{ content:"Add Node", has_submenu: true, callback: LGraphCanvas.onMenuAdd }
      -			//{content:"Collapse All", callback: LGraphCanvas.onMenuCollapseAll }
      -		];
      -
      -		if(this._graph_stack && this._graph_stack.length > 0)
      -			options = [{content:"Close subgraph", callback: this.closeSubgraph.bind(this) },null].concat(options);
      -	}
      -
      -	if(this.getExtraMenuOptions)
      -	{
      -		var extra = this.getExtraMenuOptions(this,options);
      -		if(extra)
      -			options = options.concat( extra );
      -	}
      -
      -	return options;
      -}
      -
      -//called by processContextMenu to extract the menu list
      -LGraphCanvas.prototype.getNodeMenuOptions = function( node )
      -{
      -	var options = null;
      -
      -	if(node.getMenuOptions)
      -		options = node.getMenuOptions(this);
      -	else
      -		options = [
      -			{content:"Inputs", has_submenu: true, disabled:true, callback: LGraphCanvas.showMenuNodeOptionalInputs },
      -			{content:"Outputs", has_submenu: true, disabled:true, callback: LGraphCanvas.showMenuNodeOptionalOutputs },
      -			null,
      -			{content:"Properties", has_submenu: true, callback: LGraphCanvas.onShowMenuNodeProperties },
      -			null,
      -			{content:"Title", callback: LGraphCanvas.onShowTitleEditor },
      -			{content:"Mode", has_submenu: true, callback: LGraphCanvas.onMenuNodeMode },
      -			{content:"Resize", callback: LGraphCanvas.onResizeNode },
      -			{content:"Collapse", callback: LGraphCanvas.onMenuNodeCollapse },
      -			{content:"Pin", callback: LGraphCanvas.onMenuNodePin },
      -			{content:"Colors", has_submenu: true, callback: LGraphCanvas.onMenuNodeColors },
      -			{content:"Shapes", has_submenu: true, callback: LGraphCanvas.onMenuNodeShapes },
      -			null
      -		];
      -
      -	if(node.getExtraMenuOptions)
      -	{
      -		var extra = node.getExtraMenuOptions(this);
      -		if(extra)
      -		{
      -			extra.push(null);
      -			options = extra.concat( options );
      -		}
      -	}
      -
      -	if( node.clonable !== false )
      -			options.push({content:"Clone", callback: LGraphCanvas.onMenuNodeClone });
      -	if( node.removable !== false )
      -			options.push(null,{content:"Remove", callback: LGraphCanvas.onMenuNodeRemove });
      -
      -	if(node.onGetInputs)
      -	{
      -		var inputs = node.onGetInputs();
      -		if(inputs && inputs.length)
      -			options[0].disabled = false;
      -	}
      -
      -	if(node.onGetOutputs)
      -	{
      -		var outputs = node.onGetOutputs();
      -		if(outputs && outputs.length )
      -			options[1].disabled = false;
      -	}
      -
      -	if(node.graph && node.graph.onGetNodeMenuOptions )
      -		node.graph.onGetNodeMenuOptions( options, node );
      -
      -	return options;
      -}
      -
      -LGraphCanvas.prototype.processContextMenu = function( node, event )
      -{
      -	var that = this;
      -	var canvas = LGraphCanvas.active_canvas;
      -	var ref_window = canvas.getCanvasWindow();
      -
      -	var menu_info = null;
      -	var options = { event: event, callback: inner_option_clicked, node: node };
      -
      -	//check if mouse is in input
      -	var slot = null;
      -	if(node)
      -	{
      -		slot = node.getSlotInPosition( event.canvasX, event.canvasY );
      -		LGraphCanvas.active_node = node;
      -	}
      -
      -	if(slot)
      -	{
      -		menu_info = [];
      -		menu_info.push( slot.locked ? "Cannot remove"  : { content: "Remove Slot", slot: slot } );
      -		menu_info.push( { content: "Rename Slot", slot: slot } );
      -		options.title = (slot.input ? slot.input.type : slot.output.type) || "*";
      -		if(slot.input && slot.input.type == LiteGraph.ACTION)
      -			options.title = "Action";
      -		if(slot.output && slot.output.type == LiteGraph.EVENT)
      -			options.title = "Event";
      -
      -	}
      -	else
      -		menu_info = node ? this.getNodeMenuOptions(node) : this.getCanvasMenuOptions();
      -
      -	//show menu
      -	if(!menu_info)
      -		return;
      -
      -	var menu = new LiteGraph.ContextMenu( menu_info, options, ref_window );
      -
      -	function inner_option_clicked( v, options, e )
      -	{
      -		if(!v)
      -			return;
      -
      -		if(v.content == "Remove Slot")
      -		{
      -			var info = v.slot;
      -			if(info.input)
      -				node.removeInput( info.slot );
      -			else if(info.output)
      -				node.removeOutput( info.slot );
      -			return;
      -		}
      -		else if( v.content == "Rename Slot")
      -		{
      -			var info = v.slot;
      -			var dialog = that.createDialog( "<span class='name'>Name</span><input type='text'/><button>OK</button>" , options );
      -			var input = dialog.querySelector("input");
      -			dialog.querySelector("button").addEventListener("click",function(e){
      -				if(input.value)
      -				{
      -					var slot_info = info.input ? node.getInputInfo( info.slot ) : node.getOutputInfo( info.slot );
      -					if( slot_info )
      -						slot_info.label = input.value;
      -					that.setDirty(true);
      -				}
      -				dialog.close();
      -			});
      -		}
      -
      -		//if(v.callback)
      -		//	return v.callback.call(that, node, options, e, menu, that, event );
      -	}
      -}
      -
      -
      -
      -
      -
      -
      -//API *************************************************
      -//function roundRect(ctx, x, y, width, height, radius, radius_low) {
      -if(this.CanvasRenderingContext2D)
      -CanvasRenderingContext2D.prototype.roundRect = function (x, y, width, height, radius, radius_low) {
      -  if ( radius === undefined ) {
      -    radius = 5;
      -  }
      -
      -  if(radius_low === undefined)
      -	 radius_low  = radius;
      -
      -  this.beginPath();
      -  this.moveTo(x + radius, y);
      -  this.lineTo(x + width - radius, y);
      -  this.quadraticCurveTo(x + width, y, x + width, y + radius);
      -
      -  this.lineTo(x + width, y + height - radius_low);
      -  this.quadraticCurveTo(x + width, y + height, x + width - radius_low, y + height);
      -  this.lineTo(x + radius_low, y + height);
      -  this.quadraticCurveTo(x, y + height, x, y + height - radius_low);
      -  this.lineTo(x, y + radius);
      -  this.quadraticCurveTo(x, y, x + radius, y);
      -}
      -
      -function compareObjects(a,b)
      -{
      -	for(var i in a)
      -		if(a[i] != b[i])
      -			return false;
      -	return true;
      -}
      -LiteGraph.compareObjects = compareObjects;
      -
      -function distance(a,b)
      -{
      -	return Math.sqrt( (b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]) );
      -}
      -LiteGraph.distance = distance;
      -
      -function colorToString(c)
      -{
      -	return "rgba(" + Math.round(c[0] * 255).toFixed() + "," + Math.round(c[1] * 255).toFixed() + "," + Math.round(c[2] * 255).toFixed() + "," + (c.length == 4 ? c[3].toFixed(2) : "1.0") + ")";
      -}
      -LiteGraph.colorToString = colorToString;
      -
      -function isInsideRectangle( x,y, left, top, width, height)
      -{
      -	if (left < x && (left + width) > x &&
      -		top < y && (top + height) > y)
      -		return true;
      -	return false;
      -}
      -LiteGraph.isInsideRectangle = isInsideRectangle;
      -
      -//[minx,miny,maxx,maxy]
      -function growBounding( bounding, x,y)
      -{
      -	if(x < bounding[0])
      -		bounding[0] = x;
      -	else if(x > bounding[2])
      -		bounding[2] = x;
      -
      -	if(y < bounding[1])
      -		bounding[1] = y;
      -	else if(y > bounding[3])
      -		bounding[3] = y;
      -}
      -LiteGraph.growBounding = growBounding;
      -
      -//point inside boundin box
      -function isInsideBounding(p,bb)
      -{
      -	if (p[0] < bb[0][0] ||
      -		p[1] < bb[0][1] ||
      -		p[0] > bb[1][0] ||
      -		p[1] > bb[1][1])
      -		return false;
      -	return true;
      -}
      -LiteGraph.isInsideBounding = isInsideBounding;
      -
      -//boundings overlap, format: [ startx, starty, width, height ]
      -function overlapBounding(a,b)
      -{
      -	var A_end_x = a[0] + a[2];
      -	var A_end_y = a[1] + a[3];
      -	var B_end_x = b[0] + b[2];
      -	var B_end_y = b[1] + b[3];
      -
      -	if ( a[0] > B_end_x ||
      -		a[1] > B_end_y ||
      -		A_end_x < b[0] ||
      -		A_end_y < b[1])
      -		return false;
      -	return true;
      -}
      -LiteGraph.overlapBounding = overlapBounding;
      -
      -//Convert a hex value to its decimal value - the inputted hex must be in the
      -//	format of a hex triplet - the kind we use for HTML colours. The function
      -//	will return an array with three values.
      -function hex2num(hex) {
      -	if(hex.charAt(0) == "#") hex = hex.slice(1); //Remove the '#' char - if there is one.
      -	hex = hex.toUpperCase();
      -	var hex_alphabets = "0123456789ABCDEF";
      -	var value = new Array(3);
      -	var k = 0;
      -	var int1,int2;
      -	for(var i=0;i<6;i+=2) {
      -		int1 = hex_alphabets.indexOf(hex.charAt(i));
      -		int2 = hex_alphabets.indexOf(hex.charAt(i+1));
      -		value[k] = (int1 * 16) + int2;
      -		k++;
      -	}
      -	return(value);
      -}
      -
      -LiteGraph.hex2num = hex2num;
      -
      -//Give a array with three values as the argument and the function will return
      -//	the corresponding hex triplet.
      -function num2hex(triplet) {
      -	var hex_alphabets = "0123456789ABCDEF";
      -	var hex = "#";
      -	var int1,int2;
      -	for(var i=0;i<3;i++) {
      -		int1 = triplet[i] / 16;
      -		int2 = triplet[i] % 16;
      -
      -		hex += hex_alphabets.charAt(int1) + hex_alphabets.charAt(int2);
      -	}
      -	return(hex);
      -}
      -
      -LiteGraph.num2hex = num2hex;
      -
      -/* LiteGraph GUI elements used for canvas editing *************************************/
      -
      -/**
      -* ContextMenu from LiteGUI
      -*
      -* @class ContextMenu
      -* @constructor
      -* @param {Array} values (allows object { title: "Nice text", callback: function ... })
      -* @param {Object} options [optional] Some options:\
      -* - title: title to show on top of the menu
      -* - callback: function to call when an option is clicked, it receives the item information
      -* - ignore_item_callbacks: ignores the callback inside the item, it just calls the options.callback
      -* - event: you can pass a MouseEvent, this way the ContextMenu appears in that position
      -*/
      -function ContextMenu( values, options )
      -{
      -	options = options || {};
      -	this.options = options;
      -	var that = this;
      -
      -	//to link a menu with its parent
      -	if(options.parentMenu)
      -	{
      -		if( options.parentMenu.constructor !== this.constructor )
      -		{
      -			console.error("parentMenu must be of class ContextMenu, ignoring it");
      -			options.parentMenu = null;
      -		}
      -		else
      -		{
      -			this.parentMenu = options.parentMenu;
      -			this.parentMenu.lock = true;
      -			this.parentMenu.current_submenu = this;
      -		}
      -	}
      -
      -	if(options.event && options.event.constructor !== MouseEvent && options.event.constructor !== CustomEvent)
      -	{
      -		console.error("Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it.");
      -		options.event = null;
      -	}
      -
      -	var root = document.createElement("div");
      -	root.className = "litegraph litecontextmenu litemenubar-panel";
      -	root.style.minWidth = 100;
      -	root.style.minHeight = 100;
      -	root.style.pointerEvents = "none";
      -	setTimeout( function() { root.style.pointerEvents = "auto"; },100); //delay so the mouse up event is not caugh by this element
      -
      -	//this prevents the default context browser menu to open in case this menu was created when pressing right button
      -	root.addEventListener("mouseup", function(e){
      -		e.preventDefault(); return true;
      -	}, true);
      -	root.addEventListener("contextmenu", function(e) {
      -		if(e.button != 2) //right button
      -			return false;
      -		e.preventDefault();
      -		return false;
      -	},true);
      -
      -	root.addEventListener("mousedown", function(e){
      -		if(e.button == 2)
      -		{
      -			that.close();
      -			e.preventDefault(); return true;
      -		}
      -	}, true);
      -
      -
      -	this.root = root;
      -
      -	//title
      -	if(options.title)
      -	{
      -		var element = document.createElement("div");
      -		element.className = "litemenu-title";
      -		element.innerHTML = options.title;
      -		root.appendChild(element);
      -	}
      -
      -	//entries
      -	var num = 0;
      -	for(var i in values)
      -	{
      -		var name = values.constructor == Array ? values[i] : i;
      -		if( name != null && name.constructor !== String )
      -			name = name.content === undefined ? String(name) : name.content;
      -		var value = values[i];
      -		this.addItem( name, value, options );
      -		num++;
      -	}
      -
      -	//close on leave
      -	root.addEventListener("mouseleave", function(e) {
      -		if(that.lock)
      -			return;
      -		that.close(e);
      -	});
      -
      -	//insert before checking position
      -	var root_document = document;
      -	if(options.event)
      -		root_document = options.event.target.ownerDocument;
      -
      -	if(!root_document)
      -		root_document = document;
      -	root_document.body.appendChild(root);
      -
      -	//compute best position
      -	var left = options.left || 0;
      -	var top = options.top || 0;
      -	if(options.event)
      -	{
      -		left = (options.event.pageX - 10);
      -		top = (options.event.pageY - 10);
      -		if(options.title)
      -			top -= 20;
      -
      -		if(options.parentMenu)
      -		{
      -			var rect = options.parentMenu.root.getBoundingClientRect();
      -			left = rect.left + rect.width;
      -		}
      -
      -		var body_rect = document.body.getBoundingClientRect();
      -		var root_rect = root.getBoundingClientRect();
      -
      -		if(left > (body_rect.width - root_rect.width - 10))
      -			left = (body_rect.width - root_rect.width - 10);
      -		if(top > (body_rect.height - root_rect.height - 10))
      -			top = (body_rect.height - root_rect.height - 10);
      -	}
      -
      -	root.style.left = left + "px";
      -	root.style.top = top  + "px";
      -}
      -
      -ContextMenu.prototype.addItem = function( name, value, options )
      -{
      -	var that = this;
      -	options = options || {};
      -
      -	var element = document.createElement("div");
      -	element.className = "litemenu-entry submenu";
      -
      -	var disabled = false;
      -
      -	if(value === null)
      -	{
      -		element.classList.add("separator");
      -		//element.innerHTML = "<hr/>"
      -		//continue;
      -	}
      -	else
      -	{
      -		element.innerHTML = value && value.title ? value.title : name;
      -		element.value = value;
      -
      -		if(value)
      -		{
      -			if(value.disabled)
      -			{
      -				disabled = true;
      -				element.classList.add("disabled");
      -			}
      -			if(value.submenu || value.has_submenu)
      -				element.classList.add("has_submenu");
      -		}
      -
      -		if(typeof(value) == "function")
      -		{
      -			element.dataset["value"] = name;
      -			element.onclick_callback = value;
      -		}
      -		else
      -			element.dataset["value"] = value;
      -
      -		if(value.className)
      -			element.className += " " + value.className;
      -	}
      -
      -	this.root.appendChild(element);
      -	if(!disabled)
      -		element.addEventListener("click", inner_onclick);
      -	if(options.autoopen)
      -		element.addEventListener("mouseenter", inner_over);
      -
      -	function inner_over(e)
      -	{
      -		var value = this.value;
      -		if(!value || !value.has_submenu)
      -			return;
      -		inner_onclick.call(this,e);
      -	}
      -
      -	//menu option clicked
      -	function inner_onclick(e) {
      -		var value = this.value;
      -		var close_parent = true;
      -
      -		if(that.current_submenu)
      -			that.current_submenu.close(e);
      -
      -		//global callback
      -		if(options.callback)
      -		{
      -			var r = options.callback.call( this, value, options, e, that, options.node );
      -			if(r === true)
      -				close_parent = false;
      -		}
      -
      -		//special cases
      -		if(value)
      -		{
      -			if (value.callback && !options.ignore_item_callbacks && value.disabled !== true )  //item callback
      -			{
      -				var r = value.callback.call( this, value, options, e, that, options.node );
      -				if(r === true)
      -					close_parent = false;
      -			}
      -			if(value.submenu)
      -			{
      -				if(!value.submenu.options)
      -					throw("ContextMenu submenu needs options");
      -				var submenu = new that.constructor( value.submenu.options, {
      -					callback: value.submenu.callback,
      -					event: e,
      -					parentMenu: that,
      -					ignore_item_callbacks: value.submenu.ignore_item_callbacks,
      -					title: value.submenu.title,
      -					autoopen: options.autoopen
      -				});
      -				close_parent = false;
      -			}
      -		}
      -
      -		if(close_parent && !that.lock)
      -			that.close();
      -	}
      -
      -	return element;
      -}
      -
      -ContextMenu.prototype.close = function(e, ignore_parent_menu)
      -{
      -	if(this.root.parentNode)
      -		this.root.parentNode.removeChild( this.root );
      -	if(this.parentMenu && !ignore_parent_menu)
      -	{
      -		this.parentMenu.lock = false;
      -		this.parentMenu.current_submenu = null;
      -		if( e === undefined )
      -			this.parentMenu.close();
      -		else if( e && !ContextMenu.isCursorOverElement( e, this.parentMenu.root) )
      -		{
      -			ContextMenu.trigger( this.parentMenu.root, "mouseleave", e );
      -		}
      -	}
      -	if(this.current_submenu)
      -		this.current_submenu.close(e, true);
      -}
      -
      -//this code is used to trigger events easily (used in the context menu mouseleave
      -ContextMenu.trigger = function(element, event_name, params, origin)
      -{
      -	var evt = document.createEvent( 'CustomEvent' );
      -	evt.initCustomEvent( event_name, true,true, params ); //canBubble, cancelable, detail
      -	evt.srcElement = origin;
      -	if( element.dispatchEvent )
      -		element.dispatchEvent( evt );
      -	else if( element.__events )
      -		element.__events.dispatchEvent( evt );
      -	//else nothing seems binded here so nothing to do
      -	return evt;
      -}
      -
      -//returns the top most menu
      -ContextMenu.prototype.getTopMenu = function()
      -{
      -	if( this.options.parentMenu )
      -		return this.options.parentMenu.getTopMenu();
      -	return this;
      -}
      -
      -ContextMenu.prototype.getFirstEvent = function()
      -{
      -	if( this.options.parentMenu )
      -		return this.options.parentMenu.getFirstEvent();
      -	return this.options.event;
      -}
      -
      -
      -
      -ContextMenu.isCursorOverElement = function( event, element )
      -{
      -	var left = event.pageX;
      -	var top = event.pageY;
      -	var rect = element.getBoundingClientRect();
      -	if(!rect)
      -		return false;
      -	if(top > rect.top && top < (rect.top + rect.height) &&
      -		left > rect.left && left < (rect.left + rect.width) )
      -		return true;
      -	return false;
      -}
      -
      -
      -
      -LiteGraph.ContextMenu = ContextMenu;
      -
      -LiteGraph.closeAllContextMenus = function( ref_window )
      -{
      -	ref_window = ref_window || window;
      -
      -	var elements = ref_window.document.querySelectorAll(".litecontextmenu");
      -	if(!elements.length)
      -		return;
      -
      -	var result = [];
      -	for(var i = 0; i < elements.length; i++)
      -		result.push(elements[i]);
      -
      -	for(var i in result)
      -	{
      -		if(result[i].close)
      -			result[i].close();
      -		else if(result[i].parentNode)
      -			result[i].parentNode.removeChild( result[i] );
      -	}
      -}
      -
      -LiteGraph.extendClass = function ( target, origin )
      -{
      -	for(var i in origin) //copy class properties
      -	{
      -		if(target.hasOwnProperty(i))
      -			continue;
      -		target[i] = origin[i];
      -	}
      -
      -	if(origin.prototype) //copy prototype properties
      -		for(var i in origin.prototype) //only enumerables
      -		{
      -			if(!origin.prototype.hasOwnProperty(i))
      -				continue;
      -
      -			if(target.prototype.hasOwnProperty(i)) //avoid overwritting existing ones
      -				continue;
      -
      -			//copy getters
      -			if(origin.prototype.__lookupGetter__(i))
      -				target.prototype.__defineGetter__(i, origin.prototype.__lookupGetter__(i));
      -			else
      -				target.prototype[i] = origin.prototype[i];
      -
      -			//and setters
      -			if(origin.prototype.__lookupSetter__(i))
      -				target.prototype.__defineSetter__(i, origin.prototype.__lookupSetter__(i));
      -		}
      -}
      -
      -//used to create nodes from wrapping functions
      -LiteGraph.getParameterNames = function(func) {
      -    return (func + '')
      -      .replace(/[/][/].*$/mg,'') // strip single-line comments
      -      .replace(/\s+/g, '') // strip white space
      -      .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments  /**/
      -      .split('){', 1)[0].replace(/^[^(]*[(]/, '') // extract the parameters
      -      .replace(/=[^,]+/g, '') // strip any ES6 defaults
      -      .split(',').filter(Boolean); // split & filter [""]
      -}
      -
      -Math.clamp = function(v,a,b) { return (a > v ? a : (b < v ? b : v)); }
      -
      -if( typeof(window) != "undefined" && !window["requestAnimationFrame"] )
      -{
      -	window.requestAnimationFrame = window.webkitRequestAnimationFrame ||
      -		  window.mozRequestAnimationFrame    ||
      -		  (function( callback ){
      -			window.setTimeout(callback, 1000 / 60);
      -		  });
      -}
      -
      -})(this);
      -
      -if(typeof(exports) != "undefined")
      -	exports.LiteGraph = this.LiteGraph;
      +(function(global){
      +// *************************************************************
      +//   LiteGraph CLASS                                     *******
      +// *************************************************************
      +
      +/* FYI: links are stored in graph.links with this structure per object
      +{
      +	id: number
      +	type: string,
      +	origin_id: number,
      +	origin_slot: number,
      +	target_id: number,
      +	target_slot: number,
      +	data: *
      +};
      +*/
      +
      +/**
      +* The Global Scope. It contains all the registered node classes.
      +*
      +* @class LiteGraph
      +* @constructor
      +*/
      +
      +var LiteGraph = global.LiteGraph = {
      +
      +	NODE_TITLE_HEIGHT: 20,
      +	NODE_SLOT_HEIGHT: 15,
      +	NODE_WIDGET_HEIGHT: 20,
      +	NODE_WIDTH: 140,
      +	NODE_MIN_WIDTH: 50,
      +	NODE_COLLAPSED_RADIUS: 10,
      +	NODE_COLLAPSED_WIDTH: 80,
      +	CANVAS_GRID_SIZE: 10,
      +	NODE_TITLE_COLOR: "#999",
      +	NODE_TEXT_SIZE: 14,
      +	NODE_TEXT_COLOR: "#AAA",
      +	NODE_SUBTEXT_SIZE: 12,
      +	NODE_DEFAULT_COLOR: "#333",
      +	NODE_DEFAULT_BGCOLOR: "#444",
      +	NODE_DEFAULT_BOXCOLOR: "#888",
      +	NODE_DEFAULT_SHAPE: "box",
      +	MAX_NUMBER_OF_NODES: 1000, //avoid infinite loops
      +	DEFAULT_POSITION: [100,100],//default node position
      +	node_images_path: "",
      +
      +	VALID_SHAPES: ["default","box","round","card"], //,"circle"
      +
      +	//shapes are used for nodes but also for slots
      +	BOX_SHAPE: 1,
      +	ROUND_SHAPE: 2,
      +	CIRCLE_SHAPE: 3,
      +	CARD_SHAPE: 4,
      +	ARROW_SHAPE: 5,
      +
      +	//enums
      +	INPUT: 1,
      +	OUTPUT: 2,
      +
      +	EVENT: -1, //for outputs
      +	ACTION: -1, //for inputs
      +
      +	ALWAYS: 0,
      +	ON_EVENT: 1,
      +	NEVER: 2,
      +	ON_TRIGGER: 3,
      +
      +	NORMAL_TITLE: 0,
      +	NO_TITLE: 1,
      +	TRANSPARENT_TITLE: 2,
      +	AUTOHIDE_TITLE: 3,
      +
      +	proxy: null, //used to redirect calls
      +
      +	debug: false,
      +	throw_errors: true,
      +	allow_scripts: true,
      +	registered_node_types: {}, //nodetypes by string
      +	node_types_by_file_extension: {}, //used for droping files in the canvas
      +	Nodes: {}, //node types by classname
      +
      +	/**
      +	* Register a node class so it can be listed when the user wants to create a new one
      +	* @method registerNodeType
      +	* @param {String} type name of the node and path
      +	* @param {Class} base_class class containing the structure of a node
      +	*/
      +
      +	registerNodeType: function(type, base_class)
      +	{
      +		if(!base_class.prototype)
      +			throw("Cannot register a simple object, it must be a class with a prototype");
      +		base_class.type = type;
      +
      +		if(LiteGraph.debug)
      +			console.log("Node registered: " + type);
      +
      +		var categories = type.split("/");
      +		var classname = base_class.name;
      +
      +		var pos = type.lastIndexOf("/");
      +		base_class.category = type.substr(0,pos);
      +
      +		if(!base_class.title)
      +			base_class.title = classname;
      +		//info.name = name.substr(pos+1,name.length - pos);
      +
      +		//extend class
      +		if(base_class.prototype) //is a class
      +			for(var i in LGraphNode.prototype)
      +				if(!base_class.prototype[i])
      +					base_class.prototype[i] = LGraphNode.prototype[i];
      +
      +		Object.defineProperty( base_class.prototype, "shape",{
      +			set: function(v) {
      +				switch(v)
      +				{
      +					case "default": delete this._shape; break;
      +					case "box": this._shape = LiteGraph.BOX_SHAPE; break;
      +					case "round": this._shape = LiteGraph.ROUND_SHAPE; break;
      +					case "circle": this._shape = LiteGraph.CIRCLE_SHAPE; break;
      +					case "card": this._shape = LiteGraph.CARD_SHAPE; break;
      +					default:
      +						this._shape = v;
      +				}
      +			},
      +			get: function(v)
      +			{
      +				return this._shape;
      +			},
      +			enumerable: true
      +		});
      +
      +		this.registered_node_types[ type ] = base_class;
      +		if(base_class.constructor.name)
      +			this.Nodes[ classname ] = base_class;
      +
      +		//warnings
      +		if(base_class.prototype.onPropertyChange)
      +			console.warn("LiteGraph node class " + type + " has onPropertyChange method, it must be called onPropertyChanged with d at the end");
      +
      +		if( base_class.supported_extensions )
      +		{
      +			for(var i in base_class.supported_extensions )
      +				this.node_types_by_file_extension[ base_class.supported_extensions[i].toLowerCase() ] = base_class;
      +		}
      +	},
      +
      +	/**
      +	* Create a new node type by passing a function, it wraps it with a propper class and generates inputs according to the parameters of the function.
      +	* Useful to wrap simple methods that do not require properties, and that only process some input to generate an output.
      +	* @method wrapFunctionAsNode
      +	* @param {String} name node name with namespace (p.e.: 'math/sum')
      +	* @param {Function} func
      +	* @param {Array} param_types [optional] an array containing the type of every parameter, otherwise parameters will accept any type
      +	* @param {String} return_type [optional] string with the return type, otherwise it will be generic
      +	*/
      +	wrapFunctionAsNode: function( name, func, param_types, return_type )
      +	{
      +		var params = Array(func.length);
      +		var code = "";
      +		var names = LiteGraph.getParameterNames( func );
      +		for(var i = 0; i < names.length; ++i)
      +			code += "this.addInput('"+names[i]+"',"+(param_types && param_types[i] ? "'" + param_types[i] + "'" : "0") + ");\n";
      +		code += "this.addOutput('out',"+( return_type ? "'" + return_type + "'" : 0 )+");\n";
      +		var classobj = Function(code);
      +		classobj.title = name.split("/").pop();
      +		classobj.desc = "Generated from " + func.name;
      +		classobj.prototype.onExecute = function onExecute()
      +		{
      +			for(var i = 0; i < params.length; ++i)
      +				params[i] = this.getInputData(i);
      +			var r = func.apply( this, params );
      +			this.setOutputData(0,r);
      +		}
      +		this.registerNodeType( name, classobj );
      +	},
      +
      +	/**
      +	* Adds this method to all nodetypes, existing and to be created
      +	* (You can add it to LGraphNode.prototype but then existing node types wont have it)
      +	* @method addNodeMethod
      +	* @param {Function} func
      +	*/
      +	addNodeMethod: function( name, func )
      +	{
      +		LGraphNode.prototype[name] = func;
      +		for(var i in this.registered_node_types)
      +		{
      +			var type = this.registered_node_types[i];
      +			if(type.prototype[name])
      +				type.prototype["_" + name] = type.prototype[name]; //keep old in case of replacing
      +			type.prototype[name] = func;
      +		}
      +	},
      +
      +	/**
      +	* Create a node of a given type with a name. The node is not attached to any graph yet.
      +	* @method createNode
      +	* @param {String} type full name of the node class. p.e. "math/sin"
      +	* @param {String} name a name to distinguish from other nodes
      +	* @param {Object} options to set options
      +	*/
      +
      +	createNode: function( type, title, options )
      +	{
      +		var base_class = this.registered_node_types[type];
      +		if (!base_class)
      +		{
      +			if(LiteGraph.debug)
      +				console.log("GraphNode type \"" + type + "\" not registered.");
      +			return null;
      +		}
      +
      +		var prototype = base_class.prototype || base_class;
      +
      +		title = title || base_class.title || type;
      +
      +		var node = new base_class( title );
      +		node.type = type;
      +
      +		if(!node.title && title) node.title = title;
      +		if(!node.properties) node.properties = {};
      +		if(!node.properties_info) node.properties_info = [];
      +		if(!node.flags) node.flags = {};
      +		if(!node.size) node.size = node.computeSize();
      +		if(!node.pos) node.pos = LiteGraph.DEFAULT_POSITION.concat();
      +		if(!node.mode) node.mode = LiteGraph.ALWAYS;
      +
      +		//extra options
      +		if(options)
      +		{
      +			for(var i in options)
      +				node[i] = options[i];
      +		}
      +
      +		return node;
      +	},
      +
      +	/**
      +	* Returns a registered node type with a given name
      +	* @method getNodeType
      +	* @param {String} type full name of the node class. p.e. "math/sin"
      +	* @return {Class} the node class
      +	*/
      +
      +	getNodeType: function(type)
      +	{
      +		return this.registered_node_types[type];
      +	},
      +
      +
      +	/**
      +	* Returns a list of node types matching one category
      +	* @method getNodeType
      +	* @param {String} category category name
      +	* @return {Array} array with all the node classes
      +	*/
      +
      +	getNodeTypesInCategory: function( category, filter )
      +	{
      +		var r = [];
      +		for(var i in this.registered_node_types)
      +		{
      +			var type = this.registered_node_types[i];
      +			if(filter && type.filter && type.filter != filter)
      +				continue;
      +
      +			if(category == "" )
      +			{
      +				if (type.category == null)
      +					r.push(type);
      +			}
      +			else if (type.category == category)
      +				r.push(type);
      +		}
      +
      +		return r;
      +	},
      +
      +	/**
      +	* Returns a list with all the node type categories
      +	* @method getNodeTypesCategories
      +	* @return {Array} array with all the names of the categories
      +	*/
      +
      +	getNodeTypesCategories: function()
      +	{
      +		var categories = {"":1};
      +		for(var i in this.registered_node_types)
      +			if(this.registered_node_types[i].category && !this.registered_node_types[i].skip_list)
      +				categories[ this.registered_node_types[i].category ] = 1;
      +		var result = [];
      +		for(var i in categories)
      +			result.push(i);
      +		return result;
      +	},
      +
      +	//debug purposes: reloads all the js scripts that matches a wilcard
      +	reloadNodes: function (folder_wildcard)
      +	{
      +		var tmp = document.getElementsByTagName("script");
      +		//weird, this array changes by its own, so we use a copy
      +		var script_files = [];
      +		for(var i in tmp)
      +			script_files.push(tmp[i]);
      +
      +
      +		var docHeadObj = document.getElementsByTagName("head")[0];
      +		folder_wildcard = document.location.href + folder_wildcard;
      +
      +		for(var i in script_files)
      +		{
      +			var src = script_files[i].src;
      +			if( !src || src.substr(0,folder_wildcard.length ) != folder_wildcard)
      +				continue;
      +
      +			try
      +			{
      +				if(LiteGraph.debug)
      +					console.log("Reloading: " + src);
      +				var dynamicScript = document.createElement("script");
      +				dynamicScript.type = "text/javascript";
      +				dynamicScript.src = src;
      +				docHeadObj.appendChild(dynamicScript);
      +				docHeadObj.removeChild(script_files[i]);
      +			}
      +			catch (err)
      +			{
      +				if(LiteGraph.throw_errors)
      +					throw err;
      +				if(LiteGraph.debug)
      +					console.log("Error while reloading " + src);
      +			}
      +		}
      +
      +		if(LiteGraph.debug)
      +			console.log("Nodes reloaded");
      +	},
      +
      +	//separated just to improve if it doesnt work
      +	cloneObject: function(obj, target)
      +	{
      +		if(obj == null) return null;
      +		var r = JSON.parse( JSON.stringify( obj ) );
      +		if(!target) return r;
      +
      +		for(var i in r)
      +			target[i] = r[i];
      +		return target;
      +	},
      +
      +	isValidConnection: function( type_a, type_b )
      +	{
      +		if( !type_a ||  //generic output
      +			!type_b || //generic input
      +			type_a == type_b || //same type (is valid for triggers)
      +			type_a == LiteGraph.EVENT && type_b == LiteGraph.ACTION )
      +        return true;
      +
      +		// Enforce string type to handle toLowerCase call (-1 number not ok)
      +		type_a = String(type_a); 
      +		type_b = String(type_b);
      +		type_a = type_a.toLowerCase();
      +		type_b = type_b.toLowerCase();
      +
      +		// For nodes supporting multiple connection types
      +		if( type_a.indexOf(",") == -1 && type_b.indexOf(",") == -1 )
      +			return type_a == type_b;
      +
      +		// Check all permutations to see if one is valid
      +		var supported_types_a = type_a.split(",");
      +		var supported_types_b = type_b.split(",");
      +		for(var i = 0; i < supported_types_a.length; ++i)
      +			for(var j = 0; j < supported_types_b.length; ++j)
      +				if( supported_types_a[i] == supported_types_b[j] )
      +					return true;
      +
      +		return false;
      +	}
      +};
      +
      +//timer that works everywhere
      +if(typeof(performance) != "undefined")
      +	LiteGraph.getTime = performance.now.bind(performance);
      +else if(typeof(Date) != "undefined" && Date.now)
      +	LiteGraph.getTime = Date.now.bind(Date);
      +else if(typeof(process) != "undefined")
      +	LiteGraph.getTime = function(){
      +		var t = process.hrtime();
      +		return t[0]*0.001 + t[1]*(1e-6);
      +	}
      +else
      +  LiteGraph.getTime = function getTime() { return (new Date).getTime(); }
      +
      +
      +
      +
      +
      +
      +//*********************************************************************************
      +// LGraph CLASS
      +//*********************************************************************************
      +
      +/**
      +* LGraph is the class that contain a full graph. We instantiate one and add nodes to it, and then we can run the execution loop.
      +*
      +* @class LGraph
      +* @constructor
      +* @param {Object} o data from previous serialization [optional]
      +*/
      +
      +function LGraph( o )
      +{
      +	if (LiteGraph.debug)
      +		console.log("Graph created");
      +	this.list_of_graphcanvas = null;
      +	this.clear();
      +
      +	if(o)
      +		this.configure(o);
      +}
      +
      +global.LGraph = LiteGraph.LGraph = LGraph;
      +
      +//default supported types
      +LGraph.supported_types = ["number","string","boolean"];
      +
      +//used to know which types of connections support this graph (some graphs do not allow certain types)
      +LGraph.prototype.getSupportedTypes = function() { return this.supported_types || LGraph.supported_types; }
      +
      +LGraph.STATUS_STOPPED = 1;
      +LGraph.STATUS_RUNNING = 2;
      +
      +/**
      +* Removes all nodes from this graph
      +* @method clear
      +*/
      +
      +LGraph.prototype.clear = function()
      +{
      +	this.stop();
      +	this.status = LGraph.STATUS_STOPPED;
      +
      +	this.last_node_id = 1;
      +	this.last_link_id = 1;
      +
      +	this._version = -1; //used to detect changes
      +
      +	//nodes
      +	this._nodes = [];
      +	this._nodes_by_id = {};
      +	this._nodes_in_order = []; //nodes that are executable sorted in execution order
      +	this._nodes_executable = null; //nodes that contain onExecute
      +
      +	//other scene stuff
      +	this._groups = [];
      +
      +	//links
      +	this.links = {}; //container with all the links
      +
      +	//iterations
      +	this.iteration = 0;
      +
      +	//custom data
      +	this.config = {};
      +
      +	//timing
      +	this.globaltime = 0;
      +	this.runningtime = 0;
      +	this.fixedtime =  0;
      +	this.fixedtime_lapse = 0.01;
      +	this.elapsed_time = 0.01;
      +	this.last_update_time = 0;
      +	this.starttime = 0;
      +
      +	this.catch_errors = true;
      +
      +	//subgraph_data
      +	this.global_inputs = {};
      +	this.global_outputs = {};
      +
      +	//notify canvas to redraw
      +	this.change();
      +
      +	this.sendActionToCanvas("clear");
      +}
      +
      +/**
      +* Attach Canvas to this graph
      +* @method attachCanvas
      +* @param {GraphCanvas} graph_canvas
      +*/
      +
      +LGraph.prototype.attachCanvas = function(graphcanvas)
      +{
      +	if(graphcanvas.constructor != LGraphCanvas)
      +		throw("attachCanvas expects a LGraphCanvas instance");
      +	if(graphcanvas.graph && graphcanvas.graph != this)
      +		graphcanvas.graph.detachCanvas( graphcanvas );
      +
      +	graphcanvas.graph = this;
      +	if(!this.list_of_graphcanvas)
      +		this.list_of_graphcanvas = [];
      +	this.list_of_graphcanvas.push(graphcanvas);
      +}
      +
      +/**
      +* Detach Canvas from this graph
      +* @method detachCanvas
      +* @param {GraphCanvas} graph_canvas
      +*/
      +LGraph.prototype.detachCanvas = function(graphcanvas)
      +{
      +	if(!this.list_of_graphcanvas)
      +		return;
      +
      +	var pos = this.list_of_graphcanvas.indexOf( graphcanvas );
      +	if(pos == -1)
      +		return;
      +	graphcanvas.graph = null;
      +	this.list_of_graphcanvas.splice(pos,1);
      +}
      +
      +/**
      +* Starts running this graph every interval milliseconds.
      +* @method start
      +* @param {number} interval amount of milliseconds between executions, default is 1
      +*/
      +
      +LGraph.prototype.start = function(interval)
      +{
      +	if( this.status == LGraph.STATUS_RUNNING )
      +		return;
      +	this.status = LGraph.STATUS_RUNNING;
      +
      +	if(this.onPlayEvent)
      +		this.onPlayEvent();
      +
      +	this.sendEventToAllNodes("onStart");
      +
      +	//launch
      +	this.starttime = LiteGraph.getTime();
      +	this.last_update_time = this.starttime;
      +	interval = interval || 1;
      +	var that = this;
      +
      +	this.execution_timer_id = setInterval( function() {
      +		//execute
      +		that.runStep(1, !this.catch_errors );
      +	},interval);
      +}
      +
      +/**
      +* Stops the execution loop of the graph
      +* @method stop execution
      +*/
      +
      +LGraph.prototype.stop = function()
      +{
      +	if( this.status == LGraph.STATUS_STOPPED )
      +		return;
      +
      +	this.status = LGraph.STATUS_STOPPED;
      +
      +	if(this.onStopEvent)
      +		this.onStopEvent();
      +
      +	if(this.execution_timer_id != null)
      +		clearInterval(this.execution_timer_id);
      +	this.execution_timer_id = null;
      +
      +	this.sendEventToAllNodes("onStop");
      +}
      +
      +/**
      +* Run N steps (cycles) of the graph
      +* @method runStep
      +* @param {number} num number of steps to run, default is 1
      +*/
      +
      +LGraph.prototype.runStep = function( num, do_not_catch_errors )
      +{
      +	num = num || 1;
      +
      +	var start = LiteGraph.getTime();
      +	this.globaltime = 0.001 * (start - this.starttime);
      +
      +	var nodes = this._nodes_executable ? this._nodes_executable : this._nodes;
      +	if(!nodes)
      +		return;
      +
      +	if( do_not_catch_errors )
      +	{
      +		//iterations
      +		for(var i = 0; i < num; i++)
      +		{
      +			for( var j = 0, l = nodes.length; j < l; ++j )
      +			{
      +				var node = nodes[j];
      +				if( node.mode == LiteGraph.ALWAYS && node.onExecute )
      +					node.onExecute();
      +			}
      +
      +			this.fixedtime += this.fixedtime_lapse;
      +			if( this.onExecuteStep )
      +				this.onExecuteStep();
      +		}
      +
      +		if( this.onAfterExecute )
      +			this.onAfterExecute();
      +	}
      +	else
      +	{
      +		try
      +		{
      +			//iterations
      +			for(var i = 0; i < num; i++)
      +			{
      +				for( var j = 0, l = nodes.length; j < l; ++j )
      +				{
      +					var node = nodes[j];
      +					if( node.mode == LiteGraph.ALWAYS && node.onExecute )
      +						node.onExecute();
      +				}
      +
      +				this.fixedtime += this.fixedtime_lapse;
      +				if( this.onExecuteStep )
      +					this.onExecuteStep();
      +			}
      +
      +			if( this.onAfterExecute )
      +				this.onAfterExecute();
      +			this.errors_in_execution = false;
      +		}
      +		catch (err)
      +		{
      +			this.errors_in_execution = true;
      +			if(LiteGraph.throw_errors)
      +				throw err;
      +			if(LiteGraph.debug)
      +				console.log("Error during execution: " + err);
      +			this.stop();
      +		}
      +	}
      +
      +	var now = LiteGraph.getTime();
      +	var elapsed = now - start;
      +	if (elapsed == 0)
      +		elapsed = 1;
      +	this.execution_time = 0.001 * elapsed;
      +	this.globaltime += 0.001 * elapsed;
      +	this.iteration += 1;
      +	this.elapsed_time = (now - this.last_update_time) * 0.001;
      +	this.last_update_time = now;
      +}
      +
      +/**
      +* Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than
      +* nodes with only inputs.
      +* @method updateExecutionOrder
      +*/
      +LGraph.prototype.updateExecutionOrder = function()
      +{
      +	this._nodes_in_order = this.computeExecutionOrder( false );
      +	this._nodes_executable = [];
      +	for(var i = 0; i < this._nodes_in_order.length; ++i)
      +		if( this._nodes_in_order[i].onExecute )
      +			this._nodes_executable.push( this._nodes_in_order[i] );
      +}
      +
      +//This is more internal, it computes the order and returns it
      +LGraph.prototype.computeExecutionOrder = function( only_onExecute, set_level )
      +{
      +	var L = [];
      +	var S = [];
      +	var M = {};
      +	var visited_links = {}; //to avoid repeating links
      +	var remaining_links = {}; //to a
      +
      +	//search for the nodes without inputs (starting nodes)
      +	for (var i = 0, l = this._nodes.length; i < l; ++i)
      +	{
      +		var node = this._nodes[i];
      +		if( only_onExecute && !node.onExecute )
      +			continue;
      +
      +		M[node.id] = node; //add to pending nodes
      +
      +		var num = 0; //num of input connections
      +		if(node.inputs)
      +			for(var j = 0, l2 = node.inputs.length; j < l2; j++)
      +				if(node.inputs[j] && node.inputs[j].link != null)
      +					num += 1;
      +
      +		if(num == 0) //is a starting node
      +		{
      +			S.push(node);
      +			if(set_level)
      +				node._level = 1;
      +		}
      +		else //num of input links
      +		{
      +			if(set_level)
      +				node._level = 0;
      +			remaining_links[node.id] = num;
      +		}
      +	}
      +
      +	while(true)
      +	{
      +		if(S.length == 0)
      +			break;
      +
      +		//get an starting node
      +		var node = S.shift();
      +		L.push(node); //add to ordered list
      +		delete M[node.id]; //remove from the pending nodes
      +
      +		if(!node.outputs)
      +			continue;
      +
      +		//for every output
      +		for(var i = 0; i < node.outputs.length; i++)
      +		{
      +			var output = node.outputs[i];
      +			//not connected
      +			if(output == null || output.links == null || output.links.length == 0)
      +				continue;
      +
      +			//for every connection
      +			for(var j = 0; j < output.links.length; j++)
      +			{
      +				var link_id = output.links[j];
      +				var link = this.links[link_id];
      +				if(!link)
      +					continue;
      +
      +				//already visited link (ignore it)
      +				if(visited_links[ link.id ])
      +					continue;
      +
      +				var target_node = this.getNodeById( link.target_id );
      +				if(target_node == null)
      +				{
      +					visited_links[ link.id ] = true;
      +					continue;
      +				}
      +
      +				if(set_level && (!target_node._level || target_node._level <= node._level))
      +					target_node._level = node._level + 1;
      +
      +				visited_links[link.id] = true; //mark as visited
      +				remaining_links[target_node.id] -= 1; //reduce the number of links remaining
      +				if (remaining_links[ target_node.id ] == 0)
      +					S.push(target_node); //if no more links, then add to starters array
      +			}
      +		}
      +	}
      +
      +	//the remaining ones (loops)
      +	for(var i in M)
      +		L.push( M[i] );
      +
      +	if( L.length != this._nodes.length && LiteGraph.debug )
      +		console.warn("something went wrong, nodes missing");
      +
      +	var l = L.length;
      +
      +	//save order number in the node
      +	for(var i = 0; i < l; ++i)
      +		L[i].order = i;
      +
      +	//sort now by priority
      +	L = L.sort(function(A,B){ 
      +		var Ap = A.constructor.priority || A.priority || 0;
      +		var Bp = B.constructor.priority || B.priority || 0;
      +		if(Ap == Bp)
      +			return A.order - B.order;
      +		return Ap - Bp;
      +	});
      +
      +	//save order number in the node, again...
      +	for(var i = 0; i < l; ++i)
      +		L[i].order = i;
      +
      +	return L;
      +}
      +
      +/**
      +* Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively.
      +* It doesnt include the node itself
      +* @method getAncestors
      +* @return {Array} an array with all the LGraphNodes that affect this node, in order of execution
      +*/
      +LGraph.prototype.getAncestors = function( node )
      +{
      +	var ancestors = [];
      +	var pending = [node];
      +	var visited = {};
      +
      +	while (pending.length)
      +	{
      +		var current = pending.shift();
      +		if(!current.inputs)
      +			continue;
      +		if( !visited[ current.id ] && current != node )
      +		{
      +			visited[ current.id ] = true;
      +			ancestors.push( current );
      +		}
      +
      +		for(var i = 0; i < current.inputs.length;++i)
      +		{
      +			var input = current.getInputNode(i);
      +			if( input && ancestors.indexOf( input ) == -1)
      +			{
      +				pending.push( input );
      +			}
      +		}
      +	}
      +
      +	ancestors.sort(function(a,b){ return a.order - b.order;});
      +	return ancestors;
      +}
      +
      +/**
      +* Positions every node in a more readable manner
      +* @method arrange
      +*/
      +LGraph.prototype.arrange = function( margin )
      +{
      +	margin = margin || 40;
      +
      +	var nodes = this.computeExecutionOrder( false, true );
      +	var columns = [];
      +	for(var i = 0; i < nodes.length; ++i)
      +	{
      +		var node = nodes[i];
      +		var col = node._level || 1;
      +		if(!columns[col])
      +			columns[col] = [];
      +		columns[col].push( node );
      +	}
      +
      +	var x = margin;
      +
      +	for(var i = 0; i < columns.length; ++i)
      +	{
      +		var column = columns[i];
      +		if(!column)
      +			continue;
      +		var max_size = 100;
      +		var y = margin;
      +		for(var j = 0; j < column.length; ++j)
      +		{
      +			var node = column[j];
      +			node.pos[0] = x;
      +			node.pos[1] = y;
      +			if(node.size[0] > max_size)
      +				max_size = node.size[0];
      +			y += node.size[1] + margin;
      +		}
      +		x += max_size + margin;
      +	}
      +
      +	this.setDirtyCanvas(true,true);
      +}
      +
      +
      +/**
      +* Returns the amount of time the graph has been running in milliseconds
      +* @method getTime
      +* @return {number} number of milliseconds the graph has been running
      +*/
      +LGraph.prototype.getTime = function()
      +{
      +	return this.globaltime;
      +}
      +
      +/**
      +* Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant
      +* @method getFixedTime
      +* @return {number} number of milliseconds the graph has been running
      +*/
      +
      +LGraph.prototype.getFixedTime = function()
      +{
      +	return this.fixedtime;
      +}
      +
      +/**
      +* Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct
      +* if the nodes are using graphical actions
      +* @method getElapsedTime
      +* @return {number} number of milliseconds it took the last cycle
      +*/
      +
      +LGraph.prototype.getElapsedTime = function()
      +{
      +	return this.elapsed_time;
      +}
      +
      +/**
      +* Sends an event to all the nodes, useful to trigger stuff
      +* @method sendEventToAllNodes
      +* @param {String} eventname the name of the event (function to be called)
      +* @param {Array} params parameters in array format
      +*/
      +
      +LGraph.prototype.sendEventToAllNodes = function( eventname, params, mode )
      +{
      +	mode = mode || LiteGraph.ALWAYS;
      +
      +	var nodes = this._nodes_in_order ? this._nodes_in_order : this._nodes;
      +	if(!nodes)
      +		return;
      +
      +	for( var j = 0, l = nodes.length; j < l; ++j )
      +	{
      +		var node = nodes[j];
      +		if(node[eventname] && node.mode == mode )
      +		{
      +			if(params === undefined)
      +				node[eventname]();
      +			else if(params && params.constructor === Array)
      +				node[eventname].apply( node, params );
      +			else
      +				node[eventname](params);
      +		}
      +	}
      +}
      +
      +LGraph.prototype.sendActionToCanvas = function(action, params)
      +{
      +	if(!this.list_of_graphcanvas)
      +		return;
      +
      +	for(var i = 0; i < this.list_of_graphcanvas.length; ++i)
      +	{
      +		var c = this.list_of_graphcanvas[i];
      +		if( c[action] )
      +			c[action].apply(c, params);
      +	}
      +}
      +
      +/**
      +* Adds a new node instasnce to this graph
      +* @method add
      +* @param {LGraphNode} node the instance of the node
      +*/
      +
      +LGraph.prototype.add = function( node, skip_compute_order)
      +{
      +	if(!node)
      +		return;
      +
      +	//groups
      +	if( node.constructor === LGraphGroup )
      +	{
      +		this._groups.push( node );
      +		this.setDirtyCanvas(true);
      +		this.change();
      +		node.graph = this;
      +		this._version++;
      +		return;
      +	}
      +
      +	//nodes
      +	if(node.id != -1 && this._nodes_by_id[node.id] != null)
      +	{
      +		console.warn("LiteGraph: there is already a node with this ID, changing it");
      +		node.id = ++this.last_node_id;
      +	}
      +
      +	if(this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES)
      +		throw("LiteGraph: max number of nodes in a graph reached");
      +
      +	//give him an id
      +	if(node.id == null || node.id == -1)
      +		node.id = ++this.last_node_id;
      +	else if (this.last_node_id < node.id)
      +		this.last_node_id = node.id;
      +
      +
      +	node.graph = this;
      +	this._version++;
      +
      +	this._nodes.push(node);
      +	this._nodes_by_id[node.id] = node;
      +
      +	if(node.onAdded)
      +		node.onAdded( this );
      +
      +	if(this.config.align_to_grid)
      +		node.alignToGrid();
      +
      +	if(!skip_compute_order)
      +		this.updateExecutionOrder();
      +
      +	if(this.onNodeAdded)
      +		this.onNodeAdded(node);
      +
      +
      +	this.setDirtyCanvas(true);
      +	this.change();
      +
      +	return node; //to chain actions
      +}
      +
      +/**
      +* Removes a node from the graph
      +* @method remove
      +* @param {LGraphNode} node the instance of the node
      +*/
      +
      +LGraph.prototype.remove = function(node)
      +{
      +	if(node.constructor === LiteGraph.LGraphGroup)
      +	{
      +		var index = this._groups.indexOf(node);
      +		if(index != -1)
      +			this._groups.splice(index,1);
      +		node.graph = null;
      +		this._version++;
      +		this.setDirtyCanvas(true,true);
      +		this.change();
      +		return;
      +	}
      +
      +	if(this._nodes_by_id[node.id] == null)
      +		return; //not found
      +
      +	if(node.ignore_remove)
      +		return; //cannot be removed
      +
      +	//disconnect inputs
      +	if(node.inputs)
      +		for(var i = 0; i < node.inputs.length; i++)
      +		{
      +			var slot = node.inputs[i];
      +			if(slot.link != null)
      +				node.disconnectInput(i);
      +		}
      +
      +	//disconnect outputs
      +	if(node.outputs)
      +		for(var i = 0; i < node.outputs.length; i++)
      +		{
      +			var slot = node.outputs[i];
      +			if(slot.links != null && slot.links.length)
      +				node.disconnectOutput(i);
      +		}
      +
      +	//node.id = -1; //why?
      +
      +	//callback
      +	if(node.onRemoved)
      +		node.onRemoved();
      +
      +	node.graph = null;
      +	this._version++;
      +
      +	//remove from canvas render
      +	if(this.list_of_graphcanvas)
      +	{
      +		for(var i = 0; i < this.list_of_graphcanvas.length; ++i)
      +		{
      +			var canvas = this.list_of_graphcanvas[i];
      +			if(canvas.selected_nodes[node.id])
      +				delete canvas.selected_nodes[node.id];
      +			if(canvas.node_dragged == node)
      +				canvas.node_dragged = null;
      +		}
      +	}
      +
      +	//remove from containers
      +	var pos = this._nodes.indexOf(node);
      +	if(pos != -1)
      +		this._nodes.splice(pos,1);
      +	delete this._nodes_by_id[node.id];
      +
      +	if(this.onNodeRemoved)
      +		this.onNodeRemoved(node);
      +
      +	this.setDirtyCanvas(true,true);
      +	this.change();
      +
      +	this.updateExecutionOrder();
      +}
      +
      +/**
      +* Returns a node by its id.
      +* @method getNodeById
      +* @param {Number} id
      +*/
      +
      +LGraph.prototype.getNodeById = function( id )
      +{
      +	if( id == null )
      +		return null;
      +	return this._nodes_by_id[ id ];
      +}
      +
      +/**
      +* Returns a list of nodes that matches a class
      +* @method findNodesByClass
      +* @param {Class} classObject the class itself (not an string)
      +* @return {Array} a list with all the nodes of this type
      +*/
      +
      +LGraph.prototype.findNodesByClass = function(classObject)
      +{
      +	var r = [];
      +	for(var i = 0, l = this._nodes.length; i < l; ++i)
      +		if(this._nodes[i].constructor === classObject)
      +			r.push(this._nodes[i]);
      +	return r;
      +}
      +
      +/**
      +* Returns a list of nodes that matches a type
      +* @method findNodesByType
      +* @param {String} type the name of the node type
      +* @return {Array} a list with all the nodes of this type
      +*/
      +
      +LGraph.prototype.findNodesByType = function(type)
      +{
      +	var type = type.toLowerCase();
      +	var r = [];
      +	for(var i = 0, l = this._nodes.length; i < l; ++i)
      +		if(this._nodes[i].type.toLowerCase() == type )
      +			r.push(this._nodes[i]);
      +	return r;
      +}
      +
      +/**
      +* Returns a list of nodes that matches a name
      +* @method findNodesByTitle
      +* @param {String} name the name of the node to search
      +* @return {Array} a list with all the nodes with this name
      +*/
      +
      +LGraph.prototype.findNodesByTitle = function(title)
      +{
      +	var result = [];
      +	for(var i = 0, l = this._nodes.length; i < l; ++i)
      +		if(this._nodes[i].title == title)
      +			result.push(this._nodes[i]);
      +	return result;
      +}
      +
      +/**
      +* Returns the top-most node in this position of the canvas
      +* @method getNodeOnPos
      +* @param {number} x the x coordinate in canvas space
      +* @param {number} y the y coordinate in canvas space
      +* @param {Array} nodes_list a list with all the nodes to search from, by default is all the nodes in the graph
      +* @return {LGraphNode} the node at this position or null
      +*/
      +LGraph.prototype.getNodeOnPos = function(x,y, nodes_list)
      +{
      +	nodes_list = nodes_list || this._nodes;
      +	for (var i = nodes_list.length - 1; i >= 0; i--)
      +	{
      +		var n = nodes_list[i];
      +		if(n.isPointInside( x, y, 2 ))
      +			return n;
      +	}
      +	return null;
      +}
      +
      +/**
      +* Returns the top-most group in that position
      +* @method getGroupOnPos
      +* @param {number} x the x coordinate in canvas space
      +* @param {number} y the y coordinate in canvas space
      +* @return {LGraphGroup} the group or null
      +*/
      +LGraph.prototype.getGroupOnPos = function(x,y)
      +{
      +	for (var i = this._groups.length - 1; i >= 0; i--)
      +	{
      +		var g = this._groups[i];
      +		if(g.isPointInside( x, y, 2 ))
      +			return g;
      +	}
      +	return null;
      +}
      +
      +// ********** GLOBALS *****************
      +
      +/**
      +* Tell this graph it has a global graph input of this type
      +* @method addGlobalInput
      +* @param {String} name
      +* @param {String} type
      +* @param {*} value [optional]
      +*/
      +LGraph.prototype.addGlobalInput = function(name, type, value)
      +{
      +	this.global_inputs[name] = { name: name, type: type, value: value };
      +	this._version++;
      +
      +	if(this.onGlobalInputAdded)
      +		this.onGlobalInputAdded(name, type);
      +
      +	if(this.onGlobalsChange)
      +		this.onGlobalsChange();
      +}
      +
      +/**
      +* Assign a data to the global graph input
      +* @method setGlobalInputData
      +* @param {String} name
      +* @param {*} data
      +*/
      +LGraph.prototype.setGlobalInputData = function(name, data)
      +{
      +	var input = this.global_inputs[name];
      +	if (!input)
      +		return;
      +	input.value = data;
      +}
      +
      +/**
      +* Assign a data to the global graph input (same as setGlobalInputData)
      +* @method setInputData
      +* @param {String} name
      +* @param {*} data
      +*/
      +LGraph.prototype.setInputData = LGraph.prototype.setGlobalInputData;
      +
      +
      +/**
      +* Returns the current value of a global graph input
      +* @method getGlobalInputData
      +* @param {String} name
      +* @return {*} the data
      +*/
      +LGraph.prototype.getGlobalInputData = function(name)
      +{
      +	var input = this.global_inputs[name];
      +	if (!input)
      +		return null;
      +	return input.value;
      +}
      +
      +/**
      +* Changes the name of a global graph input
      +* @method renameGlobalInput
      +* @param {String} old_name
      +* @param {String} new_name
      +*/
      +LGraph.prototype.renameGlobalInput = function(old_name, name)
      +{
      +	if(name == old_name)
      +		return;
      +
      +	if(!this.global_inputs[old_name])
      +		return false;
      +
      +	if(this.global_inputs[name])
      +	{
      +		console.error("there is already one input with that name");
      +		return false;
      +	}
      +
      +	this.global_inputs[name] = this.global_inputs[old_name];
      +	delete this.global_inputs[old_name];
      +	this._version++;
      +
      +	if(this.onGlobalInputRenamed)
      +		this.onGlobalInputRenamed(old_name, name);
      +
      +	if(this.onGlobalsChange)
      +		this.onGlobalsChange();
      +}
      +
      +/**
      +* Changes the type of a global graph input
      +* @method changeGlobalInputType
      +* @param {String} name
      +* @param {String} type
      +*/
      +LGraph.prototype.changeGlobalInputType = function(name, type)
      +{
      +	if(!this.global_inputs[name])
      +		return false;
      +
      +	if(this.global_inputs[name].type && this.global_inputs[name].type.toLowerCase() == type.toLowerCase() )
      +		return;
      +
      +	this.global_inputs[name].type = type;
      +	this._version++;
      +	if(this.onGlobalInputTypeChanged)
      +		this.onGlobalInputTypeChanged(name, type);
      +}
      +
      +/**
      +* Removes a global graph input
      +* @method removeGlobalInput
      +* @param {String} name
      +* @param {String} type
      +*/
      +LGraph.prototype.removeGlobalInput = function(name)
      +{
      +	if(!this.global_inputs[name])
      +		return false;
      +
      +	delete this.global_inputs[name];
      +	this._version++;
      +
      +	if(this.onGlobalInputRemoved)
      +		this.onGlobalInputRemoved(name);
      +
      +	if(this.onGlobalsChange)
      +		this.onGlobalsChange();
      +	return true;
      +}
      +
      +/**
      +* Creates a global graph output
      +* @method addGlobalOutput
      +* @param {String} name
      +* @param {String} type
      +* @param {*} value
      +*/
      +LGraph.prototype.addGlobalOutput = function(name, type, value)
      +{
      +	this.global_outputs[name] = { name: name, type: type, value: value };
      +	this._version++;
      +
      +	if(this.onGlobalOutputAdded)
      +		this.onGlobalOutputAdded(name, type);
      +
      +	if(this.onGlobalsChange)
      +		this.onGlobalsChange();
      +}
      +
      +/**
      +* Assign a data to the global output
      +* @method setGlobalOutputData
      +* @param {String} name
      +* @param {String} value
      +*/
      +LGraph.prototype.setGlobalOutputData = function(name, value)
      +{
      +	var output = this.global_outputs[ name ];
      +	if (!output)
      +		return;
      +	output.value = value;
      +}
      +
      +/**
      +* Returns the current value of a global graph output
      +* @method getGlobalOutputData
      +* @param {String} name
      +* @return {*} the data
      +*/
      +LGraph.prototype.getGlobalOutputData = function(name)
      +{
      +	var output = this.global_outputs[name];
      +	if (!output)
      +		return null;
      +	return output.value;
      +}
      +
      +/**
      +* Returns the current value of a global graph output (sames as getGlobalOutputData)
      +* @method getOutputData
      +* @param {String} name
      +* @return {*} the data
      +*/
      +LGraph.prototype.getOutputData = LGraph.prototype.getGlobalOutputData;
      +
      +
      +/**
      +* Renames a global graph output
      +* @method renameGlobalOutput
      +* @param {String} old_name
      +* @param {String} new_name
      +*/
      +LGraph.prototype.renameGlobalOutput = function(old_name, name)
      +{
      +	if(!this.global_outputs[old_name])
      +		return false;
      +
      +	if(this.global_outputs[name])
      +	{
      +		console.error("there is already one output with that name");
      +		return false;
      +	}
      +
      +	this.global_outputs[name] = this.global_outputs[old_name];
      +	delete this.global_outputs[old_name];
      +	this._version++;
      +
      +	if(this.onGlobalOutputRenamed)
      +		this.onGlobalOutputRenamed(old_name, name);
      +
      +	if(this.onGlobalsChange)
      +		this.onGlobalsChange();
      +}
      +
      +/**
      +* Changes the type of a global graph output
      +* @method changeGlobalOutputType
      +* @param {String} name
      +* @param {String} type
      +*/
      +LGraph.prototype.changeGlobalOutputType = function(name, type)
      +{
      +	if(!this.global_outputs[name])
      +		return false;
      +
      +	if(this.global_outputs[name].type && this.global_outputs[name].type.toLowerCase() == type.toLowerCase() )
      +		return;
      +
      +	this.global_outputs[name].type = type;
      +	this._version++;
      +	if(this.onGlobalOutputTypeChanged)
      +		this.onGlobalOutputTypeChanged(name, type);
      +}
      +
      +/**
      +* Removes a global graph output
      +* @method removeGlobalOutput
      +* @param {String} name
      +*/
      +LGraph.prototype.removeGlobalOutput = function(name)
      +{
      +	if(!this.global_outputs[name])
      +		return false;
      +	delete this.global_outputs[name];
      +	this._version++;
      +
      +	if(this.onGlobalOutputRemoved)
      +		this.onGlobalOutputRemoved(name);
      +
      +	if(this.onGlobalsChange)
      +		this.onGlobalsChange();
      +	return true;
      +}
      +
      +LGraph.prototype.triggerInput = function(name,value)
      +{
      +	var nodes = this.findNodesByTitle(name);
      +	for(var i = 0; i < nodes.length; ++i)
      +		nodes[i].onTrigger(value);
      +}
      +
      +LGraph.prototype.setCallback = function(name,func)
      +{
      +	var nodes = this.findNodesByTitle(name);
      +	for(var i = 0; i < nodes.length; ++i)
      +		nodes[i].setTrigger(func);
      +}
      +
      +
      +LGraph.prototype.connectionChange = function( node )
      +{
      +	this.updateExecutionOrder();
      +	if( this.onConnectionChange )
      +		this.onConnectionChange( node );
      +	this._version++;
      +	this.sendActionToCanvas("onConnectionChange");
      +}
      +
      +/**
      +* returns if the graph is in live mode
      +* @method isLive
      +*/
      +
      +LGraph.prototype.isLive = function()
      +{
      +	if(!this.list_of_graphcanvas)
      +		return false;
      +
      +	for(var i = 0; i < this.list_of_graphcanvas.length; ++i)
      +	{
      +		var c = this.list_of_graphcanvas[i];
      +		if(c.live_mode)
      +			return true;
      +	}
      +	return false;
      +}
      +
      +/* Called when something visually changed (not the graph!) */
      +LGraph.prototype.change = function()
      +{
      +	if(LiteGraph.debug)
      +		console.log("Graph changed");
      +	this.sendActionToCanvas("setDirty",[true,true]);
      +	if(this.on_change)
      +		this.on_change(this);
      +}
      +
      +LGraph.prototype.setDirtyCanvas = function(fg,bg)
      +{
      +	this.sendActionToCanvas("setDirty",[fg,bg]);
      +}
      +
      +//save and recover app state ***************************************
      +/**
      +* Creates a Object containing all the info about this graph, it can be serialized
      +* @method serialize
      +* @return {Object} value of the node
      +*/
      +LGraph.prototype.serialize = function()
      +{
      +	var nodes_info = [];
      +	for(var i = 0, l = this._nodes.length; i < l; ++i)
      +		nodes_info.push( this._nodes[i].serialize() );
      +
      +	//pack link info into a non-verbose format
      +	var links = [];
      +	for(var i in this.links) //links is an OBJECT
      +	{
      +		var link = this.links[i];
      +		links.push([ link.id, link.origin_id, link.origin_slot, link.target_id, link.target_slot, link.type ]);
      +	}
      +
      +	var groups_info = [];
      +	for(var i = 0; i < this._groups.length; ++i)
      +		groups_info.push( this._groups[i].serialize() );
      +
      +	var data = {
      +		last_node_id: this.last_node_id,
      +		last_link_id: this.last_link_id,
      +		nodes: nodes_info,
      +		links: links, 
      +		groups: groups_info,
      +		config: this.config
      +	};
      +
      +	return data;
      +}
      +
      +
      +/**
      +* Configure a graph from a JSON string
      +* @method configure
      +* @param {String} str configure a graph from a JSON string
      +* @param {Boolean} returns if there was any error parsing
      +*/
      +LGraph.prototype.configure = function( data, keep_old )
      +{
      +	if(!data)
      +		return;
      +
      +	if(!keep_old)
      +		this.clear();
      +
      +	var nodes = data.nodes;
      +
      +	//decode links info (they are very verbose)
      +	if(data.links && data.links.constructor === Array)
      +	{
      +		var links = [];
      +		for(var i = 0; i < data.links.length; ++i)
      +		{
      +			var link = data.links[i];
      +			links[ link[0] ] = { id: link[0], origin_id: link[1], origin_slot: link[2], target_id: link[3], target_slot: link[4], type: link[5] };
      +		}
      +		data.links = links;
      +	}
      +
      +	//copy all stored fields
      +	for (var i in data)
      +		this[i] = data[i];
      +
      +	var error = false;
      +
      +	//create nodes
      +	this._nodes = [];
      +	if(nodes)
      +	{
      +		for(var i = 0, l = nodes.length; i < l; ++i)
      +		{
      +			var n_info = nodes[i]; //stored info
      +			var node = LiteGraph.createNode( n_info.type, n_info.title );
      +			if(!node)
      +			{
      +				if(LiteGraph.debug)
      +					console.log("Node not found: " + n_info.type);
      +				error = true;
      +				continue;
      +			}
      +
      +			node.id = n_info.id; //id it or it will create a new id
      +			this.add(node, true); //add before configure, otherwise configure cannot create links
      +		}
      +
      +		//configure nodes afterwards so they can reach each other
      +		for(var i = 0, l = nodes.length; i < l; ++i)
      +		{
      +			var n_info = nodes[i];
      +			var node = this.getNodeById( n_info.id );
      +			if(node)
      +				node.configure( n_info );
      +		}
      +	}
      +
      +	//groups
      +	this._groups.length = 0;
      +	if( data.groups )
      +	for(var i = 0; i < data.groups.length; ++i )
      +	{
      +		var group = new LiteGraph.LGraphGroup();
      +		group.configure( data.groups[i] );
      +		this.add( group );
      +	}
      +
      +	this.updateExecutionOrder();
      +	this._version++;
      +	this.setDirtyCanvas(true,true);
      +	return error;
      +}
      +
      +LGraph.prototype.load = function(url)
      +{
      +	var that = this;
      +	var req = new XMLHttpRequest();
      +	req.open('GET', url, true);
      +	req.send(null);
      +	req.onload = function (oEvent) {
      +		if(req.status !== 200)
      +		{
      +			console.error("Error loading graph:",req.status,req.response);
      +			return;
      +		}
      +		var data = JSON.parse( req.response );
      +		that.configure(data);
      +	}
      +	req.onerror = function(err)
      +	{
      +		console.error("Error loading graph:",err);
      +	}
      +}
      +
      +LGraph.prototype.onNodeTrace = function(node, msg, color)
      +{
      +	//TODO
      +}
      +
      +// *************************************************************
      +//   Node CLASS                                          *******
      +// *************************************************************
      +
      +/*
      +	title: string
      +	pos: [x,y]
      +	size: [x,y]
      +
      +	input|output: every connection
      +		+  { name:string, type:string, pos: [x,y]=Optional, direction: "input"|"output", links: Array });
      +
      +	flags:
      +		+ clip_area: if you render outside the node, it will be cliped
      +		+ unsafe_execution: not allowed for safe execution
      +		+ skip_repeated_outputs: when adding new outputs, it wont show if there is one already connected
      +		+ resizable: if set to false it wont be resizable with the mouse
      +
      +	supported callbacks:
      +		+ onAdded: when added to graph
      +		+ onRemoved: when removed from graph
      +		+ onStart:	when the graph starts playing
      +		+ onStop:	when the graph stops playing
      +		+ onDrawForeground: render the inside widgets inside the node
      +		+ onDrawBackground: render the background area inside the node (only in edit mode)
      +		+ onMouseDown
      +		+ onMouseMove
      +		+ onMouseUp
      +		+ onMouseEnter
      +		+ onMouseLeave
      +		+ onExecute: execute the node
      +		+ onPropertyChanged: when a property is changed in the panel (return true to skip default behaviour)
      +		+ onGetInputs: returns an array of possible inputs
      +		+ onGetOutputs: returns an array of possible outputs
      +		+ onDblClick
      +		+ onSerialize
      +		+ onSelected
      +		+ onDeselected
      +		+ onDropItem : DOM item dropped over the node
      +		+ onDropFile : file dropped over the node
      +		+ onConnectInput : if returns false the incoming connection will be canceled
      +		+ onConnectionsChange : a connection changed (new one or removed) (LiteGraph.INPUT or LiteGraph.OUTPUT, slot, true if connected, link_info, input_info )
      +*/
      +
      +/**
      +* Base Class for all the node type classes
      +* @class LGraphNode
      +* @param {String} name a name for the node
      +*/
      +
      +function LGraphNode(title)
      +{
      +	this._ctor(title);
      +}
      +
      +global.LGraphNode = LiteGraph.LGraphNode = LGraphNode;
      +
      +LGraphNode.prototype._ctor = function( title )
      +{
      +	this.title = title || "Unnamed";
      +	this.size = [LiteGraph.NODE_WIDTH,60];
      +	this.graph = null;
      +
      +	this._pos = new Float32Array(10,10);
      +
      +	Object.defineProperty( this, "pos", {
      +		set: function(v)
      +		{
      +			if(!v || v.length < 2)
      +				return;
      +			this._pos[0] = v[0];
      +			this._pos[1] = v[1];
      +		},
      +		get: function()
      +		{
      +			return this._pos;
      +		},
      +		enumerable: true
      +	});
      +
      +	this.id = -1; //not know till not added
      +	this.type = null;
      +
      +	//inputs available: array of inputs
      +	this.inputs = [];
      +	this.outputs = [];
      +	this.connections = [];
      +
      +	//local data
      +	this.properties = {}; //for the values
      +	this.properties_info = []; //for the info
      +
      +	this.data = null; //persistent local data
      +	this.flags = {};
      +}
      +
      +/**
      +* configure a node from an object containing the serialized info
      +* @method configure
      +*/
      +LGraphNode.prototype.configure = function(info)
      +{
      +	if(this.graph)
      +		this.graph._version++;
      +
      +	for (var j in info)
      +	{
      +		if(j == "console")
      +			continue;
      +
      +		if(j == "properties")
      +		{
      +			//i dont want to clone properties, I want to reuse the old container
      +			for(var k in info.properties)
      +			{
      +				this.properties[k] = info.properties[k];
      +				if(this.onPropertyChanged)
      +					this.onPropertyChanged(k,info.properties[k]);
      +			}
      +			continue;
      +		}
      +
      +		if(info[j] == null)
      +			continue;
      +
      +		else if (typeof(info[j]) == 'object') //object
      +		{
      +			if(this[j] && this[j].configure)
      +				this[j].configure( info[j] );
      +			else
      +				this[j] = LiteGraph.cloneObject(info[j], this[j]);
      +		}
      +		else //value
      +			this[j] = info[j];
      +	}
      +
      +	if(!info.title)
      +		this.title = this.constructor.title;
      +
      +	if(this.onConnectionsChange)
      +	{
      +		if(this.inputs)
      +		for(var i = 0; i < this.inputs.length; ++i)
      +		{
      +			var input = this.inputs[i];
      +			var link_info = this.graph ? this.graph.links[ input.link ] : null;
      +			this.onConnectionsChange( LiteGraph.INPUT, i, true, link_info, input ); //link_info has been created now, so its updated
      +		}
      +
      +		if(this.outputs)
      +		for(var i = 0; i < this.outputs.length; ++i)
      +		{
      +			var output = this.outputs[i];
      +			if(!output.links)
      +				continue;
      +			for(var j = 0; j < output.links.length; ++j)
      +			{
      +				var link_info = this.graph ? this.graph.links[ output.links[j] ] : null;
      +				this.onConnectionsChange( LiteGraph.OUTPUT, i, true, link_info, output ); //link_info has been created now, so its updated
      +			}
      +		}
      +	}
      +
      +	//FOR LEGACY, PLEASE REMOVE ON NEXT VERSION
      +	for(var i in this.inputs)
      +	{
      +		var input = this.inputs[i];
      +		if(!input.link || !input.link.length )
      +			continue;
      +		var link = input.link;
      +		if(typeof(link) != "object")
      +			continue;
      +		input.link = link[0];
      +		if(this.graph)
      +		this.graph.links[ link[0] ] = {
      +			id: link[0],
      +			origin_id: link[1],
      +			origin_slot: link[2],
      +			target_id: link[3],
      +			target_slot: link[4]
      +		};
      +	}
      +	for(var i in this.outputs)
      +	{
      +		var output = this.outputs[i];
      +		if(!output.links || output.links.length == 0)
      +			continue;
      +		for(var j in output.links)
      +		{
      +			var link = output.links[j];
      +			if(typeof(link) != "object")
      +				continue;
      +			output.links[j] = link[0];
      +		}
      +	}
      +
      +	if( this.onConfigure )
      +		this.onConfigure( info );
      +}
      +
      +/**
      +* serialize the content
      +* @method serialize
      +*/
      +
      +LGraphNode.prototype.serialize = function()
      +{
      +	//create serialization object
      +	var o = {
      +		id: this.id,
      +		type: this.type,
      +		pos: this.pos,
      +		size: this.size,
      +		data: this.data,
      +		flags: LiteGraph.cloneObject(this.flags),
      +		mode: this.mode
      +	};
      +
      +	if( this.inputs )
      +		o.inputs = this.inputs;
      +
      +	if( this.outputs )
      +	{
      +		//clear outputs last data (because data in connections is never serialized but stored inside the outputs info)
      +		for(var i = 0; i < this.outputs.length; i++)
      +			delete this.outputs[i]._data;
      +		o.outputs = this.outputs;
      +	}
      +
      +	if( this.title && this.title != this.constructor.title )
      +		o.title = this.title;
      +
      +	if( this.properties )
      +		o.properties = LiteGraph.cloneObject( this.properties );
      +
      +	if( !o.type )
      +		o.type = this.constructor.type;
      +
      +	if( this.color )
      +		o.color = this.color;
      +	if( this.bgcolor )
      +		o.bgcolor = this.bgcolor;
      +	if( this.boxcolor )
      +		o.boxcolor = this.boxcolor;
      +	if( this.shape )
      +		o.shape = this.shape;
      +
      +	if(this.onSerialize)
      +		this.onSerialize(o);
      +
      +	return o;
      +}
      +
      +
      +/* Creates a clone of this node */
      +LGraphNode.prototype.clone = function()
      +{
      +	var node = LiteGraph.createNode(this.type);
      +
      +	//we clone it because serialize returns shared containers
      +	var data = LiteGraph.cloneObject( this.serialize() );
      +
      +	//remove links
      +	if(data.inputs)
      +		for(var i = 0; i < data.inputs.length; ++i)
      +			data.inputs[i].link = null;
      +
      +	if(data.outputs)
      +		for(var i = 0; i < data.outputs.length; ++i)
      +		{
      +			if(data.outputs[i].links)
      +				data.outputs[i].links.length = 0;
      +		}
      +
      +	delete data["id"];
      +	//remove links
      +	node.configure(data);
      +
      +	return node;
      +}
      +
      +
      +/**
      +* serialize and stringify
      +* @method toString
      +*/
      +
      +LGraphNode.prototype.toString = function()
      +{
      +	return JSON.stringify( this.serialize() );
      +}
      +//LGraphNode.prototype.unserialize = function(info) {} //this cannot be done from within, must be done in LiteGraph
      +
      +
      +/**
      +* get the title string
      +* @method getTitle
      +*/
      +
      +LGraphNode.prototype.getTitle = function()
      +{
      +	return this.title || this.constructor.title;
      +}
      +
      +
      +
      +// Execution *************************
      +/**
      +* sets the output data
      +* @method setOutputData
      +* @param {number} slot
      +* @param {*} data
      +*/
      +LGraphNode.prototype.setOutputData = function(slot, data)
      +{
      +	if(!this.outputs)
      +		return;
      +
      +	//this maybe slow and a niche case
      +	//if(slot && slot.constructor === String)
      +	//	slot = this.findOutputSlot(slot);
      +
      +	if(slot == -1 || slot >= this.outputs.length)
      +		return;
      +
      +	var output_info = this.outputs[slot];
      +	if(!output_info)
      +		return;
      +
      +	//store data in the output itself in case we want to debug
      +	output_info._data = data;
      +
      +	//if there are connections, pass the data to the connections
      +	if( this.outputs[slot].links )
      +	{
      +		for(var i = 0; i < this.outputs[slot].links.length; i++)
      +		{
      +			var link_id = this.outputs[slot].links[i];
      +			this.graph.links[ link_id ].data = data;
      +		}
      +	}
      +}
      +
      +/**
      +* Retrieves the input data (data traveling through the connection) from one slot
      +* @method getInputData
      +* @param {number} slot
      +* @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link
      +* @return {*} data or if it is not connected returns undefined
      +*/
      +LGraphNode.prototype.getInputData = function( slot, force_update )
      +{
      +	if(!this.inputs)
      +		return; //undefined;
      +
      +	if(slot >= this.inputs.length || this.inputs[slot].link == null)
      +		return;
      +
      +	var link_id = this.inputs[slot].link;
      +	var link = this.graph.links[ link_id ];
      +	if(!link) //bug: weird case but it happens sometimes
      +		return null;
      +
      +	if(!force_update)
      +		return link.data;
      +
      +	//special case: used to extract data from the incomming connection before the graph has been executed
      +	var node = this.graph.getNodeById( link.origin_id );
      +	if(!node)
      +		return link.data;
      +
      +	if(node.updateOutputData)
      +		node.updateOutputData( link.origin_slot );
      +	else if(node.onExecute)
      +		node.onExecute();
      +
      +	return link.data;
      +}
      +
      +/**
      +* Retrieves the input data from one slot using its name instead of slot number
      +* @method getInputDataByName
      +* @param {String} slot_name
      +* @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link
      +* @return {*} data or if it is not connected returns null
      +*/
      +LGraphNode.prototype.getInputDataByName = function( slot_name, force_update )
      +{
      +	var slot = this.findInputSlot( slot_name );
      +	if( slot == -1 )
      +		return null;
      +	return this.getInputData( slot, force_update );
      +}
      +
      +
      +/**
      +* tells you if there is a connection in one input slot
      +* @method isInputConnected
      +* @param {number} slot
      +* @return {boolean}
      +*/
      +LGraphNode.prototype.isInputConnected = function(slot)
      +{
      +	if(!this.inputs)
      +		return false;
      +	return (slot < this.inputs.length && this.inputs[slot].link != null);
      +}
      +
      +/**
      +* tells you info about an input connection (which node, type, etc)
      +* @method getInputInfo
      +* @param {number} slot
      +* @return {Object} object or null { link: id, name: string, type: string or 0 }
      +*/
      +LGraphNode.prototype.getInputInfo = function(slot)
      +{
      +	if(!this.inputs)
      +		return null;
      +	if(slot < this.inputs.length)
      +		return this.inputs[slot];
      +	return null;
      +}
      +
      +/**
      +* returns the node connected in the input slot
      +* @method getInputNode
      +* @param {number} slot
      +* @return {LGraphNode} node or null
      +*/
      +LGraphNode.prototype.getInputNode = function( slot )
      +{
      +	if(!this.inputs)
      +		return null;
      +	if(slot >= this.inputs.length)
      +		return null;
      +	var input = this.inputs[slot];
      +	if(!input || input.link === null)
      +		return null;
      +	var link_info = this.graph.links[ input.link ];
      +	if(!link_info)
      +		return null;
      +	return this.graph.getNodeById( link_info.origin_id );
      +}
      +
      +
      +/**
      +* returns the value of an input with this name, otherwise checks if there is a property with that name
      +* @method getInputOrProperty
      +* @param {string} name
      +* @return {*} value
      +*/
      +LGraphNode.prototype.getInputOrProperty = function( name )
      +{
      +	if(!this.inputs || !this.inputs.length)
      +		return this.properties ? this.properties[name] : null;
      +
      +	for(var i = 0, l = this.inputs.length; i < l; ++i)
      +		if(name == this.inputs[i].name)
      +		{
      +			var link_id = this.inputs[i].link;
      +			var link = this.graph.links[ link_id ];
      +			return link ? link.data : null;
      +		}
      +	return this.properties[name];
      +}
      +
      +
      +
      +
      +/**
      +* tells you the last output data that went in that slot
      +* @method getOutputData
      +* @param {number} slot
      +* @return {Object}  object or null
      +*/
      +LGraphNode.prototype.getOutputData = function(slot)
      +{
      +	if(!this.outputs)
      +		return null;
      +	if(slot >= this.outputs.length)
      +		return null;
      +
      +	var info = this.outputs[slot];
      +	return info._data;
      +}
      +
      +
      +/**
      +* tells you info about an output connection (which node, type, etc)
      +* @method getOutputInfo
      +* @param {number} slot
      +* @return {Object}  object or null { name: string, type: string, links: [ ids of links in number ] }
      +*/
      +LGraphNode.prototype.getOutputInfo = function(slot)
      +{
      +	if(!this.outputs)
      +		return null;
      +	if(slot < this.outputs.length)
      +		return this.outputs[slot];
      +	return null;
      +}
      +
      +
      +/**
      +* tells you if there is a connection in one output slot
      +* @method isOutputConnected
      +* @param {number} slot
      +* @return {boolean}
      +*/
      +LGraphNode.prototype.isOutputConnected = function(slot)
      +{
      +	if(!this.outputs)
      +		return false;
      +	return (slot < this.outputs.length && this.outputs[slot].links && this.outputs[slot].links.length);
      +}
      +
      +/**
      +* tells you if there is any connection in the output slots
      +* @method isAnyOutputConnected
      +* @return {boolean}
      +*/
      +LGraphNode.prototype.isAnyOutputConnected = function()
      +{
      +	if(!this.outputs)
      +		return false;
      +	for(var i = 0; i < this.outputs.length; ++i)
      +		if( this.outputs[i].links && this.outputs[i].links.length )
      +			return true;
      +	return false;
      +}
      +
      +
      +/**
      +* retrieves all the nodes connected to this output slot
      +* @method getOutputNodes
      +* @param {number} slot
      +* @return {array}
      +*/
      +LGraphNode.prototype.getOutputNodes = function(slot)
      +{
      +	if(!this.outputs || this.outputs.length == 0)
      +		return null;
      +
      +	if(slot >= this.outputs.length)
      +		return null;
      +
      +	var output = this.outputs[slot];
      +	if(!output.links || output.links.length == 0)
      +		return null;
      +
      +	var r = [];
      +	for(var i = 0; i < output.links.length; i++)
      +	{
      +		var link_id = output.links[i];
      +		var link = this.graph.links[ link_id ];
      +		if(link)
      +		{
      +			var target_node = this.graph.getNodeById( link.target_id );
      +			if( target_node )
      +				r.push( target_node );
      +		}
      +	}
      +	return r;
      +}
      +
      +/**
      +* Triggers an event in this node, this will trigger any output with the same name
      +* @method trigger
      +* @param {String} event name ( "on_play", ... ) if action is equivalent to false then the event is send to all
      +* @param {*} param
      +*/
      +LGraphNode.prototype.trigger = function( action, param )
      +{
      +	if( !this.outputs || !this.outputs.length )
      +		return;
      +
      +	if(this.graph)
      +		this.graph._last_trigger_time = LiteGraph.getTime();
      +
      +	for(var i = 0; i < this.outputs.length; ++i)
      +	{
      +		var output = this.outputs[ i ];
      +		if(!output || output.type !== LiteGraph.EVENT || (action && output.name != action) )
      +			continue;
      +		this.triggerSlot( i, param );
      +	}
      +}
      +
      +/**
      +* Triggers an slot event in this node
      +* @method triggerSlot
      +* @param {Number} slot the index of the output slot
      +* @param {*} param
      +*/
      +LGraphNode.prototype.triggerSlot = function( slot, param )
      +{
      +	if( !this.outputs )
      +		return;
      +
      +	var output = this.outputs[ slot ];
      +	if( !output )
      +		return;
      +
      +	var links = output.links;
      +	if(!links || !links.length)
      +		return;
      +
      +	if(this.graph)
      +		this.graph._last_trigger_time = LiteGraph.getTime();
      +
      +	//for every link attached here
      +	for(var k = 0; k < links.length; ++k)
      +	{
      +		var link_info = this.graph.links[ links[k] ];
      +		if(!link_info) //not connected
      +			continue;
      +		var node = this.graph.getNodeById( link_info.target_id );
      +		if(!node) //node not found?
      +			continue;
      +
      +		//used to mark events in graph
      +		link_info._last_time = LiteGraph.getTime();
      +
      +		var target_connection = node.inputs[ link_info.target_slot ];
      +
      +		if(node.onAction)
      +			node.onAction( target_connection.name, param );
      +		else if(node.mode === LiteGraph.ON_TRIGGER)
      +		{
      +			if(node.onExecute)
      +				node.onExecute(param);
      +		}
      +	}
      +}
      +
      +/**
      +* add a new property to this node
      +* @method addProperty
      +* @param {string} name
      +* @param {*} default_value
      +* @param {string} type string defining the output type ("vec3","number",...)
      +* @param {Object} extra_info this can be used to have special properties of the property (like values, etc)
      +*/
      +LGraphNode.prototype.addProperty = function( name, default_value, type, extra_info )
      +{
      +	var o = { name: name, type: type, default_value: default_value };
      +	if(extra_info)
      +		for(var i in extra_info)
      +			o[i] = extra_info[i];
      +	if(!this.properties_info)
      +		this.properties_info = [];
      +	this.properties_info.push(o);
      +	if(!this.properties)
      +		this.properties = {};
      +	this.properties[ name ] = default_value;
      +	return o;
      +}
      +
      +
      +//connections
      +
      +/**
      +* add a new output slot to use in this node
      +* @method addOutput
      +* @param {string} name
      +* @param {string} type string defining the output type ("vec3","number",...)
      +* @param {Object} extra_info this can be used to have special properties of an output (label, special color, position, etc)
      +*/
      +LGraphNode.prototype.addOutput = function(name,type,extra_info)
      +{
      +	var o = { name: name, type: type, links: null };
      +	if(extra_info)
      +		for(var i in extra_info)
      +			o[i] = extra_info[i];
      +
      +	if(!this.outputs)
      +		this.outputs = [];
      +	this.outputs.push(o);
      +	if(this.onOutputAdded)
      +		this.onOutputAdded(o);
      +	this.size = this.computeSize();
      +	return o;
      +}
      +
      +/**
      +* add a new output slot to use in this node
      +* @method addOutputs
      +* @param {Array} array of triplets like [[name,type,extra_info],[...]]
      +*/
      +LGraphNode.prototype.addOutputs = function(array)
      +{
      +	for(var i = 0; i < array.length; ++i)
      +	{
      +		var info = array[i];
      +		var o = {name:info[0],type:info[1],link:null};
      +		if(array[2])
      +			for(var j in info[2])
      +				o[j] = info[2][j];
      +
      +		if(!this.outputs)
      +			this.outputs = [];
      +		this.outputs.push(o);
      +		if(this.onOutputAdded)
      +			this.onOutputAdded(o);
      +	}
      +
      +	this.size = this.computeSize();
      +}
      +
      +/**
      +* remove an existing output slot
      +* @method removeOutput
      +* @param {number} slot
      +*/
      +LGraphNode.prototype.removeOutput = function(slot)
      +{
      +	this.disconnectOutput(slot);
      +	this.outputs.splice(slot,1);
      +	this.size = this.computeSize();
      +	if(this.onOutputRemoved)
      +		this.onOutputRemoved(slot);
      +}
      +
      +/**
      +* add a new input slot to use in this node
      +* @method addInput
      +* @param {string} name
      +* @param {string} type string defining the input type ("vec3","number",...), it its a generic one use 0
      +* @param {Object} extra_info this can be used to have special properties of an input (label, color, position, etc)
      +*/
      +LGraphNode.prototype.addInput = function(name,type,extra_info)
      +{
      +	type = type || 0;
      +	var o = {name:name,type:type,link:null};
      +	if(extra_info)
      +		for(var i in extra_info)
      +			o[i] = extra_info[i];
      +
      +	if(!this.inputs)
      +		this.inputs = [];
      +	this.inputs.push(o);
      +	this.size = this.computeSize();
      +	if(this.onInputAdded)
      +		this.onInputAdded(o);
      +	return o;
      +}
      +
      +/**
      +* add several new input slots in this node
      +* @method addInputs
      +* @param {Array} array of triplets like [[name,type,extra_info],[...]]
      +*/
      +LGraphNode.prototype.addInputs = function(array)
      +{
      +	for(var i = 0; i < array.length; ++i)
      +	{
      +		var info = array[i];
      +		var o = {name:info[0], type:info[1], link:null};
      +		if(array[2])
      +			for(var j in info[2])
      +				o[j] = info[2][j];
      +
      +		if(!this.inputs)
      +			this.inputs = [];
      +		this.inputs.push(o);
      +		if(this.onInputAdded)
      +			this.onInputAdded(o);
      +	}
      +
      +	this.size = this.computeSize();
      +}
      +
      +/**
      +* remove an existing input slot
      +* @method removeInput
      +* @param {number} slot
      +*/
      +LGraphNode.prototype.removeInput = function(slot)
      +{
      +	this.disconnectInput(slot);
      +	this.inputs.splice(slot,1);
      +	this.size = this.computeSize();
      +	if(this.onInputRemoved)
      +		this.onInputRemoved(slot);
      +}
      +
      +/**
      +* add an special connection to this node (used for special kinds of graphs)
      +* @method addConnection
      +* @param {string} name
      +* @param {string} type string defining the input type ("vec3","number",...)
      +* @param {[x,y]} pos position of the connection inside the node
      +* @param {string} direction if is input or output
      +*/
      +LGraphNode.prototype.addConnection = function(name,type,pos,direction)
      +{
      +	var o = {
      +		name: name,
      +		type: type,
      +		pos: pos,
      +		direction: direction,
      +		links: null
      +	};
      +	this.connections.push( o );
      +	return o;
      +}
      +
      +/**
      +* computes the size of a node according to its inputs and output slots
      +* @method computeSize
      +* @param {number} minHeight
      +* @return {number} the total size
      +*/
      +LGraphNode.prototype.computeSize = function( minHeight, out )
      +{
      +	var rows = Math.max( this.inputs ? this.inputs.length : 1, this.outputs ? this.outputs.length : 1);
      +	var size = out || new Float32Array([0,0]);
      +	rows = Math.max(rows, 1);
      +	var font_size = LiteGraph.NODE_TEXT_SIZE; //although it should be graphcanvas.inner_text_font size
      +	size[1] = (this.constructor.slot_start_y || 0) + rows * (font_size + 1) + ( this.widgets ? this.widgets.length : 0 ) * (LiteGraph.NODE_WIDGET_HEIGHT + 4 ) + 4;
      +
      +	var font_size = font_size;
      +	var title_width = compute_text_size( this.title );
      +	var input_width = 0;
      +	var output_width = 0;
      +
      +	if(this.inputs)
      +		for(var i = 0, l = this.inputs.length; i < l; ++i)
      +		{
      +			var input = this.inputs[i];
      +			var text = input.label || input.name || "";
      +			var text_width = compute_text_size( text );
      +			if(input_width < text_width)
      +				input_width = text_width;
      +		}
      +
      +	if(this.outputs)
      +		for(var i = 0, l = this.outputs.length; i < l; ++i)
      +		{
      +			var output = this.outputs[i];
      +			var text = output.label || output.name || "";
      +			var text_width = compute_text_size( text );
      +			if(output_width < text_width)
      +				output_width = text_width;
      +		}
      +
      +	size[0] = Math.max( input_width + output_width + 10, title_width );
      +	size[0] = Math.max( size[0], LiteGraph.NODE_WIDTH );
      +
      +	if(this.onResize)
      +		this.onResize(size);
      +
      +	function compute_text_size( text )
      +	{
      +		if(!text)
      +			return 0;
      +		return font_size * text.length * 0.6;
      +	}
      +
      +	return size;
      +}
      +
      +/**
      +* Allows to pass 
      +* 
      +* @method addWidget
      +* @return {Float32Array[4]} the total size
      +*/
      +LGraphNode.prototype.addWidget = function( type, name, value, callback, options )
      +{
      +	if(!this.widgets)
      +		this.widgets = [];
      +	var w = {
      +		type: type.toLowerCase(),
      +		name: name,
      +		value: value,
      +		callback: callback,
      +		options: options || {}
      +	};
      +
      +	if(w.options.y !== undefined )
      +		w.y = w.options.y;
      +
      +	if( type == "combo" && !w.options.values )
      +		throw("LiteGraph addWidget('combo',...) requires to pass values in options: { values:['red','blue'] }");
      +	this.widgets.push(w);
      +	return w;
      +}
      +
      +
      +/**
      +* returns the bounding of the object, used for rendering purposes
      +* bounding is: [topleft_cornerx, topleft_cornery, width, height]
      +* @method getBounding
      +* @return {Float32Array[4]} the total size
      +*/
      +LGraphNode.prototype.getBounding = function( out )
      +{
      +	out = out || new Float32Array(4);
      +	out[0] = this.pos[0] - 4;
      +	out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT;
      +	out[2] = this.size[0] + 4;
      +	out[3] = this.size[1] + LiteGraph.NODE_TITLE_HEIGHT;
      +	return out;
      +}
      +
      +/**
      +* checks if a point is inside the shape of a node
      +* @method isPointInside
      +* @param {number} x
      +* @param {number} y
      +* @return {boolean}
      +*/
      +LGraphNode.prototype.isPointInside = function(x,y, margin)
      +{
      +	margin = margin || 0;
      +
      +	var margin_top = this.graph && this.graph.isLive() ? 0 : 20;
      +	if(this.flags && this.flags.collapsed)
      +	{
      +		//if ( distance([x,y], [this.pos[0] + this.size[0]*0.5, this.pos[1] + this.size[1]*0.5]) < LiteGraph.NODE_COLLAPSED_RADIUS)
      +		if( isInsideRectangle( x, y, this.pos[0] - margin, this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT - margin, (this._collapsed_width||LiteGraph.NODE_COLLAPSED_WIDTH) + 2 * margin, LiteGraph.NODE_TITLE_HEIGHT + 2 * margin ) )
      +			return true;
      +	}
      +	else if ( (this.pos[0] - 4 - margin) < x && (this.pos[0] + this.size[0] + 4 + margin) > x
      +		&& (this.pos[1] - margin_top - margin) < y && (this.pos[1] + this.size[1] + margin) > y)
      +		return true;
      +	return false;
      +}
      +
      +/**
      +* checks if a point is inside a node slot, and returns info about which slot
      +* @method getSlotInPosition
      +* @param {number} x
      +* @param {number} y
      +* @return {Object} if found the object contains { input|output: slot object, slot: number, link_pos: [x,y] }
      +*/
      +LGraphNode.prototype.getSlotInPosition = function( x, y )
      +{
      +	//search for inputs
      +	if(this.inputs)
      +		for(var i = 0, l = this.inputs.length; i < l; ++i)
      +		{
      +			var input = this.inputs[i];
      +			var link_pos = this.getConnectionPos( true,i );
      +			if( isInsideRectangle(x, y, link_pos[0] - 10, link_pos[1] - 5, 20,10) )
      +				return { input: input, slot: i, link_pos: link_pos, locked: input.locked };
      +		}
      +
      +	if(this.outputs)
      +		for(var i = 0, l = this.outputs.length; i < l; ++i)
      +		{
      +			var output = this.outputs[i];
      +			var link_pos = this.getConnectionPos(false,i);
      +			if( isInsideRectangle(x, y, link_pos[0] - 10, link_pos[1] - 5, 20,10) )
      +				return { output: output, slot: i, link_pos: link_pos, locked: output.locked };
      +		}
      +
      +	return null;
      +}
      +
      +/**
      +* returns the input slot with a given name (used for dynamic slots), -1 if not found
      +* @method findInputSlot
      +* @param {string} name the name of the slot
      +* @return {number} the slot (-1 if not found)
      +*/
      +LGraphNode.prototype.findInputSlot = function(name)
      +{
      +	if(!this.inputs)
      +		return -1;
      +	for(var i = 0, l = this.inputs.length; i < l; ++i)
      +		if(name == this.inputs[i].name)
      +			return i;
      +	return -1;
      +}
      +
      +/**
      +* returns the output slot with a given name (used for dynamic slots), -1 if not found
      +* @method findOutputSlot
      +* @param {string} name the name of the slot
      +* @return {number} the slot (-1 if not found)
      +*/
      +LGraphNode.prototype.findOutputSlot = function(name)
      +{
      +	if(!this.outputs) return -1;
      +	for(var i = 0, l = this.outputs.length; i < l; ++i)
      +		if(name == this.outputs[i].name)
      +			return i;
      +	return -1;
      +}
      +
      +/**
      +* connect this node output to the input of another node
      +* @method connect
      +* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
      +* @param {LGraphNode} node the target node
      +* @param {number_or_string} target_slot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger)
      +* @return {boolean} if it was connected succesfully
      +*/
      +LGraphNode.prototype.connect = function( slot, target_node, target_slot )
      +{
      +	target_slot = target_slot || 0;
      +
      +	if(!this.graph) //could be connected before adding it to a graph
      +	{
      +		console.log("Connect: Error, node doesnt belong to any graph. Nodes must be added first to a graph before connecting them."); //due to link ids being associated with graphs
      +		return false;
      +	}
      +
      +
      +	//seek for the output slot
      +	if( slot.constructor === String )
      +	{
      +		slot = this.findOutputSlot(slot);
      +		if(slot == -1)
      +		{
      +			if(LiteGraph.debug)
      +				console.log("Connect: Error, no slot of name " + slot);
      +			return false;
      +		}
      +	}
      +	else if(!this.outputs || slot >= this.outputs.length)
      +	{
      +		if(LiteGraph.debug)
      +			console.log("Connect: Error, slot number not found");
      +		return false;
      +	}
      +
      +	if(target_node && target_node.constructor === Number)
      +		target_node = this.graph.getNodeById( target_node );
      +	if(!target_node)
      +		throw("target node is null");
      +
      +	//avoid loopback
      +	if(target_node == this)
      +		return false;
      +
      +	//you can specify the slot by name
      +	if(target_slot.constructor === String)
      +	{
      +		target_slot = target_node.findInputSlot( target_slot );
      +		if(target_slot == -1)
      +		{
      +			if(LiteGraph.debug)
      +				console.log("Connect: Error, no slot of name " + target_slot);
      +			return false;
      +		}
      +	}
      +	else if( target_slot === LiteGraph.EVENT )
      +	{
      +		//search for first slot with event?
      +		/*
      +		//create input for trigger
      +		var input = target_node.addInput("onTrigger", LiteGraph.EVENT );
      +		target_slot = target_node.inputs.length - 1; //last one is the one created
      +		target_node.mode = LiteGraph.ON_TRIGGER;
      +		*/
      +		return false;
      +	}
      +	else if( !target_node.inputs || target_slot >= target_node.inputs.length )
      +	{
      +		if(LiteGraph.debug)
      +			console.log("Connect: Error, slot number not found");
      +		return false;
      +	}
      +
      +	//if there is something already plugged there, disconnect
      +	if(target_node.inputs[ target_slot ].link != null )
      +		target_node.disconnectInput( target_slot );
      +
      +	//why here??
      +	//this.setDirtyCanvas(false,true);
      +	//this.graph.connectionChange( this );
      +
      +	var output = this.outputs[slot];
      +
      +	//allows nodes to block connection
      +	if(target_node.onConnectInput)
      +		if( target_node.onConnectInput( target_slot, output.type, output ) === false)
      +			return false;
      +
      +	var input = target_node.inputs[target_slot];
      +
      +	if( LiteGraph.isValidConnection( output.type, input.type ) )
      +	{
      +		var link_info = {
      +			id: this.graph.last_link_id++,
      +			type: input.type,
      +			origin_id: this.id,
      +			origin_slot: slot,
      +			target_id: target_node.id,
      +			target_slot: target_slot
      +		};
      +
      +		//add to graph links list
      +		this.graph.links[ link_info.id ] = link_info;
      +
      +		//connect in output
      +		if( output.links == null )
      +			output.links = [];
      +		output.links.push( link_info.id );
      +		//connect in input
      +		target_node.inputs[target_slot].link = link_info.id;
      +		if(this.graph)
      +			this.graph._version++;
      +		if(this.onConnectionsChange)
      +			this.onConnectionsChange( LiteGraph.OUTPUT, slot, true, link_info, output ); //link_info has been created now, so its updated
      +		if(target_node.onConnectionsChange)
      +			target_node.onConnectionsChange( LiteGraph.INPUT, target_slot, true, link_info, input );
      +		if( this.graph && this.graph.onNodeConnectionChange )
      +			this.graph.onNodeConnectionChange( LiteGraph.OUTPUT, this, slot, target_node, target_slot );
      +	}
      +
      +	this.setDirtyCanvas(false,true);
      +	this.graph.connectionChange( this );
      +
      +	return true;
      +}
      +
      +/**
      +* disconnect one output to an specific node
      +* @method disconnectOutput
      +* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
      +* @param {LGraphNode} target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected]
      +* @return {boolean} if it was disconnected succesfully
      +*/
      +LGraphNode.prototype.disconnectOutput = function( slot, target_node )
      +{
      +	if( slot.constructor === String )
      +	{
      +		slot = this.findOutputSlot(slot);
      +		if(slot == -1)
      +		{
      +			if(LiteGraph.debug)
      +				console.log("Connect: Error, no slot of name " + slot);
      +			return false;
      +		}
      +	}
      +	else if(!this.outputs || slot >= this.outputs.length)
      +	{
      +		if(LiteGraph.debug)
      +			console.log("Connect: Error, slot number not found");
      +		return false;
      +	}
      +
      +	//get output slot
      +	var output = this.outputs[slot];
      +	if(!output.links || output.links.length == 0)
      +		return false;
      +
      +	//one of the output links in this slot
      +	if(target_node)
      +	{
      +		if(target_node.constructor === Number)
      +			target_node = this.graph.getNodeById( target_node );
      +		if(!target_node)
      +			throw("Target Node not found");
      +
      +		for(var i = 0, l = output.links.length; i < l; i++)
      +		{
      +			var link_id = output.links[i];
      +			var link_info = this.graph.links[ link_id ];
      +
      +			//is the link we are searching for...
      +			if( link_info.target_id == target_node.id )
      +			{
      +				output.links.splice(i,1); //remove here
      +				var input = target_node.inputs[ link_info.target_slot ];
      +				input.link = null; //remove there
      +				delete this.graph.links[ link_id ]; //remove the link from the links pool
      +				if(this.graph)
      +					this.graph._version++;
      +				if(target_node.onConnectionsChange)
      +					target_node.onConnectionsChange( LiteGraph.INPUT, link_info.target_slot, false, link_info, input ); //link_info hasnt been modified so its ok
      +				if(this.onConnectionsChange)
      +					this.onConnectionsChange( LiteGraph.OUTPUT, slot, false, link_info, output );
      +				if( this.graph && this.graph.onNodeConnectionChange )
      +					this.graph.onNodeConnectionChange( LiteGraph.OUTPUT, this, slot );
      +				if( this.graph && this.graph.onNodeConnectionChange )
      +					this.graph.onNodeConnectionChange( LiteGraph.INPUT, target_node, link_info.target_slot );
      +				break;
      +			}
      +		}
      +	}
      +	else //all the links in this output slot
      +	{
      +		for(var i = 0, l = output.links.length; i < l; i++)
      +		{
      +			var link_id = output.links[i];
      +			var link_info = this.graph.links[ link_id ];
      +			if(!link_info) //bug: it happens sometimes
      +				continue;
      +
      +			var target_node = this.graph.getNodeById( link_info.target_id );
      +			var input = null;
      +			if(this.graph)
      +				this.graph._version++;
      +			if(target_node)
      +			{
      +				input = target_node.inputs[ link_info.target_slot ];
      +				input.link = null; //remove other side link
      +				if(target_node.onConnectionsChange)
      +					target_node.onConnectionsChange( LiteGraph.INPUT, link_info.target_slot, false, link_info, input ); //link_info hasnt been modified so its ok
      +				if( this.graph && this.graph.onNodeConnectionChange )
      +					this.graph.onNodeConnectionChange( LiteGraph.INPUT, target_node, link_info.target_slot );
      +			}
      +			delete this.graph.links[ link_id ]; //remove the link from the links pool
      +			if(this.onConnectionsChange)
      +				this.onConnectionsChange( LiteGraph.OUTPUT, slot, false, link_info, output );
      +			if( this.graph && this.graph.onNodeConnectionChange )
      +				this.graph.onNodeConnectionChange( LiteGraph.OUTPUT, this, slot );
      +		}
      +		output.links = null;
      +	}
      +
      +
      +	this.setDirtyCanvas(false,true);
      +	this.graph.connectionChange( this );
      +	return true;
      +}
      +
      +/**
      +* disconnect one input
      +* @method disconnectInput
      +* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
      +* @return {boolean} if it was disconnected succesfully
      +*/
      +LGraphNode.prototype.disconnectInput = function( slot )
      +{
      +	//seek for the output slot
      +	if( slot.constructor === String )
      +	{
      +		slot = this.findInputSlot(slot);
      +		if(slot == -1)
      +		{
      +			if(LiteGraph.debug)
      +				console.log("Connect: Error, no slot of name " + slot);
      +			return false;
      +		}
      +	}
      +	else if(!this.inputs || slot >= this.inputs.length)
      +	{
      +		if(LiteGraph.debug)
      +			console.log("Connect: Error, slot number not found");
      +		return false;
      +	}
      +
      +	var input = this.inputs[slot];
      +	if(!input)
      +		return false;
      +
      +	var link_id = this.inputs[slot].link;
      +	this.inputs[slot].link = null;
      +
      +	//remove other side
      +	var link_info = this.graph.links[ link_id ];
      +	if( link_info )
      +	{
      +		var target_node = this.graph.getNodeById( link_info.origin_id );
      +		if(!target_node)
      +			return false;
      +
      +		var output = target_node.outputs[ link_info.origin_slot ];
      +		if(!output || !output.links || output.links.length == 0)
      +			return false;
      +
      +		//search in the inputs list for this link
      +		for(var i = 0, l = output.links.length; i < l; i++)
      +		{
      +			if( output.links[i] == link_id )
      +			{
      +				output.links.splice(i,1);
      +				break;
      +			}
      +		}
      +
      +		delete this.graph.links[ link_id ]; //remove from the pool
      +		if(this.graph)
      +			this.graph._version++;
      +		if( this.onConnectionsChange )
      +			this.onConnectionsChange( LiteGraph.INPUT, slot, false, link_info, input );
      +		if( target_node.onConnectionsChange )
      +			target_node.onConnectionsChange( LiteGraph.OUTPUT, i, false, link_info, output );
      +	}
      +
      +	this.setDirtyCanvas(false,true);
      +	this.graph.connectionChange( this );
      +	return true;
      +}
      +
      +/**
      +* returns the center of a connection point in canvas coords
      +* @method getConnectionPos
      +* @param {boolean} is_input true if if a input slot, false if it is an output
      +* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
      +* @return {[x,y]} the position
      +**/
      +LGraphNode.prototype.getConnectionPos = function( is_input, slot_number )
      +{
      +	if(this.flags.collapsed)
      +	{
      +		if(is_input)
      +			return [this.pos[0], this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5];
      +		else
      +			return [this.pos[0] + (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH), this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5];
      +	}
      +
      +	if(is_input && slot_number == -1)
      +	{
      +		return [this.pos[0] + 10, this.pos[1] + 10];
      +	}
      +
      +	if(is_input && this.inputs.length > slot_number && this.inputs[slot_number].pos)
      +		return [this.pos[0] + this.inputs[slot_number].pos[0],this.pos[1] + this.inputs[slot_number].pos[1]];
      +	else if(!is_input && this.outputs.length > slot_number && this.outputs[slot_number].pos)
      +		return [this.pos[0] + this.outputs[slot_number].pos[0],this.pos[1] + this.outputs[slot_number].pos[1]];
      +
      +	if(!is_input) //output
      +		return [this.pos[0] + this.size[0] + 1, this.pos[1] + 10 + slot_number * LiteGraph.NODE_SLOT_HEIGHT + (this.constructor.slot_start_y || 0)];
      +	return [this.pos[0] , this.pos[1] + 10 + slot_number * LiteGraph.NODE_SLOT_HEIGHT + (this.constructor.slot_start_y || 0) ];
      +}
      +
      +/* Force align to grid */
      +LGraphNode.prototype.alignToGrid = function()
      +{
      +	this.pos[0] = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.pos[0] / LiteGraph.CANVAS_GRID_SIZE);
      +	this.pos[1] = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.pos[1] / LiteGraph.CANVAS_GRID_SIZE);
      +}
      +
      +
      +/* Console output */
      +LGraphNode.prototype.trace = function(msg)
      +{
      +	if(!this.console)
      +		this.console = [];
      +	this.console.push(msg);
      +	if(this.console.length > LGraphNode.MAX_CONSOLE)
      +		this.console.shift();
      +
      +	this.graph.onNodeTrace(this,msg);
      +}
      +
      +/* Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */
      +LGraphNode.prototype.setDirtyCanvas = function(dirty_foreground, dirty_background)
      +{
      +	if(!this.graph)
      +		return;
      +	this.graph.sendActionToCanvas("setDirty",[dirty_foreground, dirty_background]);
      +}
      +
      +LGraphNode.prototype.loadImage = function(url)
      +{
      +	var img = new Image();
      +	img.src = LiteGraph.node_images_path + url;
      +	img.ready = false;
      +
      +	var that = this;
      +	img.onload = function() {
      +		this.ready = true;
      +		that.setDirtyCanvas(true);
      +	}
      +	return img;
      +}
      +
      +//safe LGraphNode action execution (not sure if safe)
      +/*
      +LGraphNode.prototype.executeAction = function(action)
      +{
      +	if(action == "") return false;
      +
      +	if( action.indexOf(";") != -1 || action.indexOf("}") != -1)
      +	{
      +		this.trace("Error: Action contains unsafe characters");
      +		return false;
      +	}
      +
      +	var tokens = action.split("(");
      +	var func_name = tokens[0];
      +	if( typeof(this[func_name]) != "function")
      +	{
      +		this.trace("Error: Action not found on node: " + func_name);
      +		return false;
      +	}
      +
      +	var code = action;
      +
      +	try
      +	{
      +		var _foo = eval;
      +		eval = null;
      +		(new Function("with(this) { " + code + "}")).call(this);
      +		eval = _foo;
      +	}
      +	catch (err)
      +	{
      +		this.trace("Error executing action {" + action + "} :" + err);
      +		return false;
      +	}
      +
      +	return true;
      +}
      +*/
      +
      +/* Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */
      +LGraphNode.prototype.captureInput = function(v)
      +{
      +	if(!this.graph || !this.graph.list_of_graphcanvas)
      +		return;
      +
      +	var list = this.graph.list_of_graphcanvas;
      +
      +	for(var i = 0; i < list.length; ++i)
      +	{
      +		var c = list[i];
      +		//releasing somebody elses capture?!
      +		if(!v && c.node_capturing_input != this)
      +			continue;
      +
      +		//change
      +		c.node_capturing_input = v ? this : null;
      +	}
      +}
      +
      +/**
      +* Collapse the node to make it smaller on the canvas
      +* @method collapse
      +**/
      +LGraphNode.prototype.collapse = function( force )
      +{
      +	this.graph._version++;
      +	if(this.constructor.collapsable === false && !force)
      +		return;
      +	if(!this.flags.collapsed)
      +		this.flags.collapsed = true;
      +	else
      +		this.flags.collapsed = false;
      +	this.setDirtyCanvas(true,true);
      +}
      +
      +/**
      +* Forces the node to do not move or realign on Z
      +* @method pin
      +**/
      +
      +LGraphNode.prototype.pin = function(v)
      +{
      +	this.graph._version++;
      +	if(v === undefined)
      +		this.flags.pinned = !this.flags.pinned;
      +	else
      +		this.flags.pinned = v;
      +}
      +
      +LGraphNode.prototype.localToScreen = function(x,y, graphcanvas)
      +{
      +	return [(x + this.pos[0]) * graphcanvas.scale + graphcanvas.offset[0],
      +		(y + this.pos[1]) * graphcanvas.scale + graphcanvas.offset[1]];
      +}
      +
      +
      +
      +
      +function LGraphGroup( title )
      +{
      +	this._ctor( title );
      +}
      +
      +global.LGraphGroup = LiteGraph.LGraphGroup = LGraphGroup;
      +
      +LGraphGroup.prototype._ctor = function( title )
      +{
      +	this.title = title || "Group";
      +	this._bounding = new Float32Array([10,10,140,80]);
      +	this._pos = this._bounding.subarray(0,2);
      +	this._size = this._bounding.subarray(2,4);
      +	this._nodes = [];
      +	this.color = LGraphCanvas.node_colors.pale_blue ? LGraphCanvas.node_colors.pale_blue.groupcolor : "#AAA";
      +	this.graph = null;
      +
      +	Object.defineProperty( this, "pos", {
      +		set: function(v)
      +		{
      +			if(!v || v.length < 2)
      +				return;
      +			this._pos[0] = v[0];
      +			this._pos[1] = v[1];
      +		},
      +		get: function()
      +		{
      +			return this._pos;
      +		},
      +		enumerable: true
      +	});
      +
      +	Object.defineProperty( this, "size", {
      +		set: function(v)
      +		{
      +			if(!v || v.length < 2)
      +				return;
      +			this._size[0] = Math.max(140,v[0]);
      +			this._size[1] = Math.max(80,v[1]);
      +		},
      +		get: function()
      +		{
      +			return this._size;
      +		},
      +		enumerable: true
      +	});
      +}
      +
      +LGraphGroup.prototype.configure = function(o)
      +{
      +	this.title = o.title;
      +	this._bounding.set( o.bounding );
      +	this.color = o.color;
      +}
      +
      +LGraphGroup.prototype.serialize = function()
      +{
      +	var b = this._bounding;
      +	return {
      +		title: this.title,
      +		bounding: [ b[0], b[1], b[2], b[3] ],
      +		color: this.color
      +	};
      +}
      +
      +LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes)
      +{
      +	this._pos[0] += deltax;
      +	this._pos[1] += deltay;
      +	if(ignore_nodes)
      +		return;
      +	for(var i = 0; i < this._nodes.length; ++i)
      +	{
      +		var node = this._nodes[i];
      +		node.pos[0] += deltax;
      +		node.pos[1] += deltay;
      +	}
      +}
      +
      +LGraphGroup.prototype.recomputeInsideNodes = function()
      +{
      +	this._nodes.length = 0;
      +	var nodes = this.graph._nodes;
      +	var node_bounding = new Float32Array(4);
      +
      +	for(var i = 0; i < nodes.length; ++i)
      +	{
      +		var node = nodes[i];
      +		node.getBounding( node_bounding );
      +		if(!overlapBounding( this._bounding, node_bounding ))
      +			continue; //out of the visible area
      +		this._nodes.push( node );
      +	}
      +}
      +
      +LGraphGroup.prototype.isPointInside = LGraphNode.prototype.isPointInside;
      +LGraphGroup.prototype.setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas;
      +
      +//*********************************************************************************
      +// LGraphCanvas: LGraph renderer CLASS
      +//*********************************************************************************
      +
      +/**
      +* This class is in charge of rendering one graph inside a canvas. And provides all the interaction required.
      +* Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked
      +*
      +* @class LGraphCanvas
      +* @constructor
      +* @param {HTMLCanvas} canvas the canvas where you want to render (it accepts a selector in string format or the canvas element itself)
      +* @param {LGraph} graph [optional]
      +* @param {Object} options [optional] { skip_rendering, autoresize }
      +*/
      +function LGraphCanvas( canvas, graph, options )
      +{
      +	options = options || {};
      +
      +	//if(graph === undefined)
      +  //	throw ("No graph assigned");
      +	this.background_image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII='
      +
      +	if(canvas && canvas.constructor === String )
      +		canvas = document.querySelector( canvas );
      +
      +	this.max_zoom = 10;
      +	this.min_zoom = 0.1;
      +	this.zoom_modify_alpha = true; //otherwise it generates ugly patterns when scaling down too much
      +
      +	this.title_text_font = "bold "+LiteGraph.NODE_TEXT_SIZE+"px Arial";
      +	this.inner_text_font = "normal "+LiteGraph.NODE_SUBTEXT_SIZE+"px Arial";
      +	this.node_title_color = LiteGraph.NODE_TITLE_COLOR;
      +	this.default_link_color = "#AAC";
      +	this.default_connection_color = {
      +		input_off: "#AAB",
      +		input_on: "#7F7",
      +		output_off: "#AAB",
      +		output_on: "#7F7"
      +	};
      +
      +	this.highquality_render = true;
      +	this.use_gradients = false; //set to true to render titlebar with gradients
      +	this.editor_alpha = 1; //used for transition
      +	this.pause_rendering = false;
      +	this.render_shadows = true;
      +	this.clear_background = true;
      +
      +	this.render_only_selected = true;
      +	this.live_mode = false;
      +	this.show_info = true;
      +	this.allow_dragcanvas = true;
      +	this.allow_dragnodes = true;
      +	this.allow_interaction = true; //allow to control widgets, buttons, collapse, etc
      +	this.allow_searchbox = true;
      +	this.drag_mode = false;
      +	this.dragging_rectangle = null;
      +
      +	this.filter = null; //allows to filter to only accept some type of nodes in a graph
      +
      +	this.always_render_background = false;
      +	this.render_canvas_border = true;
      +	this.render_connections_shadows = false; //too much cpu
      +	this.render_connections_border = true;
      +	this.render_curved_connections = true;
      +	this.render_connection_arrows = true;
      +
      +	this.canvas_mouse = [0,0]; //mouse in canvas graph coordinates, where 0,0 is the top-left corner of the blue rectangle
      +
      +	//to personalize the search box
      +	this.onSearchBox = null;
      +	this.onSearchBoxSelection = null;
      +
      +	this.connections_width = 3;
      +	this.round_radius = 8;
      +
      +	this.current_node = null;
      +	this.node_widget = null; //used for widgets
      +	this.last_mouse_position = [0,0];
      +
      +	//link canvas and graph
      +	if(graph)
      +		graph.attachCanvas(this);
      +
      +	this.setCanvas( canvas );
      +	this.clear();
      +
      +	if(!options.skip_render)
      +		this.startRendering();
      +
      +	this.autoresize = options.autoresize;
      +}
      +
      +global.LGraphCanvas = LiteGraph.LGraphCanvas = LGraphCanvas;
      +
      +LGraphCanvas.link_type_colors = {"-1":"#F85",'number':"#AAC","node":"#DCA"};
      +LGraphCanvas.gradients = {}; //cache of gradients
      +
      +/**
      +* clears all the data inside
      +*
      +* @method clear
      +*/
      +LGraphCanvas.prototype.clear = function()
      +{
      +	this.frame = 0;
      +	this.last_draw_time = 0;
      +	this.render_time = 0;
      +	this.fps = 0;
      +
      +	this.scale = 1;
      +	this.offset = [0,0];
      +
      +	this.dragging_rectangle = null;
      +
      +	this.selected_nodes = {};
      +	this.selected_group = null;
      +
      +	this.visible_nodes = [];
      +	this.node_dragged = null;
      +	this.node_over = null;
      +	this.node_capturing_input = null;
      +	this.connecting_node = null;
      +	this.highlighted_links = {};
      +
      +	this.dirty_canvas = true;
      +	this.dirty_bgcanvas = true;
      +	this.dirty_area = null;
      +
      +	this.node_in_panel = null;
      +	this.node_widget = null;
      +
      +	this.last_mouse = [0,0];
      +	this.last_mouseclick = 0;
      +
      +	if(this.onClear)
      +		this.onClear();
      +	//this.UIinit();
      +}
      +
      +/**
      +* assigns a graph, you can reasign graphs to the same canvas
      +*
      +* @method setGraph
      +* @param {LGraph} graph
      +*/
      +LGraphCanvas.prototype.setGraph = function( graph, skip_clear )
      +{
      +	if(this.graph == graph)
      +		return;
      +
      +	if(!skip_clear)
      +		this.clear();
      +
      +	if(!graph && this.graph)
      +	{
      +		this.graph.detachCanvas(this);
      +		return;
      +	}
      +
      +	/*
      +	if(this.graph)
      +		this.graph.canvas = null; //remove old graph link to the canvas
      +	this.graph = graph;
      +	if(this.graph)
      +		this.graph.canvas = this;
      +	*/
      +	graph.attachCanvas(this);
      +	this.setDirty(true,true);
      +}
      +
      +/**
      +* opens a graph contained inside a node in the current graph
      +*
      +* @method openSubgraph
      +* @param {LGraph} graph
      +*/
      +LGraphCanvas.prototype.openSubgraph = function(graph)
      +{
      +	if(!graph)
      +		throw("graph cannot be null");
      +
      +	if(this.graph == graph)
      +		throw("graph cannot be the same");
      +
      +	this.clear();
      +
      +	if(this.graph)
      +	{
      +		if(!this._graph_stack)
      +			this._graph_stack = [];
      +		this._graph_stack.push(this.graph);
      +	}
      +
      +	graph.attachCanvas(this);
      +	this.setDirty(true,true);
      +}
      +
      +/**
      +* closes a subgraph contained inside a node
      +*
      +* @method closeSubgraph
      +* @param {LGraph} assigns a graph
      +*/
      +LGraphCanvas.prototype.closeSubgraph = function()
      +{
      +	if(!this._graph_stack || this._graph_stack.length == 0)
      +		return;
      +	var graph = this._graph_stack.pop();
      +	this.selected_nodes = {};
      +	this.highlighted_links = {};
      +	graph.attachCanvas(this);
      +	this.setDirty(true,true);
      +}
      +
      +/**
      +* assigns a canvas
      +*
      +* @method setCanvas
      +* @param {Canvas} assigns a canvas (also accepts the ID of the element (not a selector)
      +*/
      +LGraphCanvas.prototype.setCanvas = function( canvas, skip_events )
      +{
      +	var that = this;
      +
      +	if(canvas)
      +	{
      +		if( canvas.constructor === String )
      +		{
      +			canvas = document.getElementById(canvas);
      +			if(!canvas)
      +				throw("Error creating LiteGraph canvas: Canvas not found");
      +		}
      +	}
      +
      +	if(canvas === this.canvas)
      +		return;
      +
      +	if(!canvas && this.canvas)
      +	{
      +		//maybe detach events from old_canvas
      +		if(!skip_events)
      +			this.unbindEvents();
      +	}
      +
      +	this.canvas = canvas;
      +
      +	if(!canvas)
      +		return;
      +
      +	//this.canvas.tabindex = "1000";
      +	canvas.className += " lgraphcanvas";
      +	canvas.data = this;
      +
      +	//bg canvas: used for non changing stuff
      +	this.bgcanvas = null;
      +	if(!this.bgcanvas)
      +	{
      +		this.bgcanvas = document.createElement("canvas");
      +		this.bgcanvas.width = this.canvas.width;
      +		this.bgcanvas.height = this.canvas.height;
      +	}
      +
      +	if(canvas.getContext == null)
      +	{
      +		if( canvas.localName != "canvas" )
      +			throw("Element supplied for LGraphCanvas must be a <canvas> element, you passed a " + canvas.localName );
      +		throw("This browser doesnt support Canvas");
      +	}
      +
      +	var ctx = this.ctx = canvas.getContext("2d");
      +	if(ctx == null)
      +	{
      +		if(!canvas.webgl_enabled)
      +			console.warn("This canvas seems to be WebGL, enabling WebGL renderer");
      +		this.enableWebGL();
      +	}
      +
      +	//input:  (move and up could be unbinded)
      +	this._mousemove_callback = this.processMouseMove.bind(this);
      +	this._mouseup_callback = this.processMouseUp.bind(this);
      +
      +	if(!skip_events)
      +		this.bindEvents();
      +}
      +
      +//used in some events to capture them
      +LGraphCanvas.prototype._doNothing = function doNothing(e) { e.preventDefault(); return false; };
      +LGraphCanvas.prototype._doReturnTrue = function doNothing(e) { e.preventDefault(); return true; };
      +
      +/**
      +* binds mouse, keyboard, touch and drag events to the canvas
      +* @method bindEvents
      +**/
      +LGraphCanvas.prototype.bindEvents = function()
      +{
      +	if(	this._events_binded )
      +	{
      +		console.warn("LGraphCanvas: events already binded");
      +		return;
      +	}
      +
      +	var canvas = this.canvas;
      +	var ref_window = this.getCanvasWindow();
      +	var document = ref_window.document; //hack used when moving canvas between windows
      +
      +	this._mousedown_callback = this.processMouseDown.bind(this);
      +	this._mousewheel_callback = this.processMouseWheel.bind(this);
      +
      +	canvas.addEventListener("mousedown", this._mousedown_callback, true ); //down do not need to store the binded
      +	canvas.addEventListener("mousemove", this._mousemove_callback );
      +	canvas.addEventListener("mousewheel", this._mousewheel_callback, false);
      +
      +	canvas.addEventListener("contextmenu", this._doNothing );
      +	canvas.addEventListener("DOMMouseScroll", this._mousewheel_callback, false);
      +
      +	//touch events
      +	//if( 'touchstart' in document.documentElement )
      +	{
      +		canvas.addEventListener("touchstart", this.touchHandler, true);
      +		canvas.addEventListener("touchmove", this.touchHandler, true);
      +		canvas.addEventListener("touchend", this.touchHandler, true);
      +		canvas.addEventListener("touchcancel", this.touchHandler, true);
      +	}
      +
      +	//Keyboard ******************
      +	this._key_callback = this.processKey.bind(this);
      +
      +	canvas.addEventListener("keydown", this._key_callback, true );
      +	document.addEventListener("keyup", this._key_callback, true ); //in document, otherwise it doesnt fire keyup
      +
      +	//Droping Stuff over nodes ************************************
      +	this._ondrop_callback = this.processDrop.bind(this);
      +
      +	canvas.addEventListener("dragover", this._doNothing, false );
      +	canvas.addEventListener("dragend", this._doNothing, false );
      +	canvas.addEventListener("drop", this._ondrop_callback, false );
      +	canvas.addEventListener("dragenter", this._doReturnTrue, false );
      +
      +	this._events_binded = true;
      +}
      +
      +/**
      +* unbinds mouse events from the canvas
      +* @method unbindEvents
      +**/
      +LGraphCanvas.prototype.unbindEvents = function()
      +{
      +	if(	!this._events_binded )
      +	{
      +		console.warn("LGraphCanvas: no events binded");
      +		return;
      +	}
      +
      +	var ref_window = this.getCanvasWindow();
      +	var document = ref_window.document;
      +
      +	this.canvas.removeEventListener( "mousedown", this._mousedown_callback );
      +	this.canvas.removeEventListener( "mousewheel", this._mousewheel_callback );
      +	this.canvas.removeEventListener( "DOMMouseScroll", this._mousewheel_callback );
      +	this.canvas.removeEventListener( "keydown", this._key_callback );
      +	document.removeEventListener( "keyup", this._key_callback );
      +	this.canvas.removeEventListener( "contextmenu", this._doNothing );
      +	this.canvas.removeEventListener( "drop", this._ondrop_callback );
      +	this.canvas.removeEventListener( "dragenter", this._doReturnTrue );
      +
      +	this.canvas.removeEventListener("touchstart", this.touchHandler );
      +	this.canvas.removeEventListener("touchmove", this.touchHandler );
      +	this.canvas.removeEventListener("touchend", this.touchHandler );
      +	this.canvas.removeEventListener("touchcancel", this.touchHandler );
      +
      +	this._mousedown_callback = null;
      +	this._mousewheel_callback = null;
      +	this._key_callback = null;
      +	this._ondrop_callback = null;
      +
      +	this._events_binded = false;
      +}
      +
      +LGraphCanvas.getFileExtension = function (url)
      +{
      +	var question = url.indexOf("?");
      +	if(question != -1)
      +		url = url.substr(0,question);
      +	var point = url.lastIndexOf(".");
      +	if(point == -1)
      +		return "";
      +	return url.substr(point+1).toLowerCase();
      +}
      +
      +/**
      +* this function allows to render the canvas using WebGL instead of Canvas2D
      +* this is useful if you plant to render 3D objects inside your nodes, it uses litegl.js for webgl and canvas2DtoWebGL to emulate the Canvas2D calls in webGL
      +* @method enableWebGL
      +**/
      +LGraphCanvas.prototype.enableWebGL = function()
      +{
      +	if(typeof(GL) === undefined)
      +		throw("litegl.js must be included to use a WebGL canvas");
      +	if(typeof(enableWebGLCanvas) === undefined)
      +		throw("webglCanvas.js must be included to use this feature");
      +
      +	this.gl = this.ctx = enableWebGLCanvas(this.canvas);
      +	this.ctx.webgl = true;
      +	this.bgcanvas = this.canvas;
      +	this.bgctx = this.gl;
      +	this.canvas.webgl_enabled = true;
      +
      +	/*
      +	GL.create({ canvas: this.bgcanvas });
      +	this.bgctx = enableWebGLCanvas( this.bgcanvas );
      +	window.gl = this.gl;
      +	*/
      +}
      +
      +
      +/**
      +* marks as dirty the canvas, this way it will be rendered again
      +*
      +* @class LGraphCanvas
      +* @method setDirty
      +* @param {bool} fgcanvas if the foreground canvas is dirty (the one containing the nodes)
      +* @param {bool} bgcanvas if the background canvas is dirty (the one containing the wires)
      +*/
      +LGraphCanvas.prototype.setDirty = function( fgcanvas, bgcanvas )
      +{
      +	if(fgcanvas)
      +		this.dirty_canvas = true;
      +	if(bgcanvas)
      +		this.dirty_bgcanvas = true;
      +}
      +
      +/**
      +* Used to attach the canvas in a popup
      +*
      +* @method getCanvasWindow
      +* @return {window} returns the window where the canvas is attached (the DOM root node)
      +*/
      +LGraphCanvas.prototype.getCanvasWindow = function()
      +{
      +	if(!this.canvas)
      +		return window;
      +	var doc = this.canvas.ownerDocument;
      +	return doc.defaultView || doc.parentWindow;
      +}
      +
      +/**
      +* starts rendering the content of the canvas when needed
      +*
      +* @method startRendering
      +*/
      +LGraphCanvas.prototype.startRendering = function()
      +{
      +	if(this.is_rendering)
      +		return; //already rendering
      +
      +	this.is_rendering = true;
      +	renderFrame.call(this);
      +
      +	function renderFrame()
      +	{
      +		if(!this.pause_rendering)
      +			this.draw();
      +
      +		var window = this.getCanvasWindow();
      +		if(this.is_rendering)
      +			window.requestAnimationFrame( renderFrame.bind(this) );
      +	}
      +}
      +
      +/**
      +* stops rendering the content of the canvas (to save resources)
      +*
      +* @method stopRendering
      +*/
      +LGraphCanvas.prototype.stopRendering = function()
      +{
      +	this.is_rendering = false;
      +	/*
      +	if(this.rendering_timer_id)
      +	{
      +		clearInterval(this.rendering_timer_id);
      +		this.rendering_timer_id = null;
      +	}
      +	*/
      +}
      +
      +/* LiteGraphCanvas input */
      +
      +LGraphCanvas.prototype.processMouseDown = function(e)
      +{
      +	if(!this.graph)
      +		return;
      +
      +	this.adjustMouseEvent(e);
      +
      +	var ref_window = this.getCanvasWindow();
      +	var document = ref_window.document;
      +	LGraphCanvas.active_canvas = this;
      +	var that = this;
      +
      +	//move mouse move event to the window in case it drags outside of the canvas
      +	this.canvas.removeEventListener("mousemove", this._mousemove_callback );
      +	ref_window.document.addEventListener("mousemove", this._mousemove_callback, true ); //catch for the entire window
      +	ref_window.document.addEventListener("mouseup", this._mouseup_callback, true );
      +
      +	var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes );
      +	var skip_dragging = false;
      +	var skip_action = false;
      +	var now = LiteGraph.getTime();
      +	var is_double_click = (now - this.last_mouseclick) < 300;
      +
      +	this.canvas_mouse[0] = e.canvasX;
      +	this.canvas_mouse[1] = e.canvasY;
      +
      +    LiteGraph.closeAllContextMenus( ref_window );
      +
      +	if(e.which == 1) //left button mouse
      +	{
      +		if( e.ctrlKey )
      +		{
      +			this.dragging_rectangle = new Float32Array(4);
      +			this.dragging_rectangle[0] = e.canvasX;
      +			this.dragging_rectangle[1] = e.canvasY;
      +			this.dragging_rectangle[2] = 1;
      +			this.dragging_rectangle[3] = 1;
      +			skip_action = true;
      +		}
      +
      +		var clicking_canvas_bg = false;
      +
      +		//when clicked on top of a node
      +		//and it is not interactive
      +		if( node && this.allow_interaction && !skip_action )
      +		{
      +			if( !this.live_mode && !node.flags.pinned )
      +				this.bringToFront( node ); //if it wasnt selected?
      +
      +			//not dragging mouse to connect two slots
      +			if(!this.connecting_node && !node.flags.collapsed && !this.live_mode)
      +			{
      +				//search for outputs
      +				if(node.outputs)
      +					for(var i = 0, l = node.outputs.length; i < l; ++i)
      +					{
      +						var output = node.outputs[i];
      +						var link_pos = node.getConnectionPos(false,i);
      +						if( isInsideRectangle( e.canvasX, e.canvasY, link_pos[0] - 10, link_pos[1] - 5, 20,10) )
      +						{
      +							this.connecting_node = node;
      +							this.connecting_output = output;
      +							this.connecting_pos = node.getConnectionPos(false,i);
      +							this.connecting_slot = i;
      +
      +                            if (is_double_click) {
      +                                if (node.onOutputDblClick)
      +                                    node.onOutputDblClick(i, e);
      +                            } else {
      +                                if (node.onOutputClick)
      +                                    node.onOutputClick(i, e);
      +                            }
      +
      +							skip_action = true;
      +							break;
      +						}
      +					}
      +
      +				//search for inputs
      +				if(node.inputs)
      +					for(var i = 0, l = node.inputs.length; i < l; ++i)
      +					{
      +						var input = node.inputs[i];
      +						var link_pos = node.getConnectionPos( true, i );
      +						if( isInsideRectangle(e.canvasX, e.canvasY, link_pos[0] - 10, link_pos[1] - 5, 20,10) )
      +						{
      +                            if (is_double_click) {
      +                                if (node.onInputDblClick)
      +                                    node.onInputDblClick(i, e);
      +                            } else {
      +                                if (node.onInputClick)
      +                                    node.onInputClick(i, e);
      +                            }
      +
      +							if(input.link !== null)
      +							{
      +								node.disconnectInput(i);
      +								this.dirty_bgcanvas = true;
      +								skip_action = true;
      +							}
      +						}
      +					}
      +
      +				//Search for corner
      +				if( !skip_action && node.flags.resizable !== false && isInsideRectangle(e.canvasX, e.canvasY, node.pos[0] + node.size[0] - 5, node.pos[1] + node.size[1] - 5 ,5,5 ))
      +				{
      +					this.resizing_node = node;
      +					this.canvas.style.cursor = "se-resize";
      +					skip_action = true;
      +				}
      +			}
      +
      +			//Search for corner
      +			if( !skip_action && isInsideRectangle(e.canvasX, e.canvasY, node.pos[0], node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT, LiteGraph.NODE_TITLE_HEIGHT, LiteGraph.NODE_TITLE_HEIGHT ))
      +			{
      +				node.collapse();
      +				skip_action = true;
      +			}
      +
      +			//it wasnt clicked on the links boxes
      +			if(!skip_action)
      +			{
      +				var block_drag_node = false;
      +
      +				//widgets
      +				var widget = this.processNodeWidgets( node, this.canvas_mouse, e );
      +				if(widget)
      +				{
      +					block_drag_node = true;
      +					this.node_widget = [node, widget];
      +				}
      +
      +				//double clicking
      +				if (is_double_click && this.selected_nodes[ node.id ])
      +				{
      +					//double click node
      +					if( node.onDblClick)
      +						node.onDblClick(e);
      +					this.processNodeDblClicked( node );
      +					block_drag_node = true;
      +				}
      +
      +				//if do not capture mouse
      +				if( node.onMouseDown && node.onMouseDown( e, [e.canvasX - node.pos[0], e.canvasY - node.pos[1]] ) )
      +					block_drag_node = true;
      +				else if(this.live_mode)
      +				{
      +					clicking_canvas_bg = true;
      +					block_drag_node = true;
      +				}
      +
      +				if(!block_drag_node)
      +				{
      +					if(this.allow_dragnodes)
      +						this.node_dragged = node;
      +					if(!this.selected_nodes[ node.id ])
      +						this.processNodeSelected( node, e );
      +				}
      +
      +				this.dirty_canvas = true;
      +			}
      +		}
      +		else //clicked outside of nodes
      +		{
      +			this.selected_group = this.graph.getGroupOnPos( e.canvasX, e.canvasY );
      +			this.selected_group_resizing = false;
      +			if( this.selected_group )
      +			{
      +				if( e.ctrlKey )
      +					this.dragging_rectangle = null;
      +
      +				var dist = distance( [e.canvasX, e.canvasY], [ this.selected_group.pos[0] + this.selected_group.size[0], this.selected_group.pos[1] + this.selected_group.size[1] ] );
      +				if( (dist * this.scale) < 10 )
      +					this.selected_group_resizing = true;
      +				else
      +					this.selected_group.recomputeInsideNodes();
      +			}
      +
      +			if( is_double_click )
      +				this.showSearchBox( e );
      +			
      +			clicking_canvas_bg = true;
      +		}
      +
      +		if( !skip_action && clicking_canvas_bg && this.allow_dragcanvas )
      +		{
      +			this.dragging_canvas = true;
      +		}
      +	}
      +	else if (e.which == 2) //middle button
      +	{
      +
      +	}
      +	else if (e.which == 3) //right button
      +	{
      +		this.processContextMenu( node, e );
      +	}
      +
      +	//TODO
      +	//if(this.node_selected != prev_selected)
      +	//	this.onNodeSelectionChange(this.node_selected);
      +
      +	this.last_mouse[0] = e.localX;
      +	this.last_mouse[1] = e.localY;
      +	this.last_mouseclick = LiteGraph.getTime();
      +
      +	/*
      +	if( (this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null)
      +		this.draw();
      +	*/
      +
      +	this.graph.change();
      +
      +	//this is to ensure to defocus(blur) if a text input element is on focus
      +	if(!ref_window.document.activeElement || (ref_window.document.activeElement.nodeName.toLowerCase() != "input" && ref_window.document.activeElement.nodeName.toLowerCase() != "textarea"))
      +		e.preventDefault();
      +	e.stopPropagation();
      +
      +	if(this.onMouseDown)
      +		this.onMouseDown(e);
      +
      +	return false;
      +}
      +
      +/**
      +* Called when a mouse move event has to be processed
      +* @method processMouseMove
      +**/
      +LGraphCanvas.prototype.processMouseMove = function(e)
      +{
      +	if(this.autoresize)
      +		this.resize();
      +
      +	if(!this.graph)
      +		return;
      +
      +	LGraphCanvas.active_canvas = this;
      +	this.adjustMouseEvent(e);
      +	var mouse = [e.localX, e.localY];
      +	var delta = [mouse[0] - this.last_mouse[0], mouse[1] - this.last_mouse[1]];
      +	this.last_mouse = mouse;
      +	this.canvas_mouse[0] = e.canvasX;
      +	this.canvas_mouse[1] = e.canvasY;
      +
      +	if( this.node_widget )
      +	{
      +		this.processNodeWidgets( this.node_widget[0], this.canvas_mouse, e, this.node_widget[1] );
      +		this.dirty_canvas = true;
      +	}
      +
      +	if( this.dragging_rectangle )
      +	{
      +		this.dragging_rectangle[2] = e.canvasX - this.dragging_rectangle[0];
      +		this.dragging_rectangle[3] = e.canvasY - this.dragging_rectangle[1];
      +		this.dirty_canvas = true;
      +	}
      +	else if (this.selected_group)
      +	{
      +		if( this.selected_group_resizing )
      +			this.selected_group.size = [ e.canvasX - this.selected_group.pos[0], e.canvasY - this.selected_group.pos[1] ];
      +		else
      +		{
      +			var deltax = delta[0] / this.scale;
      +			var deltay = delta[1] / this.scale;
      +			this.selected_group.move( deltax, deltay, e.ctrlKey );
      +			if( this.selected_group._nodes.length)
      +				this.dirty_canvas = true;
      +		}
      +		this.dirty_bgcanvas = true;
      +	}
      +	else if(this.dragging_canvas)
      +	{
      +		this.offset[0] += delta[0] / this.scale;
      +		this.offset[1] += delta[1] / this.scale;
      +		this.dirty_canvas = true;
      +		this.dirty_bgcanvas = true;
      +	}
      +	else if(this.allow_interaction)
      +	{
      +		if(this.connecting_node)
      +			this.dirty_canvas = true;
      +
      +		//get node over
      +		var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes );
      +
      +		//remove mouseover flag
      +		for(var i = 0, l = this.graph._nodes.length; i < l; ++i)
      +		{
      +			if(this.graph._nodes[i].mouseOver && node != this.graph._nodes[i])
      +			{
      +				//mouse leave
      +				this.graph._nodes[i].mouseOver = false;
      +				if(this.node_over && this.node_over.onMouseLeave)
      +					this.node_over.onMouseLeave(e);
      +				this.node_over = null;
      +				this.dirty_canvas = true;
      +			}
      +		}
      +
      +		//mouse over a node
      +		if(node)
      +		{
      +			//this.canvas.style.cursor = "move";
      +			if(!node.mouseOver)
      +			{
      +				//mouse enter
      +				node.mouseOver = true;
      +				this.node_over = node;
      +				this.dirty_canvas = true;
      +
      +				if(node.onMouseEnter) node.onMouseEnter(e);
      +			}
      +
      +			//in case the node wants to do something
      +			if(node.onMouseMove)
      +				node.onMouseMove(e);
      +
      +			//if dragging a link 
      +			if(this.connecting_node)
      +			{
      +				var pos = this._highlight_input || [0,0]; //to store the output of isOverNodeInput
      +
      +				//on top of input
      +				if( this.isOverNodeBox( node, e.canvasX, e.canvasY ) )
      +				{
      +					//mouse on top of the corner box, dont know what to do
      +				}
      +				else
      +				{
      +					//check if I have a slot below de mouse
      +					var slot = this.isOverNodeInput( node, e.canvasX, e.canvasY, pos );
      +					if(slot != -1 && node.inputs[slot] )
      +					{
      +						var slot_type = node.inputs[slot].type;
      +						if( LiteGraph.isValidConnection( this.connecting_output.type, slot_type ) )
      +							this._highlight_input = pos;
      +					}
      +					else
      +						this._highlight_input = null;
      +				}
      +			}
      +
      +			//Search for corner
      +			if(this.canvas)
      +			{
      +				if( isInsideRectangle(e.canvasX, e.canvasY, node.pos[0] + node.size[0] - 5, node.pos[1] + node.size[1] - 5 ,5,5 ))
      +					this.canvas.style.cursor = "se-resize";
      +				else
      +					this.canvas.style.cursor = null;
      +			}
      +		}
      +		else if(this.canvas)
      +			this.canvas.style.cursor = null;
      +
      +		if(this.node_capturing_input && this.node_capturing_input != node && this.node_capturing_input.onMouseMove)
      +		{
      +			this.node_capturing_input.onMouseMove(e);
      +		}
      +
      +
      +		if(this.node_dragged && !this.live_mode)
      +		{
      +			/*
      +			this.node_dragged.pos[0] += delta[0] / this.scale;
      +			this.node_dragged.pos[1] += delta[1] / this.scale;
      +			this.node_dragged.pos[0] = Math.round(this.node_dragged.pos[0]);
      +			this.node_dragged.pos[1] = Math.round(this.node_dragged.pos[1]);
      +			*/
      +
      +			for(var i in this.selected_nodes)
      +			{
      +				var n = this.selected_nodes[i];
      +
      +				n.pos[0] += delta[0] / this.scale;
      +				n.pos[1] += delta[1] / this.scale;
      +				//n.pos[0] = Math.round(n.pos[0]);
      +				//n.pos[1] = Math.round(n.pos[1]);
      +			}
      +
      +			this.dirty_canvas = true;
      +			this.dirty_bgcanvas = true;
      +		}
      +
      +		if(this.resizing_node && !this.live_mode)
      +		{
      +			//convert mouse to node space
      +			this.resizing_node.size[0] = e.canvasX - this.resizing_node.pos[0];
      +			this.resizing_node.size[1] = e.canvasY - this.resizing_node.pos[1];
      +
      +			//constraint size
      +			var max_slots = Math.max( this.resizing_node.inputs ? this.resizing_node.inputs.length : 0, this.resizing_node.outputs ? this.resizing_node.outputs.length : 0);
      +			var min_height = max_slots * LiteGraph.NODE_SLOT_HEIGHT + ( this.resizing_node.widgets ? this.resizing_node.widgets.length : 0 ) * (LiteGraph.NODE_WIDGET_HEIGHT + 4 ) + 4;
      +			if(this.resizing_node.size[1] < min_height )
      +				this.resizing_node.size[1] = min_height;
      +			if(this.resizing_node.size[0] < LiteGraph.NODE_MIN_WIDTH)
      +				this.resizing_node.size[0] = LiteGraph.NODE_MIN_WIDTH;
      +
      +			this.canvas.style.cursor = "se-resize";
      +			this.dirty_canvas = true;
      +			this.dirty_bgcanvas = true;
      +		}
      +	}
      +
      +	/*
      +	if((this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null)
      +		this.draw();
      +	*/
      +
      +	e.preventDefault();
      +	//e.stopPropagation();
      +	return false;
      +	//this is not really optimal
      +	//this.graph.change();
      +}
      +
      +/**
      +* Called when a mouse up event has to be processed
      +* @method processMouseUp
      +**/
      +LGraphCanvas.prototype.processMouseUp = function(e)
      +{
      +	if(!this.graph)
      +		return;
      +
      +	var window = this.getCanvasWindow();
      +	var document = window.document;
      +	LGraphCanvas.active_canvas = this;
      +
      +	//restore the mousemove event back to the canvas
      +	document.removeEventListener("mousemove", this._mousemove_callback, true );
      +	this.canvas.addEventListener("mousemove", this._mousemove_callback, true);
      +	document.removeEventListener("mouseup", this._mouseup_callback, true );
      +
      +	this.adjustMouseEvent(e);
      +	var now = LiteGraph.getTime();
      +	e.click_time = (now - this.last_mouseclick);
      +
      +	if (e.which == 1) //left button
      +	{
      +		this.node_widget = null;
      +		this.selected_group = null;
      +		this.selected_group_resizing = false;
      +
      +		if( this.dragging_rectangle )
      +		{
      +			if(this.graph)
      +			{
      +				var nodes = this.graph._nodes;
      +				var node_bounding = new Float32Array(4);
      +				this.deselectAllNodes();
      +				if( this.dragging_rectangle[2] < 0 ) //flip if negative width
      +					this.dragging_rectangle[0] += this.dragging_rectangle[2];
      +				if( this.dragging_rectangle[3] < 0 ) //flip if negative height
      +					this.dragging_rectangle[1] += this.dragging_rectangle[3];
      +				this.dragging_rectangle[2] = Math.abs( this.dragging_rectangle[2] * this.scale ); //abs to convert negative width
      +				this.dragging_rectangle[3] = Math.abs( this.dragging_rectangle[3] * this.scale ); //abs to convert negative height
      +
      +				for(var i = 0; i < nodes.length; ++i)
      +				{
      +					var node = nodes[i];
      +					node.getBounding( node_bounding );
      +					if(!overlapBounding( this.dragging_rectangle, node_bounding ))
      +						continue; //out of the visible area
      +					this.selectNode( node, true );
      +				}
      +			}
      +			this.dragging_rectangle = null;
      +		}
      +		else if(this.connecting_node) //dragging a connection
      +		{
      +			this.dirty_canvas = true;
      +			this.dirty_bgcanvas = true;
      +
      +			var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes );
      +
      +			//node below mouse
      +			if(node)
      +			{
      +				if( this.connecting_output.type == LiteGraph.EVENT && this.isOverNodeBox( node, e.canvasX, e.canvasY ) )
      +				{
      +					this.connecting_node.connect( this.connecting_slot, node, LiteGraph.EVENT );
      +				}
      +				else
      +				{
      +					//slot below mouse? connect
      +					var slot = this.isOverNodeInput(node, e.canvasX, e.canvasY);
      +					if(slot != -1)
      +					{
      +						this.connecting_node.connect(this.connecting_slot, node, slot);
      +					}
      +					else
      +					{ //not on top of an input
      +						var input = node.getInputInfo(0);
      +						//auto connect
      +						if(this.connecting_output.type == LiteGraph.EVENT)
      +							this.connecting_node.connect( this.connecting_slot, node, LiteGraph.EVENT );
      +						else
      +							if(input && !input.link && LiteGraph.isValidConnection( input.type && this.connecting_output.type ) )
      +								this.connecting_node.connect( this.connecting_slot, node, 0 );
      +					}
      +				}
      +			}
      +
      +			this.connecting_output = null;
      +			this.connecting_pos = null;
      +			this.connecting_node = null;
      +			this.connecting_slot = -1;
      +
      +		}//not dragging connection
      +		else if(this.resizing_node)
      +		{
      +			this.dirty_canvas = true;
      +			this.dirty_bgcanvas = true;
      +			this.resizing_node = null;
      +		}
      +		else if(this.node_dragged) //node being dragged?
      +		{
      +			this.dirty_canvas = true;
      +			this.dirty_bgcanvas = true;
      +			this.node_dragged.pos[0] = Math.round(this.node_dragged.pos[0]);
      +			this.node_dragged.pos[1] = Math.round(this.node_dragged.pos[1]);
      +			if(this.graph.config.align_to_grid)
      +				this.node_dragged.alignToGrid();
      +			this.node_dragged = null;
      +		}
      +		else //no node being dragged
      +		{
      +			//get node over
      +			var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes );
      +			if ( !node && e.click_time < 300 )
      +				this.deselectAllNodes();
      +
      +			this.dirty_canvas = true;
      +			this.dragging_canvas = false;
      +
      +			if( this.node_over && this.node_over.onMouseUp )
      +				this.node_over.onMouseUp(e, [e.canvasX - this.node_over.pos[0], e.canvasY - this.node_over.pos[1]] );
      +			if( this.node_capturing_input && this.node_capturing_input.onMouseUp )
      +				this.node_capturing_input.onMouseUp(e, [e.canvasX - this.node_capturing_input.pos[0], e.canvasY - this.node_capturing_input.pos[1]] );
      +		}
      +	}
      +	else if (e.which == 2) //middle button
      +	{
      +		//trace("middle");
      +		this.dirty_canvas = true;
      +		this.dragging_canvas = false;
      +	}
      +	else if (e.which == 3) //right button
      +	{
      +		//trace("right");
      +		this.dirty_canvas = true;
      +		this.dragging_canvas = false;
      +	}
      +
      +	/*
      +	if((this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null)
      +		this.draw();
      +	*/
      +
      +	this.graph.change();
      +
      +	e.stopPropagation();
      +	e.preventDefault();
      +	return false;
      +}
      +
      +/**
      +* Called when a mouse wheel event has to be processed
      +* @method processMouseWheel
      +**/
      +LGraphCanvas.prototype.processMouseWheel = function(e)
      +{
      +	if(!this.graph || !this.allow_dragcanvas)
      +		return;
      +
      +	var delta = (e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60);
      +
      +	this.adjustMouseEvent(e);
      +
      +	var zoom = this.scale;
      +
      +	if (delta > 0)
      +		zoom *= 1.1;
      +	else if (delta < 0)
      +		zoom *= 1/(1.1);
      +
      +	this.setZoom( zoom, [ e.localX, e.localY ] );
      +
      +	/*
      +	if(this.rendering_timer_id == null)
      +		this.draw();
      +	*/
      +
      +	this.graph.change();
      +
      +	e.preventDefault();
      +	return false; // prevent default
      +}
      +
      +/**
      +* retuns true if a position (in graph space) is on top of a node little corner box
      +* @method isOverNodeBox
      +**/
      +LGraphCanvas.prototype.isOverNodeBox = function( node, canvasx, canvasy )
      +{
      +	var title_height = LiteGraph.NODE_TITLE_HEIGHT;
      +	if( isInsideRectangle( canvasx, canvasy, node.pos[0] + 2, node.pos[1] + 2 - title_height, title_height - 4,title_height - 4) )
      +		return true;
      +	return false;
      +}
      +
      +/**
      +* retuns true if a position (in graph space) is on top of a node input slot
      +* @method isOverNodeInput
      +**/
      +LGraphCanvas.prototype.isOverNodeInput = function(node, canvasx, canvasy, slot_pos )
      +{
      +	if(node.inputs)
      +		for(var i = 0, l = node.inputs.length; i < l; ++i)
      +		{
      +			var input = node.inputs[i];
      +			var link_pos = node.getConnectionPos(true,i);
      +			if( isInsideRectangle(canvasx, canvasy, link_pos[0] - 10, link_pos[1] - 5, 20,10) )
      +			{
      +				if(slot_pos)
      +				{
      +					slot_pos[0] = link_pos[0];
      +					slot_pos[1] = link_pos[1];
      +				}
      +				return i;
      +			}
      +		}
      +	return -1;
      +}
      +
      +/**
      +* process a key event
      +* @method processKey
      +**/
      +LGraphCanvas.prototype.processKey = function(e)
      +{
      +	if(!this.graph)
      +		return;
      +
      +	var block_default = false;
      +	//console.log(e); //debug
      +
      +	if(e.target.localName == "input")
      +		return;
      +
      +	if(e.type == "keydown")
      +	{
      +		if(e.keyCode == 32)
      +		{
      +			this.dragging_canvas = true;
      +			block_default = true;
      +		}
      +
      +		//select all Control A
      +		if(e.keyCode == 65 && e.ctrlKey)
      +		{
      +			this.selectNodes();
      +			block_default = true;
      +		}
      +
      +		if(e.code == "KeyC" && (e.metaKey || e.ctrlKey) && !e.shiftKey ) //copy
      +		{
      +			if(this.selected_nodes)
      +			{
      +				this.copyToClipboard();
      +				block_default = true;
      +			}
      +		}
      +
      +		if(e.code == "KeyV" && (e.metaKey || e.ctrlKey) && !e.shiftKey ) //paste
      +		{
      +			this.pasteFromClipboard();
      +		}
      +
      +		//delete or backspace
      +		if(e.keyCode == 46 || e.keyCode == 8)
      +		{
      +			this.deleteSelectedNodes();
      +			block_default = true;
      +		}
      +
      +		//collapse
      +		//...
      +
      +		//TODO
      +		if(this.selected_nodes)
      +			for (var i in this.selected_nodes)
      +				if(this.selected_nodes[i].onKeyDown)
      +					this.selected_nodes[i].onKeyDown(e);
      +	}
      +	else if( e.type == "keyup" )
      +	{
      +		if(e.keyCode == 32)
      +			this.dragging_canvas = false;
      +
      +		if(this.selected_nodes)
      +			for (var i in this.selected_nodes)
      +				if(this.selected_nodes[i].onKeyUp)
      +					this.selected_nodes[i].onKeyUp(e);
      +	}
      +
      +	this.graph.change();
      +
      +	if(block_default)
      +	{
      +		e.preventDefault();
      +		return false;
      +	}
      +}
      +
      +LGraphCanvas.prototype.copyToClipboard = function()
      +{
      +	var clipboard_info = {
      +		nodes: [],
      +		links: []
      +	};
      +	var index = 0;
      +	var selected_nodes_array = [];
      +	for(var i in this.selected_nodes)
      +	{
      +		var node = this.selected_nodes[i];
      +		node._relative_id = index;
      +		selected_nodes_array.push( node );
      +		index += 1;
      +	}
      +
      +	for(var i = 0; i < selected_nodes_array.length; ++i)
      +	{
      +		var node = selected_nodes_array[i];
      +		clipboard_info.nodes.push( node.clone().serialize() );
      +		if(node.inputs && node.inputs.length)
      +			for(var j = 0; j < node.inputs.length; ++j)
      +			{
      +				var input = node.inputs[j];
      +				if(!input || input.link == null)
      +					continue;
      +				var link_info = this.graph.links[ input.link ];
      +				if(!link_info)
      +					continue;
      +				var target_node = this.graph.getNodeById( link_info.origin_id );
      +				if(!target_node || !this.selected_nodes[ target_node.id ] ) //improve this by allowing connections to non-selected nodes
      +					continue; //not selected
      +				clipboard_info.links.push([ target_node._relative_id, j, node._relative_id, link_info.target_slot ]);
      +			}
      +	}
      +	localStorage.setItem( "litegrapheditor_clipboard", JSON.stringify( clipboard_info ) );
      +}
      +
      +LGraphCanvas.prototype.pasteFromClipboard = function()
      +{
      +	var data = localStorage.getItem( "litegrapheditor_clipboard" );
      +	if(!data)
      +		return;
      +
      +	//create nodes
      +	var clipboard_info = JSON.parse(data);
      +	var nodes = [];
      +	for(var i = 0; i < clipboard_info.nodes.length; ++i)
      +	{
      +		var node_data = clipboard_info.nodes[i];
      +		var node = LiteGraph.createNode( node_data.type );
      +		if(node)
      +		{
      +			node.configure(node_data);
      +			node.pos[0] += 5;
      +			node.pos[1] += 5;
      +			this.graph.add( node );
      +			nodes.push( node );
      +		}
      +	}
      +
      +	//create links
      +	for(var i = 0; i < clipboard_info.links.length; ++i)
      +	{
      +		var link_info = clipboard_info.links[i];
      +		var origin_node = nodes[ link_info[0] ];
      +		var target_node = nodes[ link_info[2] ];
      +		origin_node.connect( link_info[1], target_node, link_info[3] );
      +	}
      +
      +	this.selectNodes( nodes );
      +}
      +
      +/**
      +* process a item drop event on top the canvas
      +* @method processDrop
      +**/
      +LGraphCanvas.prototype.processDrop = function(e)
      +{
      +	e.preventDefault();
      +	this.adjustMouseEvent(e);
      +
      +
      +	var pos = [e.canvasX,e.canvasY];
      +	var node = this.graph.getNodeOnPos(pos[0],pos[1]);
      +
      +	if(!node)
      +	{
      +		var r = null;
      +		if(this.onDropItem)
      +			r = this.onDropItem( event );
      +		if(!r)
      +			this.checkDropItem(e);
      +		return;
      +	}
      +
      +	if( node.onDropFile || node.onDropData )
      +	{
      +		var files = e.dataTransfer.files;
      +		if(files && files.length)
      +		{
      +			for(var i=0; i < files.length; i++)
      +			{
      +				var file = e.dataTransfer.files[0];
      +				var filename = file.name;
      +				var ext = LGraphCanvas.getFileExtension( filename );
      +				//console.log(file);
      +
      +				if(node.onDropFile)
      +					node.onDropFile(file);
      +
      +				if(node.onDropData)
      +				{
      +					//prepare reader
      +					var reader = new FileReader();
      +					reader.onload = function (event) {
      +						//console.log(event.target);
      +						var data = event.target.result;
      +						node.onDropData( data, filename, file );
      +					};
      +
      +					//read data
      +					var type = file.type.split("/")[0];
      +					if(type == "text" || type == "")
      +						reader.readAsText(file);
      +					else if (type == "image")
      +						reader.readAsDataURL(file);
      +					else
      +						reader.readAsArrayBuffer(file);
      +				}
      +			}
      +		}
      +	}
      +
      +	if(node.onDropItem)
      +	{
      +		if( node.onDropItem( event ) )
      +			return true;
      +	}
      +
      +	if(this.onDropItem)
      +		return this.onDropItem( event );
      +
      +	return false;
      +}
      +
      +//called if the graph doesnt have a default drop item behaviour
      +LGraphCanvas.prototype.checkDropItem = function(e)
      +{
      +	if(e.dataTransfer.files.length)
      +	{
      +		var file = e.dataTransfer.files[0];
      +		var ext = LGraphCanvas.getFileExtension( file.name ).toLowerCase();
      +		var nodetype = LiteGraph.node_types_by_file_extension[ext];
      +		if(nodetype)
      +		{
      +			var node = LiteGraph.createNode( nodetype.type );
      +			node.pos = [e.canvasX, e.canvasY];
      +			this.graph.add( node );
      +			if( node.onDropFile )
      +				node.onDropFile( file );
      +		}
      +	}
      +}
      +
      +
      +LGraphCanvas.prototype.processNodeDblClicked = function(n)
      +{
      +	if(this.onShowNodePanel)
      +		this.onShowNodePanel(n);
      +
      +	if(this.onNodeDblClicked)
      +		this.onNodeDblClicked(n);
      +
      +	this.setDirty(true);
      +}
      +
      +LGraphCanvas.prototype.processNodeSelected = function(node,e)
      +{
      +	this.selectNode( node, e && e.shiftKey );
      +	if(this.onNodeSelected)
      +		this.onNodeSelected(node);
      +}
      +
      +LGraphCanvas.prototype.processNodeDeselected = function(node)
      +{
      +	this.deselectNode(node);
      +	if(this.onNodeDeselected)
      +		this.onNodeDeselected(node);
      +}
      +
      +/**
      +* selects a given node (or adds it to the current selection)
      +* @method selectNode
      +**/
      +LGraphCanvas.prototype.selectNode = function( node, add_to_current_selection )
      +{
      +	if(node == null)
      +		this.deselectAllNodes();
      +	else
      +		this.selectNodes([node], add_to_current_selection );
      +}
      +
      +/**
      +* selects several nodes (or adds them to the current selection)
      +* @method selectNodes
      +**/
      +LGraphCanvas.prototype.selectNodes = function( nodes, add_to_current_selection )
      +{
      +	if(!add_to_current_selection)
      +		this.deselectAllNodes();
      +
      +	nodes = nodes || this.graph._nodes;
      +	for(var i = 0; i < nodes.length; ++i)
      +	{
      +		var node = nodes[i];
      +		if(node.selected)
      +			continue;
      +
      +		if( !node.selected && node.onSelected )
      +			node.onSelected();
      +		node.selected = true;
      +		this.selected_nodes[ node.id ] = node;
      +
      +		if(node.inputs)
      +			for(var i = 0; i < node.inputs.length; ++i)
      +				this.highlighted_links[ node.inputs[i].link ] = true;
      +		if(node.outputs)
      +			for(var i = 0; i < node.outputs.length; ++i)
      +			{
      +				var out = node.outputs[i];
      +				if( out.links )
      +					for(var j = 0; j < out.links.length; ++j)
      +						this.highlighted_links[ out.links[j] ] = true;
      +			}
      +
      +	}
      +
      +	this.setDirty(true);
      +}
      +
      +/**
      +* removes a node from the current selection
      +* @method deselectNode
      +**/
      +LGraphCanvas.prototype.deselectNode = function( node )
      +{
      +	if(!node.selected)
      +		return;
      +	if(node.onDeselected)
      +		node.onDeselected();
      +	node.selected = false;
      +
      +	//remove highlighted
      +	if(node.inputs)
      +		for(var i = 0; i < node.inputs.length; ++i)
      +			delete this.highlighted_links[ node.inputs[i].link ];
      +	if(node.outputs)
      +		for(var i = 0; i < node.outputs.length; ++i)
      +		{
      +			var out = node.outputs[i];
      +			if( out.links )
      +				for(var j = 0; j < out.links.length; ++j)
      +					delete this.highlighted_links[ out.links[j] ];
      +		}
      +}
      +
      +/**
      +* removes all nodes from the current selection
      +* @method deselectAllNodes
      +**/
      +LGraphCanvas.prototype.deselectAllNodes = function()
      +{
      +	if(!this.graph)
      +		return;
      +	var nodes = this.graph._nodes;
      +	for(var i = 0, l = nodes.length; i < l; ++i)
      +	{
      +		var node = nodes[i];
      +		if(!node.selected)
      +			continue;
      +		if(node.onDeselected)
      +			node.onDeselected();
      +		node.selected = false;
      +	}
      +	this.selected_nodes = {};
      +	this.highlighted_links = {};
      +	this.setDirty(true);
      +}
      +
      +/**
      +* deletes all nodes in the current selection from the graph
      +* @method deleteSelectedNodes
      +**/
      +LGraphCanvas.prototype.deleteSelectedNodes = function()
      +{
      +	for(var i in this.selected_nodes)
      +	{
      +		var m = this.selected_nodes[i];
      +		//if(m == this.node_in_panel) this.showNodePanel(null);
      +		this.graph.remove(m);
      +	}
      +	this.selected_nodes = {};
      +	this.highlighted_links = {};
      +	this.setDirty(true);
      +}
      +
      +/**
      +* centers the camera on a given node
      +* @method centerOnNode
      +**/
      +LGraphCanvas.prototype.centerOnNode = function(node)
      +{
      +	this.offset[0] = -node.pos[0] - node.size[0] * 0.5 + (this.canvas.width * 0.5 / this.scale);
      +	this.offset[1] = -node.pos[1] - node.size[1] * 0.5 + (this.canvas.height * 0.5 / this.scale);
      +	this.setDirty(true,true);
      +}
      +
      +/**
      +* adds some useful properties to a mouse event, like the position in graph coordinates
      +* @method adjustMouseEvent
      +**/
      +LGraphCanvas.prototype.adjustMouseEvent = function(e)
      +{
      +	if(this.canvas)
      +	{
      +		var b = this.canvas.getBoundingClientRect();
      +		e.localX = e.pageX - b.left;
      +		e.localY = e.pageY - b.top;
      +	}
      +	else
      +	{
      +		e.localX = e.pageX;
      +		e.localY = e.pageY;
      +	}
      +
      +	e.deltaX = e.localX - this.last_mouse_position[0];
      +	e.deltaY = e.localY - this.last_mouse_position[1];
      +
      +	this.last_mouse_position[0] = e.localX;
      +	this.last_mouse_position[1] = e.localY;
      +
      +	e.canvasX = e.localX / this.scale - this.offset[0];
      +	e.canvasY = e.localY / this.scale - this.offset[1];
      +}
      +
      +/**
      +* changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom
      +* @method setZoom
      +**/
      +LGraphCanvas.prototype.setZoom = function(value, zooming_center)
      +{
      +	if(!zooming_center && this.canvas)
      +		zooming_center = [this.canvas.width * 0.5,this.canvas.height * 0.5];
      +
      +	var center = this.convertOffsetToCanvas( zooming_center );
      +
      +	this.scale = value;
      +
      +	if(this.scale > this.max_zoom)
      +		this.scale = this.max_zoom;
      +	else if(this.scale < this.min_zoom)
      +		this.scale = this.min_zoom;
      +
      +	var new_center = this.convertOffsetToCanvas( zooming_center );
      +	var delta_offset = [new_center[0] - center[0], new_center[1] - center[1]];
      +
      +	this.offset[0] += delta_offset[0];
      +	this.offset[1] += delta_offset[1];
      +
      +	this.dirty_canvas = true;
      +	this.dirty_bgcanvas = true;
      +}
      +
      +/**
      +* converts a coordinate in canvas2D space to graphcanvas space (NAME IS CONFUSION, SHOULD BE THE OTHER WAY AROUND)
      +* @method convertOffsetToCanvas
      +**/
      +LGraphCanvas.prototype.convertOffsetToCanvas = function( pos, out )
      +{
      +	out = out || [];
      +	out[0] = pos[0] / this.scale - this.offset[0];
      +	out[1] = pos[1] / this.scale - this.offset[1];
      +	return out;
      +}
      +
      +/**
      +* converts a coordinate in graphcanvas space to canvas2D space (NAME IS CONFUSION, SHOULD BE THE OTHER WAY AROUND)
      +* @method convertCanvasToOffset
      +**/
      +LGraphCanvas.prototype.convertCanvasToOffset = function( pos, out )
      +{
      +	out = out || [];
      +	out[0] = (pos[0] + this.offset[0]) * this.scale;
      +	out[1] = (pos[1] + this.offset[1]) * this.scale;
      +	return out;
      +}
      +
      +LGraphCanvas.prototype.convertEventToCanvas = function(e)
      +{
      +	var rect = this.canvas.getBoundingClientRect();
      +	return this.convertOffsetToCanvas([e.pageX - rect.left,e.pageY - rect.top]);
      +}
      +
      +/**
      +* brings a node to front (above all other nodes)
      +* @method bringToFront
      +**/
      +LGraphCanvas.prototype.bringToFront = function(node)
      +{
      +	var i = this.graph._nodes.indexOf(node);
      +	if(i == -1) return;
      +
      +	this.graph._nodes.splice(i,1);
      +	this.graph._nodes.push(node);
      +}
      +
      +/**
      +* sends a node to the back (below all other nodes)
      +* @method sendToBack
      +**/
      +LGraphCanvas.prototype.sendToBack = function(node)
      +{
      +	var i = this.graph._nodes.indexOf(node);
      +	if(i == -1) return;
      +
      +	this.graph._nodes.splice(i,1);
      +	this.graph._nodes.unshift(node);
      +}
      +
      +/* Interaction */
      +
      +
      +
      +/* LGraphCanvas render */
      +var temp = new Float32Array(4);
      +
      +/**
      +* checks which nodes are visible (inside the camera area)
      +* @method computeVisibleNodes
      +**/
      +LGraphCanvas.prototype.computeVisibleNodes = function( nodes, out )
      +{
      +	var visible_nodes = out || [];
      +	visible_nodes.length = 0;
      +	nodes = nodes || this.graph._nodes;
      +	for(var i = 0, l = nodes.length; i < l; ++i)
      +	{
      +		var n = nodes[i];
      +
      +		//skip rendering nodes in live mode
      +		if(this.live_mode && !n.onDrawBackground && !n.onDrawForeground)
      +			continue;
      +
      +		if(!overlapBounding( this.visible_area, n.getBounding( temp ) ))
      +			continue; //out of the visible area
      +
      +		visible_nodes.push(n);
      +	}
      +	return visible_nodes;
      +}
      +
      +/**
      +* renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes)
      +* @method draw
      +**/
      +LGraphCanvas.prototype.draw = function(force_canvas, force_bgcanvas)
      +{
      +	if(!this.canvas)
      +		return;
      +
      +	//fps counting
      +	var now = LiteGraph.getTime();
      +	this.render_time = (now - this.last_draw_time)*0.001;
      +	this.last_draw_time = now;
      +
      +	if(this.graph)
      +	{
      +		var start = [-this.offset[0], -this.offset[1] ];
      +		var end = [start[0] + this.canvas.width / this.scale, start[1] + this.canvas.height / this.scale];
      +		this.visible_area = new Float32Array([ start[0], start[1], end[0] - start[0], end[1] - start[1] ]);
      +	}
      +
      +	if(this.dirty_bgcanvas || force_bgcanvas || this.always_render_background || (this.graph && this.graph._last_trigger_time && (now - this.graph._last_trigger_time) < 1000) )
      +		this.drawBackCanvas();
      +
      +	if(this.dirty_canvas || force_canvas)
      +		this.drawFrontCanvas();
      +
      +	this.fps = this.render_time ? (1.0 / this.render_time) : 0;
      +	this.frame += 1;
      +}
      +
      +/**
      +* draws the front canvas (the one containing all the nodes)
      +* @method drawFrontCanvas
      +**/
      +LGraphCanvas.prototype.drawFrontCanvas = function()
      +{
      +	this.dirty_canvas = false;
      +
      +	if(!this.ctx)
      +		this.ctx = this.bgcanvas.getContext("2d");
      +	var ctx = this.ctx;
      +	if(!ctx) //maybe is using webgl...
      +		return;
      +
      +	if(ctx.start2D)
      +		ctx.start2D();
      +
      +	var canvas = this.canvas;
      +
      +	//reset in case of error
      +	ctx.restore();
      +	ctx.setTransform(1, 0, 0, 1, 0, 0);
      +
      +	//clip dirty area if there is one, otherwise work in full canvas
      +	if(this.dirty_area)
      +	{
      +		ctx.save();
      +		ctx.beginPath();
      +		ctx.rect(this.dirty_area[0],this.dirty_area[1],this.dirty_area[2],this.dirty_area[3]);
      +		ctx.clip();
      +	}
      +
      +	//clear
      +	//canvas.width = canvas.width;
      +	if(this.clear_background)
      +		ctx.clearRect(0,0,canvas.width, canvas.height);
      +
      +	//draw bg canvas
      +	if(this.bgcanvas == this.canvas)
      +		this.drawBackCanvas();
      +	else
      +		ctx.drawImage(this.bgcanvas,0,0);
      +
      +	//rendering
      +	if(this.onRender)
      +		this.onRender(canvas, ctx);
      +
      +	//info widget
      +	if(this.show_info)
      +		this.renderInfo(ctx);
      +
      +	if(this.graph)
      +	{
      +		//apply transformations
      +		ctx.save();
      +		ctx.scale(this.scale,this.scale);
      +		ctx.translate( this.offset[0],this.offset[1] );
      +
      +		//draw nodes
      +		var drawn_nodes = 0;
      +		var visible_nodes = this.computeVisibleNodes( null, this.visible_nodes );
      +
      +		for (var i = 0; i < visible_nodes.length; ++i)
      +		{
      +			var node = visible_nodes[i];
      +
      +			//transform coords system
      +			ctx.save();
      +			ctx.translate( node.pos[0], node.pos[1] );
      +
      +			//Draw
      +			this.drawNode( node, ctx );
      +			drawn_nodes += 1;
      +
      +			//Restore
      +			ctx.restore();
      +		}
      +
      +		//connections ontop?
      +		if(this.graph.config.links_ontop)
      +			if(!this.live_mode)
      +				this.drawConnections(ctx);
      +
      +		//current connection
      +		if(this.connecting_pos != null)
      +		{
      +			ctx.lineWidth = this.connections_width;
      +			var link_color = null;
      +			switch( this.connecting_output.type )
      +			{
      +				case LiteGraph.EVENT: link_color = "#F85"; break;
      +				default:
      +					link_color = "#AFA";
      +			}
      +			//the connection being dragged by the mouse
      +			this.renderLink( ctx, this.connecting_pos, [this.canvas_mouse[0],this.canvas_mouse[1]], null, false, null, link_color );
      +
      +			ctx.beginPath();
      +				if( this.connecting_output.type === LiteGraph.EVENT || this.connecting_output.shape === LiteGraph.BOX_SHAPE )
      +					ctx.rect( (this.connecting_pos[0] - 6) + 0.5, (this.connecting_pos[1] - 5) + 0.5,14,10);
      +				else
      +					ctx.arc( this.connecting_pos[0], this.connecting_pos[1],4,0,Math.PI*2);
      +			ctx.fill();
      +
      +			ctx.fillStyle = "#ffcc00";
      +			if(this._highlight_input)
      +			{
      +				ctx.beginPath();
      +					ctx.arc( this._highlight_input[0], this._highlight_input[1],6,0,Math.PI*2);
      +				ctx.fill();
      +			}
      +		}
      +
      +		if( this.dragging_rectangle )
      +		{
      +			ctx.strokeStyle = "#FFF";
      +			ctx.strokeRect( this.dragging_rectangle[0], this.dragging_rectangle[1], this.dragging_rectangle[2], this.dragging_rectangle[3] );
      +		}
      +
      +
      +		ctx.restore();
      +	}
      +
      +	if(this.dirty_area)
      +	{
      +		ctx.restore();
      +		//this.dirty_area = null;
      +	}
      +
      +	if(ctx.finish2D) //this is a function I use in webgl renderer
      +		ctx.finish2D();
      +}
      +
      +/**
      +* draws some useful stats in the corner of the canvas
      +* @method renderInfo
      +**/
      +LGraphCanvas.prototype.renderInfo = function( ctx, x, y )
      +{
      +	x = x || 0;
      +	y = y || 0;
      +
      +	ctx.save();
      +	ctx.translate( x, y );
      +
      +	ctx.font = "10px Arial";
      +	ctx.fillStyle = "#888";
      +	if(this.graph)
      +	{
      +		ctx.fillText( "T: " + this.graph.globaltime.toFixed(2)+"s",5,13*1 );
      +		ctx.fillText( "I: " + this.graph.iteration,5,13*2 );
      +		ctx.fillText( "V: " + this.graph._version,5,13*3 );
      +		ctx.fillText( "FPS:" + this.fps.toFixed(2),5,13*4 );
      +	}
      +	else
      +		ctx.fillText( "No graph selected",5,13*1 );
      +	ctx.restore();
      +}
      +
      +/**
      +* draws the back canvas (the one containing the background and the connections)
      +* @method drawBackCanvas
      +**/
      +LGraphCanvas.prototype.drawBackCanvas = function()
      +{
      +	var canvas = this.bgcanvas;
      +	if(canvas.width != this.canvas.width ||
      +		canvas.height != this.canvas.height)
      +	{
      +		canvas.width = this.canvas.width;
      +		canvas.height = this.canvas.height;
      +	}
      +
      +	if(!this.bgctx)
      +		this.bgctx = this.bgcanvas.getContext("2d");
      +	var ctx = this.bgctx;
      +	if(ctx.start)
      +		ctx.start();
      +
      +	//clear
      +	if(this.clear_background)
      +		ctx.clearRect(0,0,canvas.width, canvas.height);
      +
      +	if(this._graph_stack && this._graph_stack.length)
      +	{
      +		ctx.strokeStyle = this._graph_stack[ this._graph_stack.length - 1].bgcolor;
      +		ctx.lineWidth = 10;
      +		ctx.strokeRect(1,1,canvas.width-2,canvas.height-2);
      +		ctx.lineWidth = 1;
      +	}
      +
      +	var bg_already_painted = false;
      +	if(this.onRenderBackground)
      +		bg_already_painted = this.onRenderBackground( canvas, ctx );
      +
      +	//reset in case of error
      +	ctx.restore();
      +	ctx.setTransform(1, 0, 0, 1, 0, 0);
      +
      +	if(this.graph)
      +	{
      +		//apply transformations
      +		ctx.save();
      +		ctx.scale(this.scale,this.scale);
      +		ctx.translate(this.offset[0],this.offset[1]);
      +
      +		//render BG
      +		if(this.background_image && this.scale > 0.5 && !bg_already_painted)
      +		{
      +			if (this.zoom_modify_alpha)
      +				ctx.globalAlpha = (1.0 - 0.5 / this.scale) * this.editor_alpha;
      +			else
      +				ctx.globalAlpha = this.editor_alpha;
      +			ctx.imageSmoothingEnabled = ctx.mozImageSmoothingEnabled = ctx.imageSmoothingEnabled = false;
      +			if(!this._bg_img || this._bg_img.name != this.background_image)
      +			{
      +				this._bg_img = new Image();
      +				this._bg_img.name = this.background_image;
      +				this._bg_img.src = this.background_image;
      +				var that = this;
      +				this._bg_img.onload = function() {
      +					that.draw(true,true);
      +				}
      +			}
      +
      +			var pattern = null;
      +			if(this._pattern == null && this._bg_img.width > 0)
      +			{
      +				pattern = ctx.createPattern( this._bg_img, 'repeat' );
      +				this._pattern_img = this._bg_img;
      +				this._pattern = pattern;
      +			}
      +			else
      +				pattern = this._pattern;
      +			if(pattern)
      +			{
      +				ctx.fillStyle = pattern;
      +				ctx.fillRect(this.visible_area[0],this.visible_area[1],this.visible_area[2],this.visible_area[3]);
      +				ctx.fillStyle = "transparent";
      +			}
      +
      +			ctx.globalAlpha = 1.0;
      +			ctx.imageSmoothingEnabled = ctx.mozImageSmoothingEnabled = ctx.imageSmoothingEnabled = true;
      +		}
      +
      +		//groups
      +		if(this.graph._groups.length && !this.live_mode)
      +			this.drawGroups(canvas, ctx);
      +
      +		if(this.onBackgroundRender)
      +			this.onBackgroundRender(canvas, ctx);
      +
      +		//DEBUG: show clipping area
      +		//ctx.fillStyle = "red";
      +		//ctx.fillRect( this.visible_area[0] + 10, this.visible_area[1] + 10, this.visible_area[2] - 20, this.visible_area[3] - 20);
      +
      +		//bg
      +		if (this.render_canvas_border) {
      +			ctx.strokeStyle = "#235";
      +			ctx.strokeRect(0,0,canvas.width,canvas.height);
      +		}
      +
      +		if(this.render_connections_shadows)
      +		{
      +			ctx.shadowColor = "#000";
      +			ctx.shadowOffsetX = 0;
      +			ctx.shadowOffsetY = 0;
      +			ctx.shadowBlur = 6;
      +		}
      +		else
      +			ctx.shadowColor = "rgba(0,0,0,0)";
      +
      +		//draw connections
      +		if(!this.live_mode)
      +			this.drawConnections(ctx);
      +
      +		ctx.shadowColor = "rgba(0,0,0,0)";
      +
      +		//restore state
      +		ctx.restore();
      +	}
      +
      +	if(ctx.finish)
      +		ctx.finish();
      +
      +	this.dirty_bgcanvas = false;
      +	this.dirty_canvas = true; //to force to repaint the front canvas with the bgcanvas
      +}
      +
      +var temp_vec2 = new Float32Array(2);
      +
      +/**
      +* draws the given node inside the canvas
      +* @method drawNode
      +**/
      +LGraphCanvas.prototype.drawNode = function(node, ctx )
      +{
      +	var glow = false;
      +	this.current_node = node;
      +
      +	var color = node.color || node.constructor.color || LiteGraph.NODE_DEFAULT_COLOR;
      +	var bgcolor = node.bgcolor || node.constructor.bgcolor || LiteGraph.NODE_DEFAULT_BGCOLOR;
      +
      +	//shadow and glow
      +	if (node.mouseOver)
      +		glow = true;
      +
      +	if(node.selected)
      +	{
      +		/*
      +		ctx.shadowColor = "#EEEEFF";//glow ? "#AAF" : "#000";
      +		ctx.shadowOffsetX = 0;
      +		ctx.shadowOffsetY = 0;
      +		ctx.shadowBlur = 1;
      +		*/
      +	}
      +	else if(this.render_shadows)
      +	{
      +		ctx.shadowColor = "rgba(0,0,0,0.5)";
      +		ctx.shadowOffsetX = 2;
      +		ctx.shadowOffsetY = 2;
      +		ctx.shadowBlur = 3;
      +	}
      +	else
      +		ctx.shadowColor = "transparent";
      +
      +	//only render if it forces it to do it
      +	if(this.live_mode)
      +	{
      +		if(!node.flags.collapsed)
      +		{
      +			ctx.shadowColor = "transparent";
      +			if(node.onDrawForeground)
      +				node.onDrawForeground(ctx, this);
      +		}
      +
      +		return;
      +	}
      +
      +	//custom draw collapsed method
      +	if(node.flags.collapsed && node.onDrawCollaped && node.onDrawCollapsed(ctx, this) == true)
      +		return;
      +
      +	var editor_alpha = this.editor_alpha;
      +	ctx.globalAlpha = editor_alpha;
      +
      +	//clip if required (mask)
      +	var shape = node._shape || LiteGraph.BOX_SHAPE;
      +	var size = temp_vec2;
      +	temp_vec2.set( node.size );
      +	if( node.flags.collapsed )
      +	{
      +		ctx.font = this.inner_text_font;
      +		var title = node.getTitle ? node.getTitle() : node.title;
      +		node._collapsed_width = Math.min( node.size[0], ctx.measureText(title).width + 40 );//LiteGraph.NODE_COLLAPSED_WIDTH;
      +		size[0] = node._collapsed_width;
      +		size[1] = 0;
      +	}
      +	
      +	if( node.flags.clip_area ) //Start clipping
      +	{
      +		ctx.save();
      +		ctx.beginPath();
      +		if(shape == LiteGraph.BOX_SHAPE)
      +			ctx.rect(0,0,size[0], size[1]);
      +		else if (shape == LiteGraph.ROUND_SHAPE)
      +			ctx.roundRect(0,0,size[0], size[1],10);
      +		else if (shape == LiteGraph.CIRCLE_SHAPE)
      +			ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5, 0, Math.PI*2);
      +		ctx.clip();
      +	}
      +
      +	//draw shape
      +	this.drawNodeShape( node, ctx, size, color, bgcolor, node.selected, node.mouseOver );
      +	ctx.shadowColor = "transparent";
      +
      +	//connection slots
      +	ctx.textAlign = "left";
      +	ctx.font = this.inner_text_font;
      +
      +	var render_text = this.scale > 0.6;
      +
      +	var out_slot = this.connecting_output;
      +	ctx.lineWidth = 1;
      +
      +	var max_y = 0;
      +
      +	//render inputs and outputs
      +	if(!node.flags.collapsed)
      +	{
      +		//input connection slots
      +		if(node.inputs)
      +			for(var i = 0; i < node.inputs.length; i++)
      +			{
      +				var slot = node.inputs[i];
      +
      +				ctx.globalAlpha = editor_alpha;
      +				//change opacity of incompatible slots when dragging a connection
      +				if ( this.connecting_node && LiteGraph.isValidConnection( slot.type && out_slot.type ) )
      +					ctx.globalAlpha = 0.4 * editor_alpha;
      +
      +				ctx.fillStyle = slot.link != null ? (slot.colorOn || this.default_connection_color.input_on) : (slot.colorOff || this.default_connection_color.input_off);
      +
      +				var pos = node.getConnectionPos( true, i );
      +				pos[0] -= node.pos[0];
      +				pos[1] -= node.pos[1];
      +				if( max_y < pos[1] + LiteGraph.NODE_SLOT_HEIGHT*0.5 )
      +					max_y = pos[1] + LiteGraph.NODE_SLOT_HEIGHT*0.5;
      +
      +				ctx.beginPath();
      +
      +				if (slot.type === LiteGraph.EVENT || slot.shape === LiteGraph.BOX_SHAPE) {
      +                    ctx.rect((pos[0] - 6) + 0.5, (pos[1] - 5) + 0.5, 14, 10);
      +                } else if (slot.shape === LiteGraph.ARROW_SHAPE) {
      +                    ctx.moveTo(pos[0] + 8, pos[1] + 0.5);
      +                    ctx.lineTo(pos[0] - 4, (pos[1] + 6) + 0.5);
      +                    ctx.lineTo(pos[0] - 4, (pos[1] - 6) + 0.5);
      +                    ctx.closePath();
      +                } else {
      +                    ctx.arc(pos[0], pos[1], 4, 0, Math.PI * 2);
      +                }
      +
      +				ctx.fill();
      +
      +				//render name
      +				if(render_text)
      +				{
      +					var text = slot.label != null ? slot.label : slot.name;
      +					if(text)
      +					{
      +						ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR;
      +						ctx.fillText(text,pos[0] + 10,pos[1] + 5);
      +					}
      +				}
      +			}
      +
      +		//output connection slots
      +		if(this.connecting_node)
      +			ctx.globalAlpha = 0.4 * editor_alpha;
      +
      +		ctx.textAlign = "right";
      +		ctx.strokeStyle = "black";
      +		if(node.outputs)
      +			for(var i = 0; i < node.outputs.length; i++)
      +			{
      +				var slot = node.outputs[i];
      +
      +				var pos = node.getConnectionPos(false,i);
      +				pos[0] -= node.pos[0];
      +				pos[1] -= node.pos[1];
      +				if( max_y < pos[1] + LiteGraph.NODE_SLOT_HEIGHT*0.5)
      +					max_y = pos[1] + LiteGraph.NODE_SLOT_HEIGHT*0.5;
      +
      +				ctx.fillStyle = slot.links && slot.links.length ? (slot.colorOn || this.default_connection_color.output_on) : (slot.colorOff || this.default_connection_color.output_off);
      +				ctx.beginPath();
      +				//ctx.rect( node.size[0] - 14,i*14,10,10);
      +
      +				if (slot.type === LiteGraph.EVENT || slot.shape === LiteGraph.BOX_SHAPE) {
      +					ctx.rect((pos[0] - 6) + 0.5,(pos[1] - 5) + 0.5,14,10);
      +                } else if (slot.shape === LiteGraph.ARROW_SHAPE) {
      +                    ctx.moveTo(pos[0] + 8, pos[1] + 0.5);
      +                    ctx.lineTo(pos[0] - 4, (pos[1] + 6) + 0.5);
      +                    ctx.lineTo(pos[0] - 4, (pos[1] - 6) + 0.5);
      +                    ctx.closePath();
      +                } else {
      +                    ctx.arc(pos[0], pos[1], 4, 0, Math.PI * 2);
      +                }
      +
      +				//trigger
      +				//if(slot.node_id != null && slot.slot == -1)
      +				//	ctx.fillStyle = "#F85";
      +
      +				//if(slot.links != null && slot.links.length)
      +				ctx.fill();
      +				ctx.stroke();
      +
      +				//render output name
      +				if(render_text)
      +				{
      +					var text = slot.label != null ? slot.label : slot.name;
      +					if(text)
      +					{
      +						ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR;
      +						ctx.fillText(text, pos[0] - 10,pos[1] + 5);
      +					}
      +				}
      +			}
      +
      +		ctx.textAlign = "left";
      +		ctx.globalAlpha = 1;
      +
      +		if(node.widgets)
      +			this.drawNodeWidgets( node, max_y, ctx, (this.node_widget && this.node_widget[0] == node) ? this.node_widget[1] : null );
      +
      +		//draw foreground
      +		if(node.onDrawForeground)
      +		{
      +			//immediate gui stuff
      +			if( node.gui_rects )
      +				node.gui_rects.length = 0;
      +			node.onDrawForeground( ctx, this );
      +		}
      +	}
      +	else //if collapsed
      +	{
      +		if(node.inputs)
      +		{
      +			for(var i = 0; i < node.inputs.length; i++)
      +			{
      +				var slot = node.inputs[i];
      +				if( slot.link == null )
      +					continue;
      +				ctx.fillStyle = slot.colorOn || this.default_connection_color.input_on;
      +				ctx.beginPath();
      +				if ( slot.type === LiteGraph.EVENT || slot.shape === LiteGraph.BOX_SHAPE) {
      +					ctx.rect(0.5, 4 - LiteGraph.NODE_TITLE_HEIGHT + 0.5,14,LiteGraph.NODE_TITLE_HEIGHT - 8);
      +                } else if (slot.shape === LiteGraph.ARROW_SHAPE) {
      +                    ctx.moveTo(8, LiteGraph.NODE_TITLE_HEIGHT * -0.5);
      +                    ctx.lineTo(-4, LiteGraph.NODE_TITLE_HEIGHT * -0.8);
      +                    ctx.lineTo(-4, LiteGraph.NODE_TITLE_HEIGHT * -0.2);
      +                    ctx.closePath();
      +                } else {
      +                    ctx.arc(0, LiteGraph.NODE_TITLE_HEIGHT * -0.5, 4, 0, Math.PI * 2);
      +                }
      +				ctx.fill();
      +				break;
      +			}
      +		}
      +
      +		if(node.outputs)
      +		{
      +			for(var i = 0; i < node.outputs.length; i++)
      +			{
      +				var slot = node.outputs[i];
      +				if(!slot.links || !slot.links.length)
      +					continue;
      +				ctx.fillStyle = slot.colorOn || this.default_connection_color.output_on;
      +				ctx.strokeStyle = "black";
      +				ctx.beginPath();
      +				if (slot.type === LiteGraph.EVENT || slot.shape === LiteGraph.BOX_SHAPE) {
      +					ctx.rect( node._collapsed_width - 4 + 0.5, 4 - LiteGraph.NODE_TITLE_HEIGHT + 0.5,14,LiteGraph.NODE_TITLE_HEIGHT - 8);
      +                } else if (slot.shape === LiteGraph.ARROW_SHAPE) {
      +                    ctx.moveTo(node._collapsed_width + 6, LiteGraph.NODE_TITLE_HEIGHT * -0.5);
      +                    ctx.lineTo(node._collapsed_width - 6, LiteGraph.NODE_TITLE_HEIGHT * -0.8);
      +                    ctx.lineTo(node._collapsed_width - 6, LiteGraph.NODE_TITLE_HEIGHT * -0.2);
      +                    ctx.closePath();
      +                } else {
      +                    ctx.arc(node._collapsed_width, LiteGraph.NODE_TITLE_HEIGHT * -0.5, 4, 0, Math.PI * 2);
      +                }
      +				ctx.fill();
      +				ctx.stroke();
      +			}
      +		}
      +		
      +	}
      +
      +	if(node.flags.clip_area)
      +		ctx.restore();
      +
      +	ctx.globalAlpha = 1.0;
      +}
      +
      +/**
      +* draws the shape of the given node in the canvas
      +* @method drawNodeShape
      +**/
      +LGraphCanvas.prototype.drawNodeShape = function( node, ctx, size, fgcolor, bgcolor, selected, mouse_over )
      +{
      +	//bg rect
      +	ctx.strokeStyle = fgcolor;
      +	ctx.fillStyle = bgcolor;
      +
      +	var title_height = LiteGraph.NODE_TITLE_HEIGHT;
      +
      +	//render node area depending on shape
      +	var shape = node._shape || node.constructor.shape || LiteGraph.BOX_SHAPE;
      +	var title_mode = node.constructor.title_mode;
      +
      +	var render_title = true;
      +	if( title_mode == LiteGraph.TRANSPARENT_TITLE )
      +		render_title = false;
      +	else if( title_mode == LiteGraph.AUTOHIDE_TITLE && mouse_over)
      +		render_title = true;
      +
      +	var areax = 0;
      +	var areay = render_title ? -title_height : 0;
      +	var areaw = size[0]+1;
      +	var areah = render_title ? size[1] + title_height : size[1];
      +
      +	if(!node.flags.collapsed)
      +	{
      +		if(shape == LiteGraph.BOX_SHAPE || this.scale < 0.5)
      +		{
      +			ctx.beginPath();
      +			ctx.rect( areax, areay, areaw, areah );
      +			ctx.fill();
      +		}
      +		else if (shape == LiteGraph.ROUND_SHAPE || shape == LiteGraph.CARD_SHAPE)
      +		{
      +			ctx.beginPath();
      +			ctx.roundRect( areax, areay, areaw, areah, this.round_radius, shape == LiteGraph.CARD_SHAPE ? 0 : this.round_radius);
      +			ctx.fill();
      +		}
      +		else if (shape == LiteGraph.CIRCLE_SHAPE)
      +		{
      +			ctx.beginPath();
      +			ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5, 0, Math.PI*2);
      +			ctx.fill();
      +		}
      +	}
      +	ctx.shadowColor = "transparent";
      +
      +	//image
      +	if (node.bgImage && node.bgImage.width)
      +		ctx.drawImage( node.bgImage, (size[0] - node.bgImage.width) * 0.5 , (size[1] - node.bgImage.height) * 0.5);
      +
      +	if(node.bgImageUrl && !node.bgImage)
      +		node.bgImage = node.loadImage(node.bgImageUrl);
      +
      +	if( node.onDrawBackground )
      +	{
      +		//immediate gui stuff
      +		if( node.gui_rects )
      +			node.gui_rects.length = 0;
      +		node.onDrawBackground( ctx, this );
      +	}
      +
      +	//title bg (remember, it is rendered ABOVE the node)
      +	if(render_title || title_mode == LiteGraph.TRANSPARENT_TITLE )
      +	{
      +		//title bar
      +		if(title_mode != LiteGraph.TRANSPARENT_TITLE) //!node.flags.collapsed)
      +		{
      +			//* gradient test
      +			if(this.use_gradients)
      +			{
      +				var grad = LGraphCanvas.gradients[ fgcolor ];
      +				if(!grad)
      +				{
      +					grad = LGraphCanvas.gradients[ fgcolor ] = ctx.createLinearGradient(0,0,400,0);
      +					grad.addColorStop(0, fgcolor);
      +					grad.addColorStop(1, "#000");
      +				}
      +				ctx.fillStyle = grad;
      +			}
      +			else
      +				ctx.fillStyle = fgcolor;
      +
      +			var old_alpha = ctx.globalAlpha;
      +			//ctx.globalAlpha = 0.5 * old_alpha;
      +			ctx.beginPath();
      +			if(shape == LiteGraph.BOX_SHAPE || this.scale < 0.5)
      +			{
      +				ctx.rect(0, -title_height, size[0]+1, title_height);
      +				ctx.fill()
      +				//ctx.stroke();
      +			}
      +			else if (shape == LiteGraph.ROUND_SHAPE || shape == LiteGraph.CARD_SHAPE)
      +			{
      +				ctx.roundRect(0,-title_height,size[0]+1, title_height, this.round_radius, node.flags.collapsed ? this.round_radius : 0);
      +				ctx.fill();
      +			}
      +
      +			/*
      +			else if (shape == LiteGraph.CIRCLE_SHAPE)
      +			{
      +				ctx.beginPath();
      +				ctx.arc(title_height *0.5, title_height * -0.5, (title_height - 6) *0.5,0,Math.PI*2);
      +				ctx.fill();
      +			}
      +			*/
      +		}
      +
      +		//title box
      +		ctx.fillStyle = node.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR;
      +		ctx.beginPath();
      +		if (shape == LiteGraph.ROUND_SHAPE || shape == LiteGraph.CIRCLE_SHAPE || shape == LiteGraph.CARD_SHAPE)
      +			ctx.arc(title_height *0.5, title_height * -0.5, (title_height - 8) *0.5,0,Math.PI*2);
      +		else
      +			ctx.rect(4,-title_height + 4,title_height - 8,title_height - 8);
      +		ctx.fill();
      +		ctx.globalAlpha = old_alpha;
      +
      +		//title text
      +		if( this.scale > 0.5 )
      +		{
      +			ctx.font = this.title_text_font;
      +			var title = node.getTitle();
      +			if(title)
      +			{
      +				if(selected)
      +					ctx.fillStyle = "white";
      +				else
      +					ctx.fillStyle = node.constructor.title_text_color || this.node_title_color;
      +				if( node.flags.collapsed )
      +				{
      +					ctx.textAlign =  "center";
      +					var measure = ctx.measureText(title);
      +					ctx.fillText( title, title_height + measure.width * 0.5, -title_height * 0.2 );
      +					ctx.textAlign =  "left";
      +				}
      +				else
      +				{
      +					ctx.textAlign =  "left";
      +					ctx.fillText( title, title_height, -title_height * 0.2 );
      +				}
      +			}
      +		}
      +	}
      +
      +	//render selection marker
      +	if(selected)
      +	{
      +		if( title_mode == LiteGraph.TRANSPARENT_TITLE )
      +		{
      +			areay -= title_height;
      +			areah += title_height;
      +		}
      +		ctx.lineWidth = 1;
      +		ctx.beginPath();
      +		if(shape == LiteGraph.BOX_SHAPE)
      +			ctx.rect(-6 + areax,-6 + areay, 12 + areaw, 12 + areah );
      +		else if (shape == LiteGraph.ROUND_SHAPE || (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed) )
      +			ctx.roundRect(-6 + areax,-6 + areay, 12 + areaw, 12 + areah , this.round_radius * 2);
      +		else if (shape == LiteGraph.CARD_SHAPE)
      +			ctx.roundRect(-6 + areax,-6 + areay, 12 + areaw, 12 + areah , this.round_radius * 2, 2);
      +		else if (shape == LiteGraph.CIRCLE_SHAPE)
      +			ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI*2);
      +		ctx.strokeStyle = "#DDD";
      +		ctx.stroke();
      +		ctx.strokeStyle = fgcolor;
      +	}
      +}
      +
      +/**
      +* draws every connection visible in the canvas
      +* OPTIMIZE THIS: precatch connections position instead of recomputing them every time
      +* @method drawConnections
      +**/
      +LGraphCanvas.prototype.drawConnections = function(ctx)
      +{
      +	var now = LiteGraph.getTime();
      +
      +	//draw connections
      +	ctx.lineWidth = this.connections_width;
      +
      +	ctx.fillStyle = "#AAA";
      +	ctx.strokeStyle = "#AAA";
      +	ctx.globalAlpha = this.editor_alpha;
      +	//for every node
      +	for (var n = 0, l = this.graph._nodes.length; n < l; ++n)
      +	{
      +		var node = this.graph._nodes[n];
      +		//for every input (we render just inputs because it is easier as every slot can only have one input)
      +		if(node.inputs && node.inputs.length)
      +			for(var i = 0; i < node.inputs.length; ++i)
      +			{
      +				var input = node.inputs[i];
      +				if(!input || input.link == null)
      +					continue;
      +				var link_id = input.link;
      +				var link = this.graph.links[ link_id ];
      +				if(!link)
      +					continue;
      +
      +				var start_node = this.graph.getNodeById( link.origin_id );
      +				if(start_node == null) continue;
      +				var start_node_slot = link.origin_slot;
      +				var start_node_slotpos = null;
      +
      +				if(start_node_slot == -1)
      +					start_node_slotpos = [start_node.pos[0] + 10, start_node.pos[1] + 10];
      +				else
      +					start_node_slotpos = start_node.getConnectionPos(false, start_node_slot);
      +
      +				this.renderLink( ctx, start_node_slotpos, node.getConnectionPos(true,i), link );
      +
      +				//event triggered rendered on top
      +				if(link && link._last_time && (now - link._last_time) < 1000 )
      +				{
      +					var f = 2.0 - (now - link._last_time) * 0.002;
      +					var color = "rgba(255,255,255, " + f.toFixed(2) + ")";
      +					this.renderLink( ctx, start_node_slotpos, node.getConnectionPos(true,i), link, true, f, color );
      +				}
      +			}
      +	}
      +	ctx.globalAlpha = 1;
      +}
      +
      +/**
      +* draws a link between two points
      +* @method renderLink
      +**/
      +LGraphCanvas.prototype.renderLink = function( ctx, a, b, link, skip_border, flow, color )
      +{
      +	if(!this.highquality_render)
      +	{
      +		ctx.beginPath();
      +		ctx.moveTo(a[0],a[1]);
      +		ctx.lineTo(b[0],b[1]);
      +		ctx.stroke();
      +		return;
      +	}
      +
      +	var dist = distance(a,b);
      +
      +	if(this.render_connections_border && this.scale > 0.6)
      +		ctx.lineWidth = this.connections_width + 4;
      +
      +	//choose color
      +	if( !color && link )
      +		color = LGraphCanvas.link_type_colors[ link.type ];
      +	if(!color)
      +		color = this.default_link_color;
      +
      +	if( link != null && this.highlighted_links[ link.id ] )
      +		color = "#FFF";
      +
      +	//begin line shape
      +	ctx.beginPath();
      +
      +	if(this.render_curved_connections) //splines
      +	{
      +		ctx.moveTo(a[0],a[1]);
      +		ctx.bezierCurveTo(a[0] + dist*0.25, a[1],
      +							b[0] - dist*0.25 , b[1],
      +							b[0] ,b[1] );
      +	}
      +	else //lines
      +	{
      +		ctx.moveTo(a[0]+10,a[1]);
      +		ctx.lineTo(((a[0]+10) + (b[0]-10))*0.5,a[1]);
      +		ctx.lineTo(((a[0]+10) + (b[0]-10))*0.5,b[1]);
      +		ctx.lineTo(b[0]-10,b[1]);
      +	}
      +
      +	//rendering the outline of the connection can be a little bit slow
      +	if(this.render_connections_border && this.scale > 0.6 && !skip_border)
      +	{
      +		ctx.strokeStyle = "rgba(0,0,0,0.5)";
      +		ctx.stroke();
      +	}
      +
      +	ctx.lineWidth = this.connections_width;
      +	ctx.fillStyle = ctx.strokeStyle = color;
      +	ctx.stroke();
      +	//end line shape
      +
      +	//render arrow in the middle
      +	if( this.render_connection_arrows && this.scale >= 0.6 )
      +	{
      +		//render arrow
      +		if(this.render_connection_arrows && this.scale > 0.6)
      +		{
      +			//compute two points in the connection
      +			var pos = this.computeConnectionPoint(a,b,0.5);
      +			var pos2 = this.computeConnectionPoint(a,b,0.51);
      +
      +			//compute the angle between them so the arrow points in the right direction
      +			var angle = 0;
      +			if(this.render_curved_connections)
      +				angle = -Math.atan2( pos2[0] - pos[0], pos2[1] - pos[1]);
      +			else
      +				angle = b[1] > a[1] ? 0 : Math.PI;
      +
      +			//render arrow
      +			ctx.save();
      +			ctx.translate(pos[0],pos[1]);
      +			ctx.rotate(angle);
      +			ctx.beginPath();
      +			ctx.moveTo(-5,-5);
      +			ctx.lineTo(0,+5);
      +			ctx.lineTo(+5,-5);
      +			ctx.fill();
      +			ctx.restore();
      +		}
      +	}
      +
      +	//render flowing points
      +	if(flow)
      +	{
      +		for(var i = 0; i < 5; ++i)
      +		{
      +			var f = (LiteGraph.getTime() * 0.001 + (i * 0.2)) % 1;
      +			var pos = this.computeConnectionPoint(a,b,f);
      +			ctx.beginPath();
      +			ctx.arc(pos[0],pos[1],5,0,2*Math.PI);
      +			ctx.fill();
      +		}
      +	}
      +}
      +
      +LGraphCanvas.prototype.computeConnectionPoint = function(a,b,t)
      +{
      +	var dist = distance(a,b);
      +	var p0 = a;
      +	var p1 = [ a[0] + dist*0.25, a[1] ];
      +	var p2 = [ b[0] - dist*0.25, b[1] ];
      +	var p3 = b;
      +
      +	var c1 = (1-t)*(1-t)*(1-t);
      +	var c2 = 3*((1-t)*(1-t))*t;
      +	var c3 = 3*(1-t)*(t*t);
      +	var c4 = t*t*t;
      +
      +	var x = c1*p0[0] + c2*p1[0] + c3*p2[0] + c4*p3[0];
      +	var y = c1*p0[1] + c2*p1[1] + c3*p2[1] + c4*p3[1];
      +	return [x,y];
      +}
      +
      +/**
      +* draws the widgets stored inside a node
      +* @method drawNodeWidgets
      +**/
      +LGraphCanvas.prototype.drawNodeWidgets = function( node, posY, ctx, active_widget )
      +{
      +	if(!node.widgets || !node.widgets.length)
      +		return 0;
      +	var width = node.size[0];
      +	var widgets = node.widgets;
      +	posY += 2;
      +	var H = LiteGraph.NODE_WIDGET_HEIGHT;
      +	var show_text = this.scale > 0.5;
      +
      +	for(var i = 0; i < widgets.length; ++i)
      +	{
      +		var w = widgets[i];
      +		var y = posY;
      +		if(w.y)
      +			y = w.y;
      +		w.last_y = y;
      +		ctx.strokeStyle = "#AAA";
      +		ctx.fillStyle = "#222";
      +		ctx.textAlign = "left";
      +
      +		switch( w.type )
      +		{
      +			case "button": 
      +				if(w.clicked)
      +				{
      +					ctx.fillStyle = "#AAA";
      +					w.clicked = false;
      +					this.dirty_canvas = true;
      +				}
      +				ctx.fillRect(10,y,width-20,H);
      +				ctx.strokeRect(10,y,width-20,H);
      +				if(show_text)
      +				{
      +					ctx.textAlign = "center";
      +					ctx.fillStyle = "#AAA";
      +					ctx.fillText( w.name, width*0.5, y + H*0.7 );
      +				}
      +				break;
      +			case "slider": 
      +				ctx.fillStyle = "#111";
      +				ctx.fillRect(10,y,width-20,H);
      +				var range = w.options.max - w.options.min;
      +				var nvalue = (w.value - w.options.min) / range;
      +				ctx.fillStyle = active_widget == w ? "#89A" : "#678";
      +				ctx.fillRect(10,y,nvalue*(width-20),H);
      +				ctx.strokeRect(10,y,width-20,H);
      +				if(show_text)
      +				{
      +					ctx.textAlign = "center";
      +					ctx.fillStyle = "#DDD";
      +					ctx.fillText( w.name + "  " + Number(w.value).toFixed(3), width*0.5, y + H*0.7 );
      +				}
      +				break;
      +			case "number":
      +			case "combo":
      +				ctx.textAlign = "left";
      +				ctx.strokeStyle = "#AAA";
      +				ctx.fillStyle = "#111";
      +				ctx.beginPath();
      +				ctx.roundRect( 10, posY, width - 20, H,H*0.5 );
      +				ctx.fill();
      +				ctx.stroke();
      +				ctx.fillStyle = "#AAA";
      +				ctx.beginPath();
      +				ctx.moveTo( 26, posY + 5 );
      +				ctx.lineTo( 16, posY + H*0.5 );
      +				ctx.lineTo( 26, posY + H - 5 );
      +				ctx.moveTo( width - 26, posY + 5 );
      +				ctx.lineTo( width - 16, posY + H*0.5 );
      +				ctx.lineTo( width - 26, posY + H - 5 );
      +				ctx.fill();
      +				if(show_text)
      +				{
      +					ctx.fillStyle = "#999";
      +					ctx.fillText( w.name, 30, y + H*0.7 );
      +					ctx.fillStyle = "#DDD";
      +					ctx.textAlign = "right";
      +					if(w.type == "number")
      +						ctx.fillText( Number(w.value).toFixed( w.options.precision !== undefined ? w.options.precision : 3), width - 40, y + H*0.7 );
      +					else
      +						ctx.fillText( w.value, width - 40, y + H*0.7 );
      +				}
      +				break;
      +			default:
      +				break;
      +		}
      +		posY += H + 4;
      +	}
      +	ctx.textAlign = "left";
      +}
      +
      +/**
      +* process an event on widgets 
      +* @method processNodeWidgets
      +**/
      +LGraphCanvas.prototype.processNodeWidgets = function( node, pos, event, active_widget )
      +{
      +	if(!node.widgets || !node.widgets.length)
      +		return null;
      +
      +	var x = pos[0] - node.pos[0];
      +	var y = pos[1] - node.pos[1];
      +	var width = node.size[0];
      +	var that = this;
      +
      +	for(var i = 0; i < node.widgets.length; ++i)
      +	{
      +		var w = node.widgets[i];
      +		if( w == active_widget || (x > 6 && x < (width - 12) && y > w.last_y && y < (w.last_y + LiteGraph.NODE_WIDGET_HEIGHT)) )
      +		{
      +			//inside widget
      +			switch( w.type )
      +			{
      +				case "button": 
      +					if(w.callback)
      +						setTimeout( function(){	w.callback( w, that, node, pos ); }, 20 );
      +					w.clicked = true;
      +					this.dirty_canvas = true;
      +					break;
      +				case "slider": 
      +					var range = w.options.max - w.options.min;
      +					var nvalue = Math.clamp( (x - 10) / (width - 20), 0, 1);
      +					w.value = w.options.min + (w.options.max - w.options.min) * nvalue;
      +					if(w.callback)
      +						setTimeout( function(){	w.callback( w.value, that, node, pos ); }, 20 );
      +					this.dirty_canvas = true;
      +					break;
      +				case "number": 
      +				case "combo": 
      +					if(event.type == "mousemove" && w.type == "number")
      +					{
      +						w.value += (event.deltaX * 0.1) * (w.options.step || 1);
      +						if(w.options.min != null && w.value < w.options.min)
      +							w.value = w.options.min;
      +						if(w.options.max != null && w.value > w.options.max)
      +							w.value = w.options.max;
      +					}
      +					else if( event.type == "mousedown" )
      +					{
      +						var delta = ( x < 40 ? -1 : ( x > width - 40 ? 1 : 0) );
      +						if (w.type == "number")
      +						{
      +							w.value += delta * 0.1 * (w.options.step || 1);
      +							if(w.options.min != null && w.value < w.options.min)
      +								w.value = w.options.min;
      +							if(w.options.max != null && w.value > w.options.max)
      +								w.value = w.options.max;
      +						}
      +						else if(delta)
      +						{
      +							var index = w.options.values.indexOf( w.value ) + delta;
      +							if( index >= w.options.values.length )
      +								index = 0;
      +							if( index < 0 )
      +								index = w.options.values.length - 1;
      +							w.value = w.options.values[ index ];
      +						}
      +					}
      +					if(w.callback)
      +						setTimeout( function(){	w.callback( w.value, that, node, pos ); }, 20 );
      +					this.dirty_canvas = true;
      +					break;
      +			}
      +
      +			return w;
      +		}
      +	}
      +	return null;
      +}
      +
      +/**
      +* draws every group area in the background
      +* @method drawGroups
      +**/
      +LGraphCanvas.prototype.drawGroups = function(canvas, ctx)
      +{
      +	if(!this.graph)
      +		return;
      +
      +	var groups = this.graph._groups;
      +
      +	ctx.save();
      +	ctx.globalAlpha = 0.5;
      +	ctx.font = "24px Arial";
      +
      +	for(var i = 0; i < groups.length; ++i)
      +	{
      +		var group = groups[i];
      +
      +		if(!overlapBounding( this.visible_area, group._bounding ))
      +			continue; //out of the visible area
      +
      +		ctx.fillStyle = group.color || "#335";
      +		ctx.strokeStyle = group.color || "#335";
      +		var pos = group._pos;
      +		var size = group._size;
      +		ctx.globalAlpha = 0.25;
      +		ctx.beginPath();
      +		ctx.rect( pos[0] + 0.5, pos[1] + 0.5, size[0], size[1] );
      +		ctx.fill();
      +		ctx.globalAlpha = 1;
      +		ctx.stroke();
      +
      +		ctx.beginPath();
      +		ctx.moveTo( pos[0] + size[0], pos[1] + size[1] );
      +		ctx.lineTo( pos[0] + size[0] - 10, pos[1] + size[1] );
      +		ctx.lineTo( pos[0] + size[0], pos[1] + size[1] - 10 );
      +		ctx.fill();
      +
      +		ctx.fillText( group.title, pos[0] + 4, pos[1] + 24 );
      +	}
      +
      +	ctx.restore();
      +}
      +
      +/**
      +* resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode
      +* @method resize
      +**/
      +LGraphCanvas.prototype.resize = function(width, height)
      +{
      +	if(!width && !height)
      +	{
      +		var parent = this.canvas.parentNode;
      +		width = parent.offsetWidth;
      +		height = parent.offsetHeight;
      +	}
      +
      +	if(this.canvas.width == width && this.canvas.height == height)
      +		return;
      +
      +	this.canvas.width = width;
      +	this.canvas.height = height;
      +	this.bgcanvas.width = this.canvas.width;
      +	this.bgcanvas.height = this.canvas.height;
      +	this.setDirty(true,true);
      +}
      +
      +/**
      +* switches to live mode (node shapes are not rendered, only the content)
      +* this feature was designed when graphs where meant to create user interfaces
      +* @method switchLiveMode
      +**/
      +LGraphCanvas.prototype.switchLiveMode = function(transition)
      +{
      +	if(!transition)
      +	{
      +		this.live_mode = !this.live_mode;
      +		this.dirty_canvas = true;
      +		this.dirty_bgcanvas = true;
      +		return;
      +	}
      +
      +	var self = this;
      +	var delta = this.live_mode ? 1.1 : 0.9;
      +	if(this.live_mode)
      +	{
      +		this.live_mode = false;
      +		this.editor_alpha = 0.1;
      +	}
      +
      +	var t = setInterval(function() {
      +		self.editor_alpha *= delta;
      +		self.dirty_canvas = true;
      +		self.dirty_bgcanvas = true;
      +
      +		if(delta < 1  && self.editor_alpha < 0.01)
      +		{
      +			clearInterval(t);
      +			if(delta < 1)
      +				self.live_mode = true;
      +		}
      +		if(delta > 1 && self.editor_alpha > 0.99)
      +		{
      +			clearInterval(t);
      +			self.editor_alpha = 1;
      +		}
      +	},1);
      +}
      +
      +LGraphCanvas.prototype.onNodeSelectionChange = function(node)
      +{
      +	return; //disabled
      +}
      +
      +LGraphCanvas.prototype.touchHandler = function(event)
      +{
      +	//alert("foo");
      +    var touches = event.changedTouches,
      +        first = touches[0],
      +        type = "";
      +
      +         switch(event.type)
      +    {
      +        case "touchstart": type = "mousedown"; break;
      +        case "touchmove":  type = "mousemove"; break;
      +        case "touchend":   type = "mouseup"; break;
      +        default: return;
      +    }
      +
      +             //initMouseEvent(type, canBubble, cancelable, view, clickCount,
      +    //           screenX, screenY, clientX, clientY, ctrlKey,
      +    //           altKey, shiftKey, metaKey, button, relatedTarget);
      +
      +	var window = this.getCanvasWindow();
      +	var document = window.document;
      +
      +    var simulatedEvent = document.createEvent("MouseEvent");
      +    simulatedEvent.initMouseEvent(type, true, true, window, 1,
      +                              first.screenX, first.screenY,
      +                              first.clientX, first.clientY, false,
      +                              false, false, false, 0/*left*/, null);
      +	first.target.dispatchEvent(simulatedEvent);
      +    event.preventDefault();
      +}
      +
      +/* CONTEXT MENU ********************/
      +
      +LGraphCanvas.onGroupAdd = function(info,entry,mouse_event)
      +{
      +	var canvas = LGraphCanvas.active_canvas;
      +	var ref_window = canvas.getCanvasWindow();
      +		
      +	var group = new LiteGraph.LGraphGroup();
      +	group.pos = canvas.convertEventToCanvas( mouse_event );
      +	canvas.graph.add( group );
      +}
      +
      +LGraphCanvas.onMenuAdd = function( node, options, e, prev_menu )
      +{
      +	var canvas = LGraphCanvas.active_canvas;
      +	var ref_window = canvas.getCanvasWindow();
      +
      +	var values = LiteGraph.getNodeTypesCategories();
      +	var entries = [];
      +	for(var i in values)
      +		if(values[i])
      +			entries.push({ value: values[i], content: values[i], has_submenu: true });
      +
      +	var menu = new LiteGraph.ContextMenu( entries, { event: e, callback: inner_clicked, parentMenu: prev_menu }, ref_window);
      +
      +	function inner_clicked( v, option, e )
      +	{
      +		var category = v.value;
      +		var node_types = LiteGraph.getNodeTypesInCategory( category, canvas.filter );
      +		var values = [];
      +		for(var i in node_types)
      +			values.push( { content: node_types[i].title, value: node_types[i].type });
      +
      +		new LiteGraph.ContextMenu( values, {event: e, callback: inner_create, parentMenu: menu }, ref_window);
      +		return false;
      +	}
      +
      +	function inner_create( v, e )
      +	{
      +		var first_event = prev_menu.getFirstEvent();
      +		var node = LiteGraph.createNode( v.value );
      +		if(node)
      +		{
      +			node.pos = canvas.convertEventToCanvas( first_event );
      +			canvas.graph.add( node );
      +		}
      +	}
      +
      +	return false;
      +}
      +
      +LGraphCanvas.onMenuCollapseAll = function()
      +{
      +
      +}
      +
      +
      +LGraphCanvas.onMenuNodeEdit = function()
      +{
      +
      +}
      +
      +LGraphCanvas.showMenuNodeOptionalInputs = function( v, options, e, prev_menu, node )
      +{
      +	if(!node)
      +		return;
      +
      +	var that = this;
      +	var canvas = LGraphCanvas.active_canvas;
      +	var ref_window = canvas.getCanvasWindow();
      +
      +	var options = node.optional_inputs;
      +	if(node.onGetInputs)
      +		options = node.onGetInputs();
      +
      +	var entries = [];
      +	if(options)
      +		for (var i in options)
      +		{
      +			var entry = options[i];
      +			if(!entry)
      +			{
      +				entries.push(null);
      +				continue;
      +			}
      +			var label = entry[0];
      +			if(entry[2] && entry[2].label)
      +				label = entry[2].label;
      +			var data = {content: label, value: entry};
      +			if(entry[1] == LiteGraph.ACTION)
      +				data.className = "event";
      +			entries.push(data);
      +		}
      +
      +	if(this.onMenuNodeInputs)
      +		entries = this.onMenuNodeInputs( entries );
      +
      +	if(!entries.length)
      +		return;
      +
      +	var menu = new LiteGraph.ContextMenu(entries, { event: e, callback: inner_clicked, parentMenu: prev_menu, node: node }, ref_window);
      +
      +	function inner_clicked(v, e, prev)
      +	{
      +		if(!node)
      +			return;
      +
      +		if(v.callback)
      +			v.callback.call( that, node, v, e, prev );
      +
      +		if(v.value)
      +		{
      +			node.addInput(v.value[0],v.value[1], v.value[2]);
      +			node.setDirtyCanvas(true,true);
      +		}
      +	}
      +
      +	return false;
      +}
      +
      +LGraphCanvas.showMenuNodeOptionalOutputs = function( v, options, e, prev_menu, node )
      +{
      +	if(!node)
      +		return;
      +
      +	var that = this;
      +	var canvas = LGraphCanvas.active_canvas;
      +	var ref_window = canvas.getCanvasWindow();
      +
      +	var options = node.optional_outputs;
      +	if(node.onGetOutputs)
      +		options = node.onGetOutputs();
      +
      +	var entries = [];
      +	if(options)
      +		for (var i in options)
      +		{
      +			var entry = options[i];
      +			if(!entry) //separator?
      +			{
      +				entries.push(null);
      +				continue;
      +			}
      +
      +			if(node.flags && node.flags.skip_repeated_outputs && node.findOutputSlot(entry[0]) != -1)
      +				continue; //skip the ones already on
      +			var label = entry[0];
      +			if(entry[2] && entry[2].label)
      +				label = entry[2].label;
      +			var data = {content: label, value: entry};
      +			if(entry[1] == LiteGraph.EVENT)
      +				data.className = "event";
      +			entries.push(data);
      +		}
      +
      +	if(this.onMenuNodeOutputs)
      +		entries = this.onMenuNodeOutputs( entries );
      +
      +	if(!entries.length)
      +		return;
      +
      +	var menu = new LiteGraph.ContextMenu(entries, {event: e, callback: inner_clicked, parentMenu: prev_menu, node: node }, ref_window);
      +
      +	function inner_clicked( v, e, prev )
      +	{
      +		if(!node)
      +			return;
      +
      +		if(v.callback)
      +			v.callback.call( that, node, v, e, prev );
      +
      +		if(!v.value)
      +			return;
      +
      +		var value = v.value[1];
      +
      +		if(value && (value.constructor === Object || value.constructor === Array)) //submenu why?
      +		{
      +			var entries = [];
      +			for(var i in value)
      +				entries.push({ content: i, value: value[i]});
      +			new LiteGraph.ContextMenu( entries, { event: e, callback: inner_clicked, parentMenu: prev_menu, node: node });
      +			return false;
      +		}
      +		else
      +		{
      +			node.addOutput( v.value[0], v.value[1], v.value[2]);
      +			node.setDirtyCanvas(true,true);
      +		}
      +
      +	}
      +
      +	return false;
      +}
      +
      +LGraphCanvas.onShowMenuNodeProperties = function( value, options, e, prev_menu, node )
      +{
      +	if(!node || !node.properties)
      +		return;
      +
      +	var that = this;
      +	var canvas = LGraphCanvas.active_canvas;
      +	var ref_window = canvas.getCanvasWindow();
      +
      +	var entries = [];
      +		for (var i in node.properties)
      +		{
      +			var value = node.properties[i] !== undefined ? node.properties[i] : " ";
      +			//value could contain invalid html characters, clean that
      +			value = LGraphCanvas.decodeHTML(value);
      +			entries.push({content: "<span class='property_name'>" + i + "</span>" + "<span class='property_value'>" + value + "</span>", value: i});
      +		}
      +	if(!entries.length)
      +		return;
      +
      +	var menu = new LiteGraph.ContextMenu(entries, {event: e, callback: inner_clicked, parentMenu: prev_menu, allow_html: true, node: node },ref_window);
      +
      +	function inner_clicked( v, options, e, prev )
      +	{
      +		if(!node)
      +			return;
      +		var rect = this.getBoundingClientRect();
      +		canvas.showEditPropertyValue( node, v.value, { position: [rect.left, rect.top] });
      +	}
      +
      +	return false;
      +}
      +
      +LGraphCanvas.decodeHTML = function( str )
      +{
      +	var e = document.createElement("div");
      +	e.innerText = str;
      +	return e.innerHTML;
      +}
      +
      +LGraphCanvas.onResizeNode = function( value, options, e, menu, node )
      +{
      +	if(!node)
      +		return;
      +	node.size = node.computeSize();
      +	node.setDirtyCanvas(true,true);
      +}
      +
      +
      +LGraphCanvas.onShowTitleEditor = function( value, options, e, menu, node )
      +{
      +	var input_html = "";
      +
      +	var dialog = document.createElement("div");
      +	dialog.className = "graphdialog";
      +	dialog.innerHTML = "<span class='name'>Title</span><input autofocus type='text' class='value'/><button>OK</button>";
      +	var input = dialog.querySelector("input");
      +	if(input)
      +	{
      +		input.value = node.title;
      +		input.addEventListener("keydown", function(e){
      +			if(e.keyCode != 13)
      +				return;
      +			inner();
      +			e.preventDefault();
      +			e.stopPropagation();
      +		});
      +	}
      +
      +	var graphcanvas = LGraphCanvas.active_canvas;
      +	var canvas = graphcanvas.canvas;
      +
      +	var rect = canvas.getBoundingClientRect();
      +	var offsetx = -20;
      +	var offsety = -20;
      +	if(rect)
      +	{
      +		offsetx -= rect.left;
      +		offsety -= rect.top;
      +	}
      +
      +	if( event )
      +	{
      +		dialog.style.left = (event.pageX + offsetx) + "px";
      +		dialog.style.top = (event.pageY + offsety)+ "px";
      +	}
      +	else
      +	{
      +		dialog.style.left = (canvas.width * 0.5 + offsetx) + "px";
      +		dialog.style.top = (canvas.height * 0.5 + offsety) + "px";
      +	}
      +
      +	var button = dialog.querySelector("button");
      +	button.addEventListener("click", inner );
      +	canvas.parentNode.appendChild( dialog );
      +
      +	function inner()
      +	{
      +		setValue( input.value );
      +	}
      +
      +	function setValue(value)
      +	{
      +		node.title = value;
      +		dialog.parentNode.removeChild( dialog );
      +		node.setDirtyCanvas(true,true);
      +	}
      +}
      +
      +LGraphCanvas.prototype.showSearchBox = function(event)
      +{
      +	var that = this;
      +	var input_html = "";
      +
      +	var dialog = document.createElement("div");
      +	dialog.className = "graphdialog";
      +	dialog.innerHTML = "<span class='name'>Search</span> <input autofocus type='text' class='value'/><div class='helper'></div>";
      +	dialog.close = function()
      +	{
      +		that.search_box = null;
      +		dialog.parentNode.removeChild( dialog );
      +	}
      +
      +	dialog.addEventListener("mouseleave",function(e){
      +		 dialog.close();
      +	});
      +
      +	if(that.search_box)
      +		that.search_box.close();
      +	that.search_box = dialog;
      +
      +	var helper = dialog.querySelector(".helper");
      +
      +	var first = null;
      +	var timeout = null;
      +	var selected = null;
      +
      +	var input = dialog.querySelector("input");
      +	if(input)
      +	{
      +		input.addEventListener("keydown", function(e){
      +
      +			if(e.keyCode == 38) //UP
      +				changeSelection(false);
      +			else if(e.keyCode == 40) //DOWN
      +				changeSelection(true);
      +			else if(e.keyCode == 27) //ESC
      +				dialog.close();
      +			else if(e.keyCode == 13)
      +			{
      +				if(selected)
      +					select( selected.innerHTML )
      +				else if(first)
      +					select(first);
      +				else
      +					dialog.close();
      +			}
      +			else
      +			{
      +				if(timeout)
      +					clearInterval(timeout);
      +				timeout = setTimeout( refreshHelper, 10 );
      +				return;
      +			}
      +			e.preventDefault();
      +			e.stopPropagation();
      +		});
      +	}
      +
      +	var graphcanvas = LGraphCanvas.active_canvas;
      +	var canvas = graphcanvas.canvas;
      +
      +	var rect = canvas.getBoundingClientRect();
      +	var offsetx = -20;
      +	var offsety = -20;
      +	if(rect)
      +	{
      +		offsetx -= rect.left;
      +		offsety -= rect.top;
      +	}
      +
      +	if( event )
      +	{
      +		dialog.style.left = (event.pageX + offsetx) + "px";
      +		dialog.style.top = (event.pageY + offsety)+ "px";
      +	}
      +	else
      +	{
      +		dialog.style.left = (canvas.width * 0.5 + offsetx) + "px";
      +		dialog.style.top = (canvas.height * 0.5 + offsety) + "px";
      +	}
      +
      +	canvas.parentNode.appendChild( dialog );
      +	input.focus();
      +
      +	function select( name )
      +	{
      +		if(name)
      +		{
      +			if( that.onSearchBoxSelection )
      +				that.onSearchBoxSelection( name, event, graphcanvas );
      +			else
      +			{
      +				var node = LiteGraph.createNode( name );
      +				if(node)
      +				{
      +					node.pos = graphcanvas.convertEventToCanvas( event );
      +					graphcanvas.graph.add( node );
      +				}
      +			}
      +		}
      +
      +		dialog.close();
      +	}
      +
      +	function changeSelection( forward )
      +	{
      +		var prev = selected;
      +		if(selected)
      +			selected.classList.remove("selected");
      +		if(!selected)
      +			selected = forward ? helper.childNodes[0] : helper.childNodes[ helper.childNodes.length ];
      +		else
      +		{
      +			selected = forward ? selected.nextSibling : selected.previousSibling;
      +			if(!selected)
      +				selected = prev;
      +		}
      +		if(!selected)
      +			return;
      +		selected.classList.add("selected");
      +		selected.scrollIntoView();
      +	}
      +
      +	function refreshHelper()
      +	{
      +		timeout = null;
      +		var str = input.value;
      +		first = null;
      +		helper.innerHTML = "";
      +		if(!str)
      +			return;
      +
      +		if( that.onSearchBox )
      +			that.onSearchBox( help, str, graphcanvas );
      +		else
      +			for( var i in LiteGraph.registered_node_types )
      +				if(i.indexOf(str) != -1)
      +				{
      +					var help = document.createElement("div");
      +					if(!first) first = i;
      +					help.innerText = i;
      +					help.className = "help-item";
      +					help.addEventListener("click", function(e){
      +						select( this.innerText );
      +					});
      +					helper.appendChild(help);
      +				}
      +	}
      +
      +	return dialog;
      +}
      +
      +LGraphCanvas.prototype.showEditPropertyValue = function( node, property, options )
      +{
      +	if(!node || node.properties[ property ] === undefined )
      +		return;
      +
      +	options = options || {};
      +	var that = this;
      +
      +	var type = "string";
      +
      +	if(node.properties[ property ] !== null)
      +		type = typeof(node.properties[ property ]);
      +
      +	//for arrays
      +	if(type == "object")
      +	{
      +		if( node.properties[ property ].length )
      +			type = "array";
      +	}
      +
      +	var info = null;
      +	if(node.getPropertyInfo)
      +		info = node.getPropertyInfo(property);
      +	if(node.properties_info)
      +	{
      +		for(var i = 0; i < node.properties_info.length; ++i)
      +		{
      +			if( node.properties_info[i].name == property )
      +			{
      +				info = node.properties_info[i];
      +				break;
      +			}
      +		}
      +	}
      +
      +	if(info !== undefined && info !== null && info.type )
      +		type = info.type;
      +
      +	var input_html = "";
      +
      +	if(type == "string" || type == "number" || type == "array")
      +		input_html = "<input autofocus type='text' class='value'/>";
      +	else if(type == "enum" && info.values)
      +	{
      +		input_html = "<select autofocus type='text' class='value'>";
      +		for(var i in info.values)
      +		{
      +			var v = info.values.constructor === Array ? info.values[i] : i;
      +			input_html += "<option value='"+v+"' "+(v == node.properties[property] ? "selected" : "")+">"+info.values[i]+"</option>";
      +		}
      +		input_html += "</select>";
      +	}
      +	else if(type == "boolean")
      +	{
      +		input_html = "<input autofocus type='checkbox' class='value' "+(node.properties[property] ? "checked" : "")+"/>";
      +	}
      +	else
      +	{
      +		console.warn("unknown type: " + type );
      +		return;
      +	}
      +
      +	var dialog = this.createDialog( "<span class='name'>" + property + "</span>"+input_html+"<button>OK</button>" , options );
      +
      +	if(type == "enum" && info.values)
      +	{
      +		var input = dialog.querySelector("select");
      +		input.addEventListener("change", function(e){
      +			setValue( e.target.value );
      +			//var index = e.target.value;
      +			//setValue( e.options[e.selectedIndex].value );
      +		});
      +	}
      +	else if(type == "boolean")
      +	{
      +		var input = dialog.querySelector("input");
      +		if(input)
      +		{
      +			input.addEventListener("click", function(e){
      +				setValue( !!input.checked );
      +			});
      +		}
      +	}
      +	else
      +	{
      +		var input = dialog.querySelector("input");
      +		if(input)
      +		{
      +			input.value = node.properties[ property ] !== undefined ? node.properties[ property ] : "";
      +			input.addEventListener("keydown", function(e){
      +				if(e.keyCode != 13)
      +					return;
      +				inner();
      +				e.preventDefault();
      +				e.stopPropagation();
      +			});
      +		}
      +	}
      +
      +	var button = dialog.querySelector("button");
      +	button.addEventListener("click", inner );
      +
      +	function inner()
      +	{
      +		setValue( input.value );
      +	}
      +
      +	function setValue(value)
      +	{
      +		if(typeof( node.properties[ property ] ) == "number")
      +			value = Number(value);
      +		if(type == "array")
      +			value = value.split(",").map(Number);
      +		node.properties[ property ] = value;
      +		if(node._graph)
      +			node._graph._version++;
      +		if(node.onPropertyChanged)
      +			node.onPropertyChanged( property, value );
      +		dialog.close();
      +		node.setDirtyCanvas(true,true);
      +	}
      +}
      +
      +LGraphCanvas.prototype.createDialog = function( html, options )
      +{
      +	options = options || {};
      +
      +	var dialog = document.createElement("div");
      +	dialog.className = "graphdialog";
      +	dialog.innerHTML = html;
      +
      +	var rect = this.canvas.getBoundingClientRect();
      +	var offsetx = -20;
      +	var offsety = -20;
      +	if(rect)
      +	{
      +		offsetx -= rect.left;
      +		offsety -= rect.top;
      +	}
      +
      +	if( options.position )
      +	{
      +		offsetx += options.position[0];
      +		offsety += options.position[1];
      +	}
      +	else if( options.event )
      +	{
      +		offsetx += options.event.pageX;
      +		offsety += options.event.pageY;
      +	}
      +	else //centered
      +	{
      +		offsetx += this.canvas.width * 0.5;
      +		offsety += this.canvas.height * 0.5;
      +	}
      +
      +	dialog.style.left = offsetx + "px";
      +	dialog.style.top = offsety + "px";
      +
      +	this.canvas.parentNode.appendChild( dialog );
      +
      +	dialog.close = function()
      +	{
      +		if(this.parentNode)
      +			this.parentNode.removeChild( this );
      +	}
      +
      +	return dialog;
      +}
      +
      +LGraphCanvas.onMenuNodeCollapse = function( value, options, e, menu, node )
      +{
      +	node.collapse();
      +}
      +
      +LGraphCanvas.onMenuNodePin = function( value, options, e, menu, node )
      +{
      +	node.pin();
      +}
      +
      +LGraphCanvas.onMenuNodeMode = function( value, options, e, menu, node )
      +{
      +	new LiteGraph.ContextMenu(["Always","On Event","On Trigger","Never"], {event: e, callback: inner_clicked, parentMenu: menu, node: node });
      +
      +	function inner_clicked(v)
      +	{
      +		if(!node)
      +			return;
      +		switch(v)
      +		{
      +			case "On Event": node.mode = LiteGraph.ON_EVENT; break;
      +			case "On Trigger": node.mode = LiteGraph.ON_TRIGGER; break;
      +			case "Never": node.mode = LiteGraph.NEVER; break;
      +			case "Always":
      +			default:
      +				node.mode = LiteGraph.ALWAYS; break;
      +		}
      +	}
      +
      +	return false;
      +}
      +
      +LGraphCanvas.onMenuNodeColors = function( value, options, e, menu, node )
      +{
      +	if(!node)
      +		throw("no node for color");
      +
      +	var values = [];
      +	values.push({ value:null, content:"<span style='display: block; padding-left: 4px;'>No color</span>" });
      +
      +	for(var i in LGraphCanvas.node_colors)
      +	{
      +		var color = LGraphCanvas.node_colors[i];
      +		var value = { value:i, content:"<span style='display: block; color: #999; padding-left: 4px; border-left: 8px solid "+color.color+"; background-color:"+color.bgcolor+"'>"+i+"</span>" };
      +		values.push(value);
      +	}
      +	new LiteGraph.ContextMenu( values, { event: e, callback: inner_clicked, parentMenu: menu, node: node });
      +
      +	function inner_clicked(v)
      +	{
      +		if(!node)
      +			return;
      +
      +		var color = v.value ? LGraphCanvas.node_colors[ v.value ] : null;
      +		if(color)
      +		{
      +			if(node.constructor === LiteGraph.LGraphGroup)
      +				node.color = color.groupcolor;
      +			else
      +			{
      +				node.color = color.color;
      +				node.bgcolor = color.bgcolor;
      +			}
      +		}
      +		else
      +		{
      +			delete node.color;
      +			delete node.bgcolor;
      +		}
      +		node.setDirtyCanvas(true,true);
      +	}
      +
      +	return false;
      +}
      +
      +LGraphCanvas.onMenuNodeShapes = function( value, options, e, menu, node )
      +{
      +	if(!node)
      +		throw("no node passed");
      +
      +	new LiteGraph.ContextMenu( LiteGraph.VALID_SHAPES, { event: e, callback: inner_clicked, parentMenu: menu, node: node });
      +
      +	function inner_clicked(v)
      +	{
      +		if(!node)
      +			return;
      +		node.shape = v;
      +		node.setDirtyCanvas(true);
      +	}
      +
      +	return false;
      +}
      +
      +LGraphCanvas.onMenuNodeRemove = function( value, options, e, menu, node )
      +{
      +	if(!node)
      +		throw("no node passed");
      +
      +	if(node.removable === false)
      +		return;
      +
      +	node.graph.remove(node);
      +	node.setDirtyCanvas(true,true);
      +}
      +
      +LGraphCanvas.onMenuNodeClone = function( value, options, e, menu, node )
      +{
      +	if(node.clonable == false) return;
      +	var newnode = node.clone();
      +	if(!newnode)
      +		return;
      +	newnode.pos = [node.pos[0]+5,node.pos[1]+5];
      +	node.graph.add(newnode);
      +	node.setDirtyCanvas(true,true);
      +}
      +
      +LGraphCanvas.node_colors = {
      +	"red": { color:"#322", bgcolor:"#533", groupcolor: "#A88" },
      +	"brown": { color:"#332922", bgcolor:"#593930", groupcolor: "#b06634" },
      +	"green": { color:"#232", bgcolor:"#353", groupcolor: "#8A8" },
      +	"blue": { color:"#223", bgcolor:"#335", groupcolor: "#88A" },
      +	"pale_blue": { color:"#2a363b", bgcolor:"#3f5159", groupcolor: "#3f789e" },
      +	"cyan": { color:"#233", bgcolor:"#355", groupcolor: "#8AA" },
      +	"purple": { color:"#323", bgcolor:"#535", groupcolor: "#a1309b" },
      +	"yellow": { color:"#432", bgcolor:"#653", groupcolor: "#b58b2a" },
      +	"black": { color:"#222", bgcolor:"#000", groupcolor: "#444" }
      +};
      +
      +LGraphCanvas.prototype.getCanvasMenuOptions = function()
      +{
      +	var options = null;
      +	if(this.getMenuOptions)
      +		options = this.getMenuOptions();
      +	else
      +	{
      +		options = [
      +			{ content:"Add Node", has_submenu: true, callback: LGraphCanvas.onMenuAdd },
      +			{ content:"Add Group", callback: LGraphCanvas.onGroupAdd }
      +			//{content:"Collapse All", callback: LGraphCanvas.onMenuCollapseAll }
      +		];
      +
      +		if(this._graph_stack && this._graph_stack.length > 0)
      +			options = [{content:"Close subgraph", callback: this.closeSubgraph.bind(this) },null].concat(options);
      +	}
      +
      +	if(this.getExtraMenuOptions)
      +	{
      +		var extra = this.getExtraMenuOptions(this,options);
      +		if(extra)
      +			options = options.concat( extra );
      +	}
      +
      +	return options;
      +}
      +
      +//called by processContextMenu to extract the menu list
      +LGraphCanvas.prototype.getNodeMenuOptions = function( node )
      +{
      +	var options = null;
      +
      +	if(node.getMenuOptions)
      +		options = node.getMenuOptions(this);
      +	else
      +		options = [
      +			{content:"Inputs", has_submenu: true, disabled:true, callback: LGraphCanvas.showMenuNodeOptionalInputs },
      +			{content:"Outputs", has_submenu: true, disabled:true, callback: LGraphCanvas.showMenuNodeOptionalOutputs },
      +			null,
      +			{content:"Properties", has_submenu: true, callback: LGraphCanvas.onShowMenuNodeProperties },
      +			null,
      +			{content:"Title", callback: LGraphCanvas.onShowTitleEditor },
      +			{content:"Mode", has_submenu: true, callback: LGraphCanvas.onMenuNodeMode },
      +			{content:"Resize", callback: LGraphCanvas.onResizeNode },
      +			{content:"Collapse", callback: LGraphCanvas.onMenuNodeCollapse },
      +			{content:"Pin", callback: LGraphCanvas.onMenuNodePin },
      +			{content:"Colors", has_submenu: true, callback: LGraphCanvas.onMenuNodeColors },
      +			{content:"Shapes", has_submenu: true, callback: LGraphCanvas.onMenuNodeShapes },
      +			null
      +		];
      +
      +	if(node.getExtraMenuOptions)
      +	{
      +		var extra = node.getExtraMenuOptions(this);
      +		if(extra)
      +		{
      +			extra.push(null);
      +			options = extra.concat( options );
      +		}
      +	}
      +
      +	if( node.clonable !== false )
      +			options.push({content:"Clone", callback: LGraphCanvas.onMenuNodeClone });
      +	if( node.removable !== false )
      +			options.push(null,{content:"Remove", callback: LGraphCanvas.onMenuNodeRemove });
      +
      +	if(node.onGetInputs)
      +	{
      +		var inputs = node.onGetInputs();
      +		if(inputs && inputs.length)
      +			options[0].disabled = false;
      +	}
      +
      +	if(node.onGetOutputs)
      +	{
      +		var outputs = node.onGetOutputs();
      +		if(outputs && outputs.length )
      +			options[1].disabled = false;
      +	}
      +
      +	if(node.graph && node.graph.onGetNodeMenuOptions )
      +		node.graph.onGetNodeMenuOptions( options, node );
      +
      +	return options;
      +}
      +
      +LGraphCanvas.prototype.getGroupMenuOptions = function( node )
      +{
      +	var o = [
      +		{content:"Title", callback: LGraphCanvas.onShowTitleEditor },
      +		{content:"Color", has_submenu: true, callback: LGraphCanvas.onMenuNodeColors },
      +		null,
      +		{content:"Remove", callback: LGraphCanvas.onMenuNodeRemove }
      +	];
      +
      +	return o;
      +}
      +
      +LGraphCanvas.prototype.processContextMenu = function( node, event )
      +{
      +	var that = this;
      +	var canvas = LGraphCanvas.active_canvas;
      +	var ref_window = canvas.getCanvasWindow();
      +
      +	var menu_info = null;
      +	var options = { event: event, callback: inner_option_clicked, node: node };
      +
      +	//check if mouse is in input
      +	var slot = null;
      +	if(node)
      +	{
      +		slot = node.getSlotInPosition( event.canvasX, event.canvasY );
      +		LGraphCanvas.active_node = node;
      +	}
      +
      +	if(slot) //on slot
      +	{
      +		menu_info = [];
      +		menu_info.push( slot.locked ? "Cannot remove"  : { content: "Remove Slot", slot: slot } );
      +		menu_info.push( { content: "Rename Slot", slot: slot } );
      +		options.title = (slot.input ? slot.input.type : slot.output.type) || "*";
      +		if(slot.input && slot.input.type == LiteGraph.ACTION)
      +			options.title = "Action";
      +		if(slot.output && slot.output.type == LiteGraph.EVENT)
      +			options.title = "Event";
      +	}
      +	else
      +	{
      +		if( node ) //on node
      +			menu_info = this.getNodeMenuOptions(node);
      +		else 
      +		{
      +			var group = this.graph.getGroupOnPos( event.canvasX, event.canvasY );
      +			if( group ) //on group
      +			{
      +				options.node = group;
      +				menu_info = this.getGroupMenuOptions( group );
      +			}
      +			else
      +				menu_info = this.getCanvasMenuOptions();
      +		}
      +	}
      +
      +	//show menu
      +	if(!menu_info)
      +		return;
      +
      +	var menu = new LiteGraph.ContextMenu( menu_info, options, ref_window );
      +
      +	function inner_option_clicked( v, options, e )
      +	{
      +		if(!v)
      +			return;
      +
      +		if(v.content == "Remove Slot")
      +		{
      +			var info = v.slot;
      +			if(info.input)
      +				node.removeInput( info.slot );
      +			else if(info.output)
      +				node.removeOutput( info.slot );
      +			return;
      +		}
      +		else if( v.content == "Rename Slot")
      +		{
      +			var info = v.slot;
      +			var dialog = that.createDialog( "<span class='name'>Name</span><input type='text'/><button>OK</button>" , options );
      +			var input = dialog.querySelector("input");
      +			dialog.querySelector("button").addEventListener("click",function(e){
      +				if(input.value)
      +				{
      +					var slot_info = info.input ? node.getInputInfo( info.slot ) : node.getOutputInfo( info.slot );
      +					if( slot_info )
      +						slot_info.label = input.value;
      +					that.setDirty(true);
      +				}
      +				dialog.close();
      +			});
      +		}
      +
      +		//if(v.callback)
      +		//	return v.callback.call(that, node, options, e, menu, that, event );
      +	}
      +}
      +
      +
      +
      +
      +
      +
      +//API *************************************************
      +//like rect but rounded corners
      +if(this.CanvasRenderingContext2D)
      +CanvasRenderingContext2D.prototype.roundRect = function (x, y, width, height, radius, radius_low) {
      +  if ( radius === undefined ) {
      +    radius = 5;
      +  }
      +
      +  if(radius_low === undefined)
      +	 radius_low  = radius;
      +
      +  this.moveTo(x + radius, y);
      +  this.lineTo(x + width - radius, y);
      +  this.quadraticCurveTo(x + width, y, x + width, y + radius);
      +
      +  this.lineTo(x + width, y + height - radius_low);
      +  this.quadraticCurveTo(x + width, y + height, x + width - radius_low, y + height);
      +  this.lineTo(x + radius_low, y + height);
      +  this.quadraticCurveTo(x, y + height, x, y + height - radius_low);
      +  this.lineTo(x, y + radius);
      +  this.quadraticCurveTo(x, y, x + radius, y);
      +}
      +
      +function compareObjects(a,b)
      +{
      +	for(var i in a)
      +		if(a[i] != b[i])
      +			return false;
      +	return true;
      +}
      +LiteGraph.compareObjects = compareObjects;
      +
      +function distance(a,b)
      +{
      +	return Math.sqrt( (b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]) );
      +}
      +LiteGraph.distance = distance;
      +
      +function colorToString(c)
      +{
      +	return "rgba(" + Math.round(c[0] * 255).toFixed() + "," + Math.round(c[1] * 255).toFixed() + "," + Math.round(c[2] * 255).toFixed() + "," + (c.length == 4 ? c[3].toFixed(2) : "1.0") + ")";
      +}
      +LiteGraph.colorToString = colorToString;
      +
      +function isInsideRectangle( x,y, left, top, width, height)
      +{
      +	if (left < x && (left + width) > x &&
      +		top < y && (top + height) > y)
      +		return true;
      +	return false;
      +}
      +LiteGraph.isInsideRectangle = isInsideRectangle;
      +
      +//[minx,miny,maxx,maxy]
      +function growBounding( bounding, x,y)
      +{
      +	if(x < bounding[0])
      +		bounding[0] = x;
      +	else if(x > bounding[2])
      +		bounding[2] = x;
      +
      +	if(y < bounding[1])
      +		bounding[1] = y;
      +	else if(y > bounding[3])
      +		bounding[3] = y;
      +}
      +LiteGraph.growBounding = growBounding;
      +
      +//point inside boundin box
      +function isInsideBounding(p,bb)
      +{
      +	if (p[0] < bb[0][0] ||
      +		p[1] < bb[0][1] ||
      +		p[0] > bb[1][0] ||
      +		p[1] > bb[1][1])
      +		return false;
      +	return true;
      +}
      +LiteGraph.isInsideBounding = isInsideBounding;
      +
      +//boundings overlap, format: [ startx, starty, width, height ]
      +function overlapBounding(a,b)
      +{
      +	var A_end_x = a[0] + a[2];
      +	var A_end_y = a[1] + a[3];
      +	var B_end_x = b[0] + b[2];
      +	var B_end_y = b[1] + b[3];
      +
      +	if ( a[0] > B_end_x ||
      +		a[1] > B_end_y ||
      +		A_end_x < b[0] ||
      +		A_end_y < b[1])
      +		return false;
      +	return true;
      +}
      +LiteGraph.overlapBounding = overlapBounding;
      +
      +//Convert a hex value to its decimal value - the inputted hex must be in the
      +//	format of a hex triplet - the kind we use for HTML colours. The function
      +//	will return an array with three values.
      +function hex2num(hex) {
      +	if(hex.charAt(0) == "#") hex = hex.slice(1); //Remove the '#' char - if there is one.
      +	hex = hex.toUpperCase();
      +	var hex_alphabets = "0123456789ABCDEF";
      +	var value = new Array(3);
      +	var k = 0;
      +	var int1,int2;
      +	for(var i=0;i<6;i+=2) {
      +		int1 = hex_alphabets.indexOf(hex.charAt(i));
      +		int2 = hex_alphabets.indexOf(hex.charAt(i+1));
      +		value[k] = (int1 * 16) + int2;
      +		k++;
      +	}
      +	return(value);
      +}
      +
      +LiteGraph.hex2num = hex2num;
      +
      +//Give a array with three values as the argument and the function will return
      +//	the corresponding hex triplet.
      +function num2hex(triplet) {
      +	var hex_alphabets = "0123456789ABCDEF";
      +	var hex = "#";
      +	var int1,int2;
      +	for(var i=0;i<3;i++) {
      +		int1 = triplet[i] / 16;
      +		int2 = triplet[i] % 16;
      +
      +		hex += hex_alphabets.charAt(int1) + hex_alphabets.charAt(int2);
      +	}
      +	return(hex);
      +}
      +
      +LiteGraph.num2hex = num2hex;
      +
      +/* LiteGraph GUI elements used for canvas editing *************************************/
      +
      +/**
      +* ContextMenu from LiteGUI
      +*
      +* @class ContextMenu
      +* @constructor
      +* @param {Array} values (allows object { title: "Nice text", callback: function ... })
      +* @param {Object} options [optional] Some options:\
      +* - title: title to show on top of the menu
      +* - callback: function to call when an option is clicked, it receives the item information
      +* - ignore_item_callbacks: ignores the callback inside the item, it just calls the options.callback
      +* - event: you can pass a MouseEvent, this way the ContextMenu appears in that position
      +*/
      +function ContextMenu( values, options )
      +{
      +	options = options || {};
      +	this.options = options;
      +	var that = this;
      +
      +	//to link a menu with its parent
      +	if(options.parentMenu)
      +	{
      +		if( options.parentMenu.constructor !== this.constructor )
      +		{
      +			console.error("parentMenu must be of class ContextMenu, ignoring it");
      +			options.parentMenu = null;
      +		}
      +		else
      +		{
      +			this.parentMenu = options.parentMenu;
      +			this.parentMenu.lock = true;
      +			this.parentMenu.current_submenu = this;
      +		}
      +	}
      +
      +	if(options.event && options.event.constructor !== MouseEvent && options.event.constructor !== CustomEvent)
      +	{
      +		console.error("Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it.");
      +		options.event = null;
      +	}
      +
      +	var root = document.createElement("div");
      +	root.className = "litegraph litecontextmenu litemenubar-panel";
      +	root.style.minWidth = 100;
      +	root.style.minHeight = 100;
      +	root.style.pointerEvents = "none";
      +	setTimeout( function() { root.style.pointerEvents = "auto"; },100); //delay so the mouse up event is not caugh by this element
      +
      +	//this prevents the default context browser menu to open in case this menu was created when pressing right button
      +	root.addEventListener("mouseup", function(e){
      +		e.preventDefault(); return true;
      +	}, true);
      +	root.addEventListener("contextmenu", function(e) {
      +		if(e.button != 2) //right button
      +			return false;
      +		e.preventDefault();
      +		return false;
      +	},true);
      +
      +	root.addEventListener("mousedown", function(e){
      +		if(e.button == 2)
      +		{
      +			that.close();
      +			e.preventDefault(); return true;
      +		}
      +	}, true);
      +
      +	function on_mouse_wheel(e)
      +	{
      +		var pos = parseInt( root.style.top );
      +		root.style.top = (pos + e.deltaY * 0.1).toFixed() + "px";
      +		e.preventDefault();
      +		return true;
      +	}
      +
      +	root.addEventListener("wheel", on_mouse_wheel, true);
      +	root.addEventListener("mousewheel", on_mouse_wheel, true);
      +
      +
      +	this.root = root;
      +
      +	//title
      +	if(options.title)
      +	{
      +		var element = document.createElement("div");
      +		element.className = "litemenu-title";
      +		element.innerHTML = options.title;
      +		root.appendChild(element);
      +	}
      +
      +	//entries
      +	var num = 0;
      +	for(var i in values)
      +	{
      +		var name = values.constructor == Array ? values[i] : i;
      +		if( name != null && name.constructor !== String )
      +			name = name.content === undefined ? String(name) : name.content;
      +		var value = values[i];
      +		this.addItem( name, value, options );
      +		num++;
      +	}
      +
      +	//close on leave
      +	root.addEventListener("mouseleave", function(e) {
      +		if(that.lock)
      +			return;
      +		that.close(e);
      +	});
      +
      +	//insert before checking position
      +	var root_document = document;
      +	if(options.event)
      +		root_document = options.event.target.ownerDocument;
      +
      +	if(!root_document)
      +		root_document = document;
      +	root_document.body.appendChild(root);
      +
      +	//compute best position
      +	var left = options.left || 0;
      +	var top = options.top || 0;
      +	if(options.event)
      +	{
      +		left = (options.event.pageX - 10);
      +		top = (options.event.pageY - 10);
      +		if(options.title)
      +			top -= 20;
      +
      +		if(options.parentMenu)
      +		{
      +			var rect = options.parentMenu.root.getBoundingClientRect();
      +			left = rect.left + rect.width;
      +		}
      +
      +		var body_rect = document.body.getBoundingClientRect();
      +		var root_rect = root.getBoundingClientRect();
      +
      +		if(left > (body_rect.width - root_rect.width - 10))
      +			left = (body_rect.width - root_rect.width - 10);
      +		if(top > (body_rect.height - root_rect.height - 10))
      +			top = (body_rect.height - root_rect.height - 10);
      +	}
      +
      +	root.style.left = left + "px";
      +	root.style.top = top  + "px";
      +}
      +
      +ContextMenu.prototype.addItem = function( name, value, options )
      +{
      +	var that = this;
      +	options = options || {};
      +
      +	var element = document.createElement("div");
      +	element.className = "litemenu-entry submenu";
      +
      +	var disabled = false;
      +
      +	if(value === null)
      +	{
      +		element.classList.add("separator");
      +		//element.innerHTML = "<hr/>"
      +		//continue;
      +	}
      +	else
      +	{
      +		element.innerHTML = value && value.title ? value.title : name;
      +		element.value = value;
      +
      +		if(value)
      +		{
      +			if(value.disabled)
      +			{
      +				disabled = true;
      +				element.classList.add("disabled");
      +			}
      +			if(value.submenu || value.has_submenu)
      +				element.classList.add("has_submenu");
      +		}
      +
      +		if(typeof(value) == "function")
      +		{
      +			element.dataset["value"] = name;
      +			element.onclick_callback = value;
      +		}
      +		else
      +			element.dataset["value"] = value;
      +
      +		if(value.className)
      +			element.className += " " + value.className;
      +	}
      +
      +	this.root.appendChild(element);
      +	if(!disabled)
      +		element.addEventListener("click", inner_onclick);
      +	if(options.autoopen)
      +		element.addEventListener("mouseenter", inner_over);
      +
      +	function inner_over(e)
      +	{
      +		var value = this.value;
      +		if(!value || !value.has_submenu)
      +			return;
      +		inner_onclick.call(this,e);
      +	}
      +
      +	//menu option clicked
      +	function inner_onclick(e) {
      +		var value = this.value;
      +		var close_parent = true;
      +
      +		if(that.current_submenu)
      +			that.current_submenu.close(e);
      +
      +		//global callback
      +		if(options.callback)
      +		{
      +			var r = options.callback.call( this, value, options, e, that, options.node );
      +			if(r === true)
      +				close_parent = false;
      +		}
      +
      +		//special cases
      +		if(value)
      +		{
      +			if (value.callback && !options.ignore_item_callbacks && value.disabled !== true )  //item callback
      +			{
      +				var r = value.callback.call( this, value, options, e, that, options.node );
      +				if(r === true)
      +					close_parent = false;
      +			}
      +			if(value.submenu)
      +			{
      +				if(!value.submenu.options)
      +					throw("ContextMenu submenu needs options");
      +				var submenu = new that.constructor( value.submenu.options, {
      +					callback: value.submenu.callback,
      +					event: e,
      +					parentMenu: that,
      +					ignore_item_callbacks: value.submenu.ignore_item_callbacks,
      +					title: value.submenu.title,
      +					autoopen: options.autoopen
      +				});
      +				close_parent = false;
      +			}
      +		}
      +
      +		if(close_parent && !that.lock)
      +			that.close();
      +	}
      +
      +	return element;
      +}
      +
      +ContextMenu.prototype.close = function(e, ignore_parent_menu)
      +{
      +	if(this.root.parentNode)
      +		this.root.parentNode.removeChild( this.root );
      +	if(this.parentMenu && !ignore_parent_menu)
      +	{
      +		this.parentMenu.lock = false;
      +		this.parentMenu.current_submenu = null;
      +		if( e === undefined )
      +			this.parentMenu.close();
      +		else if( e && !ContextMenu.isCursorOverElement( e, this.parentMenu.root) )
      +		{
      +			ContextMenu.trigger( this.parentMenu.root, "mouseleave", e );
      +		}
      +	}
      +	if(this.current_submenu)
      +		this.current_submenu.close(e, true);
      +}
      +
      +//this code is used to trigger events easily (used in the context menu mouseleave
      +ContextMenu.trigger = function(element, event_name, params, origin)
      +{
      +	var evt = document.createEvent( 'CustomEvent' );
      +	evt.initCustomEvent( event_name, true,true, params ); //canBubble, cancelable, detail
      +	evt.srcElement = origin;
      +	if( element.dispatchEvent )
      +		element.dispatchEvent( evt );
      +	else if( element.__events )
      +		element.__events.dispatchEvent( evt );
      +	//else nothing seems binded here so nothing to do
      +	return evt;
      +}
      +
      +//returns the top most menu
      +ContextMenu.prototype.getTopMenu = function()
      +{
      +	if( this.options.parentMenu )
      +		return this.options.parentMenu.getTopMenu();
      +	return this;
      +}
      +
      +ContextMenu.prototype.getFirstEvent = function()
      +{
      +	if( this.options.parentMenu )
      +		return this.options.parentMenu.getFirstEvent();
      +	return this.options.event;
      +}
      +
      +
      +
      +ContextMenu.isCursorOverElement = function( event, element )
      +{
      +	var left = event.pageX;
      +	var top = event.pageY;
      +	var rect = element.getBoundingClientRect();
      +	if(!rect)
      +		return false;
      +	if(top > rect.top && top < (rect.top + rect.height) &&
      +		left > rect.left && left < (rect.left + rect.width) )
      +		return true;
      +	return false;
      +}
      +
      +
      +
      +LiteGraph.ContextMenu = ContextMenu;
      +
      +LiteGraph.closeAllContextMenus = function( ref_window )
      +{
      +	ref_window = ref_window || window;
      +
      +	var elements = ref_window.document.querySelectorAll(".litecontextmenu");
      +	if(!elements.length)
      +		return;
      +
      +	var result = [];
      +	for(var i = 0; i < elements.length; i++)
      +		result.push(elements[i]);
      +
      +	for(var i in result)
      +	{
      +		if(result[i].close)
      +			result[i].close();
      +		else if(result[i].parentNode)
      +			result[i].parentNode.removeChild( result[i] );
      +	}
      +}
      +
      +LiteGraph.extendClass = function ( target, origin )
      +{
      +	for(var i in origin) //copy class properties
      +	{
      +		if(target.hasOwnProperty(i))
      +			continue;
      +		target[i] = origin[i];
      +	}
      +
      +	if(origin.prototype) //copy prototype properties
      +		for(var i in origin.prototype) //only enumerables
      +		{
      +			if(!origin.prototype.hasOwnProperty(i))
      +				continue;
      +
      +			if(target.prototype.hasOwnProperty(i)) //avoid overwritting existing ones
      +				continue;
      +
      +			//copy getters
      +			if(origin.prototype.__lookupGetter__(i))
      +				target.prototype.__defineGetter__(i, origin.prototype.__lookupGetter__(i));
      +			else
      +				target.prototype[i] = origin.prototype[i];
      +
      +			//and setters
      +			if(origin.prototype.__lookupSetter__(i))
      +				target.prototype.__defineSetter__(i, origin.prototype.__lookupSetter__(i));
      +		}
      +}
      +
      +//used to create nodes from wrapping functions
      +LiteGraph.getParameterNames = function(func) {
      +    return (func + '')
      +      .replace(/[/][/].*$/mg,'') // strip single-line comments
      +      .replace(/\s+/g, '') // strip white space
      +      .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments  /**/
      +      .split('){', 1)[0].replace(/^[^(]*[(]/, '') // extract the parameters
      +      .replace(/=[^,]+/g, '') // strip any ES6 defaults
      +      .split(',').filter(Boolean); // split & filter [""]
      +}
      +
      +Math.clamp = function(v,a,b) { return (a > v ? a : (b < v ? b : v)); }
      +
      +if( typeof(window) != "undefined" && !window["requestAnimationFrame"] )
      +{
      +	window.requestAnimationFrame = window.webkitRequestAnimationFrame ||
      +		  window.mozRequestAnimationFrame    ||
      +		  (function( callback ){
      +			window.setTimeout(callback, 1000 / 60);
      +		  });
      +}
      +
      +})(this);
      +
      +if(typeof(exports) != "undefined")
      +	exports.LiteGraph = this.LiteGraph;
       
           
      diff --git a/src/litegraph-editor.js b/src/litegraph-editor.js index b7c638524..5219d29b0 100755 --- a/src/litegraph-editor.js +++ b/src/litegraph-editor.js @@ -182,6 +182,7 @@ Editor.prototype.addMiniWindow = function(w,h) miniwindow.className = "litegraph miniwindow"; miniwindow.innerHTML = ""; var canvas = miniwindow.querySelector("canvas"); + var that = this; var graphcanvas = new LGraphCanvas(canvas, this.graph); graphcanvas.show_info = false; @@ -189,12 +190,24 @@ Editor.prototype.addMiniWindow = function(w,h) graphcanvas.scale = 0.25; graphcanvas.allow_dragnodes = false; graphcanvas.allow_interaction = false; + graphcanvas.render_shadows = false; + graphcanvas.max_zoom = 0.25; this.miniwindow_graphcanvas = graphcanvas; graphcanvas.onClear = function() { graphcanvas.scale = 0.25; graphcanvas.allow_dragnodes = false; graphcanvas.allow_interaction = false; }; + graphcanvas.onRenderBackground = function(canvas, ctx) + { + ctx.strokeStyle = "#567"; + var tl = that.graphcanvas.convertOffsetToCanvas([0,0]); + var br = that.graphcanvas.convertOffsetToCanvas([that.graphcanvas.canvas.width,that.graphcanvas.canvas.height]); + tl = this.convertCanvasToOffset( tl ); + br = this.convertCanvasToOffset( br ); + ctx.lineWidth = 1; + ctx.strokeRect( Math.floor(tl[0]) + 0.5, Math.floor(tl[1]) + 0.5, Math.floor(br[0] - tl[0]), Math.floor(br[1] - tl[1]) ); + } miniwindow.style.position = "absolute"; miniwindow.style.top = "4px"; diff --git a/src/litegraph.js b/src/litegraph.js index c96854141..97cfa02d2 100755 --- a/src/litegraph.js +++ b/src/litegraph.js @@ -46,6 +46,7 @@ var LiteGraph = global.LiteGraph = { VALID_SHAPES: ["default","box","round","card"], //,"circle" + //shapes are used for nodes but also for slots BOX_SHAPE: 1, ROUND_SHAPE: 2, CIRCLE_SHAPE: 3, @@ -2716,8 +2717,8 @@ LGraphNode.prototype.connect = function( slot, target_node, target_slot ) target_node.disconnectInput( target_slot ); //why here?? - this.setDirtyCanvas(false,true); - this.graph.connectionChange( this ); + //this.setDirtyCanvas(false,true); + //this.graph.connectionChange( this ); var output = this.outputs[slot]; @@ -2754,6 +2755,8 @@ LGraphNode.prototype.connect = function( slot, target_node, target_slot ) this.onConnectionsChange( LiteGraph.OUTPUT, slot, true, link_info, output ); //link_info has been created now, so its updated if(target_node.onConnectionsChange) target_node.onConnectionsChange( LiteGraph.INPUT, target_slot, true, link_info, input ); + if( this.graph && this.graph.onNodeConnectionChange ) + this.graph.onNodeConnectionChange( LiteGraph.OUTPUT, this, slot, target_node, target_slot ); } this.setDirtyCanvas(false,true); @@ -2793,7 +2796,7 @@ LGraphNode.prototype.disconnectOutput = function( slot, target_node ) if(!output.links || output.links.length == 0) return false; - //one of the links + //one of the output links in this slot if(target_node) { if(target_node.constructor === Number) @@ -2819,11 +2822,15 @@ LGraphNode.prototype.disconnectOutput = function( slot, target_node ) target_node.onConnectionsChange( LiteGraph.INPUT, link_info.target_slot, false, link_info, input ); //link_info hasnt been modified so its ok if(this.onConnectionsChange) this.onConnectionsChange( LiteGraph.OUTPUT, slot, false, link_info, output ); + if( this.graph && this.graph.onNodeConnectionChange ) + this.graph.onNodeConnectionChange( LiteGraph.OUTPUT, this, slot ); + if( this.graph && this.graph.onNodeConnectionChange ) + this.graph.onNodeConnectionChange( LiteGraph.INPUT, target_node, link_info.target_slot ); break; } } } - else //all the links + else //all the links in this output slot { for(var i = 0, l = output.links.length; i < l; i++) { @@ -2842,10 +2849,14 @@ LGraphNode.prototype.disconnectOutput = function( slot, target_node ) input.link = null; //remove other side link if(target_node.onConnectionsChange) target_node.onConnectionsChange( LiteGraph.INPUT, link_info.target_slot, false, link_info, input ); //link_info hasnt been modified so its ok + if( this.graph && this.graph.onNodeConnectionChange ) + this.graph.onNodeConnectionChange( LiteGraph.INPUT, target_node, link_info.target_slot ); } delete this.graph.links[ link_id ]; //remove the link from the links pool if(this.onConnectionsChange) this.onConnectionsChange( LiteGraph.OUTPUT, slot, false, link_info, output ); + if( this.graph && this.graph.onNodeConnectionChange ) + this.graph.onNodeConnectionChange( LiteGraph.OUTPUT, this, slot ); } output.links = null; } @@ -3200,7 +3211,7 @@ LGraphGroup.prototype.setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas; //********************************************************************************* /** -* The Global Scope. It contains all the registered node classes. +* This class is in charge of rendering one graph inside a canvas. And provides all the interaction required. * Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked * * @class LGraphCanvas @@ -3484,6 +3495,10 @@ LGraphCanvas.prototype.setCanvas = function( canvas, skip_events ) LGraphCanvas.prototype._doNothing = function doNothing(e) { e.preventDefault(); return false; }; LGraphCanvas.prototype._doReturnTrue = function doNothing(e) { e.preventDefault(); return true; }; +/** +* binds mouse, keyboard, touch and drag events to the canvas +* @method bindEvents +**/ LGraphCanvas.prototype.bindEvents = function() { if( this._events_binded ) @@ -3532,6 +3547,10 @@ LGraphCanvas.prototype.bindEvents = function() this._events_binded = true; } +/** +* unbinds mouse events from the canvas +* @method unbindEvents +**/ LGraphCanvas.prototype.unbindEvents = function() { if( !this._events_binded ) @@ -3576,8 +3595,11 @@ LGraphCanvas.getFileExtension = function (url) return url.substr(point+1).toLowerCase(); } -//this file allows to render the canvas using WebGL instead of Canvas2D -//this is useful if you plant to render 3D objects inside your nodes +/** +* this function allows to render the canvas using WebGL instead of Canvas2D +* this is useful if you plant to render 3D objects inside your nodes, it uses litegl.js for webgl and canvas2DtoWebGL to emulate the Canvas2D calls in webGL +* @method enableWebGL +**/ LGraphCanvas.prototype.enableWebGL = function() { if(typeof(GL) === undefined) @@ -3599,55 +3621,6 @@ LGraphCanvas.prototype.enableWebGL = function() } -/* -LGraphCanvas.prototype.UIinit = function() -{ - var that = this; - $("#node-console input").change(function(e) - { - if(e.target.value == "") - return; - - var node = that.node_in_panel; - if(!node) - return; - - node.trace("] " + e.target.value, "#333"); - if(node.onConsoleCommand) - { - if(!node.onConsoleCommand(e.target.value)) - node.trace("command not found", "#A33"); - } - else if (e.target.value == "info") - { - node.trace("Special methods:"); - for(var i in node) - { - if(typeof(node[i]) == "function" && LGraphNode.prototype[i] == null && i.substr(0,2) != "on" && i[0] != "_") - node.trace(" + " + i); - } - } - else - { - try - { - eval("var _foo = function() { return ("+e.target.value+"); }"); - var result = _foo.call(node); - if(result) - node.trace(result.toString()); - delete window._foo; - } - catch(err) - { - node.trace("error: " + err, "#A33"); - } - } - - this.value = ""; - }); -} -*/ - /** * marks as dirty the canvas, this way it will be rendered again * @@ -3945,6 +3918,10 @@ LGraphCanvas.prototype.processMouseDown = function(e) return false; } +/** +* Called when a mouse move event has to be processed +* @method processMouseMove +**/ LGraphCanvas.prototype.processMouseMove = function(e) { if(this.autoresize) @@ -4132,6 +4109,10 @@ LGraphCanvas.prototype.processMouseMove = function(e) //this.graph.change(); } +/** +* Called when a mouse up event has to be processed +* @method processMouseUp +**/ LGraphCanvas.prototype.processMouseUp = function(e) { if(!this.graph) @@ -4279,7 +4260,10 @@ LGraphCanvas.prototype.processMouseUp = function(e) return false; } - +/** +* Called when a mouse wheel event has to be processed +* @method processMouseWheel +**/ LGraphCanvas.prototype.processMouseWheel = function(e) { if(!this.graph || !this.allow_dragcanvas) @@ -4309,6 +4293,10 @@ LGraphCanvas.prototype.processMouseWheel = function(e) return false; // prevent default } +/** +* retuns true if a position (in graph space) is on top of a node little corner box +* @method isOverNodeBox +**/ LGraphCanvas.prototype.isOverNodeBox = function( node, canvasx, canvasy ) { var title_height = LiteGraph.NODE_TITLE_HEIGHT; @@ -4317,6 +4305,10 @@ LGraphCanvas.prototype.isOverNodeBox = function( node, canvasx, canvasy ) return false; } +/** +* retuns true if a position (in graph space) is on top of a node input slot +* @method isOverNodeInput +**/ LGraphCanvas.prototype.isOverNodeInput = function(node, canvasx, canvasy, slot_pos ) { if(node.inputs) @@ -4337,6 +4329,10 @@ LGraphCanvas.prototype.isOverNodeInput = function(node, canvasx, canvasy, slot_p return -1; } +/** +* process a key event +* @method processKey +**/ LGraphCanvas.prototype.processKey = function(e) { if(!this.graph) @@ -4486,6 +4482,10 @@ LGraphCanvas.prototype.pasteFromClipboard = function() this.selectNodes( nodes ); } +/** +* process a item drop event on top the canvas +* @method processDrop +**/ LGraphCanvas.prototype.processDrop = function(e) { e.preventDefault(); @@ -4600,6 +4600,10 @@ LGraphCanvas.prototype.processNodeDeselected = function(node) this.onNodeDeselected(node); } +/** +* selects a given node (or adds it to the current selection) +* @method selectNode +**/ LGraphCanvas.prototype.selectNode = function( node, add_to_current_selection ) { if(node == null) @@ -4608,6 +4612,10 @@ LGraphCanvas.prototype.selectNode = function( node, add_to_current_selection ) this.selectNodes([node], add_to_current_selection ); } +/** +* selects several nodes (or adds them to the current selection) +* @method selectNodes +**/ LGraphCanvas.prototype.selectNodes = function( nodes, add_to_current_selection ) { if(!add_to_current_selection) @@ -4642,6 +4650,10 @@ LGraphCanvas.prototype.selectNodes = function( nodes, add_to_current_selection ) this.setDirty(true); } +/** +* removes a node from the current selection +* @method deselectNode +**/ LGraphCanvas.prototype.deselectNode = function( node ) { if(!node.selected) @@ -4664,6 +4676,10 @@ LGraphCanvas.prototype.deselectNode = function( node ) } } +/** +* removes all nodes from the current selection +* @method deselectAllNodes +**/ LGraphCanvas.prototype.deselectAllNodes = function() { if(!this.graph) @@ -4683,6 +4699,10 @@ LGraphCanvas.prototype.deselectAllNodes = function() this.setDirty(true); } +/** +* deletes all nodes in the current selection from the graph +* @method deleteSelectedNodes +**/ LGraphCanvas.prototype.deleteSelectedNodes = function() { for(var i in this.selected_nodes) @@ -4696,6 +4716,10 @@ LGraphCanvas.prototype.deleteSelectedNodes = function() this.setDirty(true); } +/** +* centers the camera on a given node +* @method centerOnNode +**/ LGraphCanvas.prototype.centerOnNode = function(node) { this.offset[0] = -node.pos[0] - node.size[0] * 0.5 + (this.canvas.width * 0.5 / this.scale); @@ -4703,6 +4727,10 @@ LGraphCanvas.prototype.centerOnNode = function(node) this.setDirty(true,true); } +/** +* adds some useful properties to a mouse event, like the position in graph coordinates +* @method adjustMouseEvent +**/ LGraphCanvas.prototype.adjustMouseEvent = function(e) { if(this.canvas) @@ -4727,6 +4755,10 @@ LGraphCanvas.prototype.adjustMouseEvent = function(e) e.canvasY = e.localY / this.scale - this.offset[1]; } +/** +* changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom +* @method setZoom +**/ LGraphCanvas.prototype.setZoom = function(value, zooming_center) { if(!zooming_center && this.canvas) @@ -4751,6 +4783,10 @@ LGraphCanvas.prototype.setZoom = function(value, zooming_center) this.dirty_bgcanvas = true; } +/** +* converts a coordinate in canvas2D space to graphcanvas space (NAME IS CONFUSION, SHOULD BE THE OTHER WAY AROUND) +* @method convertOffsetToCanvas +**/ LGraphCanvas.prototype.convertOffsetToCanvas = function( pos, out ) { out = out || []; @@ -4759,6 +4795,10 @@ LGraphCanvas.prototype.convertOffsetToCanvas = function( pos, out ) return out; } +/** +* converts a coordinate in graphcanvas space to canvas2D space (NAME IS CONFUSION, SHOULD BE THE OTHER WAY AROUND) +* @method convertCanvasToOffset +**/ LGraphCanvas.prototype.convertCanvasToOffset = function( pos, out ) { out = out || []; @@ -4773,22 +4813,30 @@ LGraphCanvas.prototype.convertEventToCanvas = function(e) return this.convertOffsetToCanvas([e.pageX - rect.left,e.pageY - rect.top]); } -LGraphCanvas.prototype.bringToFront = function(n) +/** +* brings a node to front (above all other nodes) +* @method bringToFront +**/ +LGraphCanvas.prototype.bringToFront = function(node) { - var i = this.graph._nodes.indexOf(n); + var i = this.graph._nodes.indexOf(node); if(i == -1) return; this.graph._nodes.splice(i,1); - this.graph._nodes.push(n); + this.graph._nodes.push(node); } -LGraphCanvas.prototype.sendToBack = function(n) +/** +* sends a node to the back (below all other nodes) +* @method sendToBack +**/ +LGraphCanvas.prototype.sendToBack = function(node) { - var i = this.graph._nodes.indexOf(n); + var i = this.graph._nodes.indexOf(node); if(i == -1) return; this.graph._nodes.splice(i,1); - this.graph._nodes.unshift(n); + this.graph._nodes.unshift(node); } /* Interaction */ @@ -4798,6 +4846,10 @@ LGraphCanvas.prototype.sendToBack = function(n) /* LGraphCanvas render */ var temp = new Float32Array(4); +/** +* checks which nodes are visible (inside the camera area) +* @method computeVisibleNodes +**/ LGraphCanvas.prototype.computeVisibleNodes = function( nodes, out ) { var visible_nodes = out || []; @@ -4819,6 +4871,10 @@ LGraphCanvas.prototype.computeVisibleNodes = function( nodes, out ) return visible_nodes; } +/** +* renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes) +* @method draw +**/ LGraphCanvas.prototype.draw = function(force_canvas, force_bgcanvas) { if(!this.canvas) @@ -4846,6 +4902,10 @@ LGraphCanvas.prototype.draw = function(force_canvas, force_bgcanvas) this.frame += 1; } +/** +* draws the front canvas (the one containing all the nodes) +* @method drawFrontCanvas +**/ LGraphCanvas.prototype.drawFrontCanvas = function() { this.dirty_canvas = false; @@ -4975,6 +5035,10 @@ LGraphCanvas.prototype.drawFrontCanvas = function() ctx.finish2D(); } +/** +* draws some useful stats in the corner of the canvas +* @method renderInfo +**/ LGraphCanvas.prototype.renderInfo = function( ctx, x, y ) { x = x || 0; @@ -4997,6 +5061,10 @@ LGraphCanvas.prototype.renderInfo = function( ctx, x, y ) ctx.restore(); } +/** +* draws the back canvas (the one containing the background and the connections) +* @method drawBackCanvas +**/ LGraphCanvas.prototype.drawBackCanvas = function() { var canvas = this.bgcanvas; @@ -5027,7 +5095,7 @@ LGraphCanvas.prototype.drawBackCanvas = function() var bg_already_painted = false; if(this.onRenderBackground) - bg_already_painted = this.onRenderBackground(); + bg_already_painted = this.onRenderBackground( canvas, ctx ); //reset in case of error ctx.restore(); @@ -5125,7 +5193,10 @@ LGraphCanvas.prototype.drawBackCanvas = function() var temp_vec2 = new Float32Array(2); -/* Renders the LGraphNode on the canvas */ +/** +* draws the given node inside the canvas +* @method drawNode +**/ LGraphCanvas.prototype.drawNode = function(node, ctx ) { var glow = false; @@ -5393,7 +5464,10 @@ LGraphCanvas.prototype.drawNode = function(node, ctx ) ctx.globalAlpha = 1.0; } -/* Renders the node shape */ +/** +* draws the shape of the given node in the canvas +* @method drawNodeShape +**/ LGraphCanvas.prototype.drawNodeShape = function( node, ctx, size, fgcolor, bgcolor, selected, mouse_over ) { //bg rect @@ -5562,7 +5636,11 @@ LGraphCanvas.prototype.drawNodeShape = function( node, ctx, size, fgcolor, bgcol } } -//OPTIMIZE THIS: precatch connections position instead of recomputing them every time +/** +* draws every connection visible in the canvas +* OPTIMIZE THIS: precatch connections position instead of recomputing them every time +* @method drawConnections +**/ LGraphCanvas.prototype.drawConnections = function(ctx) { var now = LiteGraph.getTime(); @@ -5613,6 +5691,10 @@ LGraphCanvas.prototype.drawConnections = function(ctx) ctx.globalAlpha = 1; } +/** +* draws a link between two points +* @method renderLink +**/ LGraphCanvas.prototype.renderLink = function( ctx, a, b, link, skip_border, flow, color ) { if(!this.highquality_render) @@ -5730,6 +5812,10 @@ LGraphCanvas.prototype.computeConnectionPoint = function(a,b,t) return [x,y]; } +/** +* draws the widgets stored inside a node +* @method drawNodeWidgets +**/ LGraphCanvas.prototype.drawNodeWidgets = function( node, posY, ctx, active_widget ) { if(!node.widgets || !node.widgets.length) @@ -5822,6 +5908,10 @@ LGraphCanvas.prototype.drawNodeWidgets = function( node, posY, ctx, active_widge ctx.textAlign = "left"; } +/** +* process an event on widgets +* @method processNodeWidgets +**/ LGraphCanvas.prototype.processNodeWidgets = function( node, pos, event, active_widget ) { if(!node.widgets || !node.widgets.length) @@ -5897,7 +5987,10 @@ LGraphCanvas.prototype.processNodeWidgets = function( node, pos, event, active_w return null; } - +/** +* draws every group area in the background +* @method drawGroups +**/ LGraphCanvas.prototype.drawGroups = function(canvas, ctx) { if(!this.graph) @@ -5939,7 +6032,10 @@ LGraphCanvas.prototype.drawGroups = function(canvas, ctx) ctx.restore(); } - +/** +* resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode +* @method resize +**/ LGraphCanvas.prototype.resize = function(width, height) { if(!width && !height) @@ -5959,7 +6055,11 @@ LGraphCanvas.prototype.resize = function(width, height) this.setDirty(true,true); } - +/** +* switches to live mode (node shapes are not rendered, only the content) +* this feature was designed when graphs where meant to create user interfaces +* @method switchLiveMode +**/ LGraphCanvas.prototype.switchLiveMode = function(transition) { if(!transition) @@ -6000,7 +6100,6 @@ LGraphCanvas.prototype.switchLiveMode = function(transition) LGraphCanvas.prototype.onNodeSelectionChange = function(node) { return; //disabled - //if(this.node_in_panel) this.showNodePanel(node); } LGraphCanvas.prototype.touchHandler = function(event)