You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
14132 lines
468 KiB
14132 lines
468 KiB
//packer version |
|
|
|
|
|
(function(global) { |
|
// ************************************************************* |
|
// LiteGraph CLASS ******* |
|
// ************************************************************* |
|
|
|
/** |
|
* The Global Scope. It contains all the registered node classes. |
|
* |
|
* @class LiteGraph |
|
* @constructor |
|
*/ |
|
|
|
var LiteGraph = (global.LiteGraph = { |
|
VERSION: 0.4, |
|
|
|
CANVAS_GRID_SIZE: 10, |
|
|
|
NODE_TITLE_HEIGHT: 30, |
|
NODE_TITLE_TEXT_Y: 20, |
|
NODE_SLOT_HEIGHT: 20, |
|
NODE_WIDGET_HEIGHT: 20, |
|
NODE_WIDTH: 140, |
|
NODE_MIN_WIDTH: 50, |
|
NODE_COLLAPSED_RADIUS: 10, |
|
NODE_COLLAPSED_WIDTH: 80, |
|
NODE_TITLE_COLOR: "#999", |
|
NODE_SELECTED_TITLE_COLOR: "#FFF", |
|
NODE_TEXT_SIZE: 14, |
|
NODE_TEXT_COLOR: "#AAA", |
|
NODE_SUBTEXT_SIZE: 12, |
|
NODE_DEFAULT_COLOR: "#333", |
|
NODE_DEFAULT_BGCOLOR: "#353535", |
|
NODE_DEFAULT_BOXCOLOR: "#666", |
|
NODE_DEFAULT_SHAPE: "box", |
|
NODE_BOX_OUTLINE_COLOR: "#FFF", |
|
DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)", |
|
DEFAULT_GROUP_FONT: 24, |
|
|
|
WIDGET_BGCOLOR: "#222", |
|
WIDGET_OUTLINE_COLOR: "#666", |
|
WIDGET_TEXT_COLOR: "#DDD", |
|
WIDGET_SECONDARY_TEXT_COLOR: "#999", |
|
|
|
LINK_COLOR: "#9A9", |
|
EVENT_LINK_COLOR: "#A86", |
|
CONNECTING_LINK_COLOR: "#AFA", |
|
|
|
MAX_NUMBER_OF_NODES: 1000, //avoid infinite loops |
|
DEFAULT_POSITION: [100, 100], //default node position |
|
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, |
|
GRID_SHAPE: 6, // intended for slot arrays |
|
|
|
//enums |
|
INPUT: 1, |
|
OUTPUT: 2, |
|
|
|
EVENT: -1, //for outputs |
|
ACTION: -1, //for inputs |
|
|
|
NODE_MODES: ["Always", "On Event", "Never", "On Trigger"], // helper, will add "On Request" and more in the future |
|
NODE_MODES_COLORS:["#666","#422","#333","#224","#626"], // use with node_box_coloured_by_mode |
|
ALWAYS: 0, |
|
ON_EVENT: 1, |
|
NEVER: 2, |
|
ON_TRIGGER: 3, |
|
|
|
UP: 1, |
|
DOWN: 2, |
|
LEFT: 3, |
|
RIGHT: 4, |
|
CENTER: 5, |
|
|
|
LINK_RENDER_MODES: ["Straight", "Linear", "Spline"], // helper |
|
STRAIGHT_LINK: 0, |
|
LINEAR_LINK: 1, |
|
SPLINE_LINK: 2, |
|
|
|
NORMAL_TITLE: 0, |
|
NO_TITLE: 1, |
|
TRANSPARENT_TITLE: 2, |
|
AUTOHIDE_TITLE: 3, |
|
|
|
proxy: null, //used to redirect calls |
|
node_images_path: "", |
|
|
|
debug: false, |
|
catch_exceptions: true, |
|
throw_errors: true, |
|
allow_scripts: false, //if set to true some nodes like Formula would be allowed to evaluate code that comes from unsafe sources (like node configuration), which could lead to exploits |
|
registered_node_types: {}, //nodetypes by string |
|
node_types_by_file_extension: {}, //used for dropping files in the canvas |
|
Nodes: {}, //node types by classname |
|
Globals: {}, //used to store vars between graphs |
|
|
|
searchbox_extras: {}, //used to add extra features to the search box |
|
auto_sort_node_types: false, // [true!] If set to true, will automatically sort node types / categories in the context menus |
|
|
|
node_box_coloured_when_on: false, // [true!] this make the nodes box (top left circle) coloured when triggered (execute/action), visual feedback |
|
node_box_coloured_by_mode: false, // [true!] nodebox based on node mode, visual feedback |
|
|
|
dialog_close_on_mouse_leave: true, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false |
|
dialog_close_on_mouse_leave_delay: 500, |
|
|
|
shift_click_do_break_link_from: false, // [false!] prefer false if results too easy to break links - implement with ALT or TODO custom keys |
|
click_do_break_link_to: false, // [false!]prefer false, way too easy to break links |
|
|
|
search_hide_on_mouse_leave: true, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false |
|
search_filter_enabled: false, // [true!] enable filtering slots type in the search widget, !requires auto_load_slot_types or manual set registered_slot_[in/out]_types and slot_types_[in/out] |
|
search_show_all_on_open: true, // [true!] opens the results list when opening the search widget |
|
|
|
auto_load_slot_types: false, // [if want false, use true, run, get vars values to be statically set, than disable] nodes types and nodeclass association with node types need to be calculated, if dont want this, calculate once and set registered_slot_[in/out]_types and slot_types_[in/out] |
|
|
|
// set these values if not using auto_load_slot_types |
|
registered_slot_in_types: {}, // slot types for nodeclass |
|
registered_slot_out_types: {}, // slot types for nodeclass |
|
slot_types_in: [], // slot types IN |
|
slot_types_out: [], // slot types OUT |
|
slot_types_default_in: [], // specify for each IN slot type a(/many) deafult node(s), use single string, array, or object (with node, title, parameters, ..) like for search |
|
slot_types_default_out: [], // specify for each OUT slot type a(/many) deafult node(s), use single string, array, or object (with node, title, parameters, ..) like for search |
|
|
|
alt_drag_do_clone_nodes: false, // [true!] very handy, ALT click to clone and drag the new node |
|
|
|
do_add_triggers_slots: false, // [true!] will create and connect event slots when using action/events connections, !WILL CHANGE node mode when using onTrigger (enable mode colors), onExecuted does not need this |
|
|
|
allow_multi_output_for_events: true, // [false!] being events, it is strongly reccomended to use them sequentually, one by one |
|
|
|
middle_click_slot_add_default_node: false, //[true!] allows to create and connect a ndoe clicking with the third button (wheel) |
|
|
|
release_link_on_empty_shows_menu: false, //[true!] dragging a link to empty space will open a menu, add from list, search or defaults |
|
|
|
pointerevents_method: "pointer", // "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now) |
|
// TODO implement pointercancel, gotpointercapture, lostpointercapture, (pointerover, pointerout if necessary) |
|
|
|
/** |
|
* 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]; |
|
} |
|
} |
|
} |
|
|
|
var prev = this.registered_node_types[type]; |
|
if(prev) |
|
console.log("replacing node type: " + type); |
|
else |
|
{ |
|
if( !Object.hasOwnProperty( base_class.prototype, "shape") ) |
|
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, |
|
configurable: true |
|
}); |
|
|
|
//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" |
|
); |
|
} |
|
|
|
//used to know which nodes create when dragging files to the canvas |
|
if (base_class.supported_extensions) { |
|
for (var i in base_class.supported_extensions) { |
|
var ext = base_class.supported_extensions[i]; |
|
if(ext && ext.constructor === String) |
|
this.node_types_by_file_extension[ ext.toLowerCase() ] = base_class; |
|
} |
|
} |
|
} |
|
|
|
this.registered_node_types[type] = base_class; |
|
if (base_class.constructor.name) { |
|
this.Nodes[classname] = base_class; |
|
} |
|
if (LiteGraph.onNodeTypeRegistered) { |
|
LiteGraph.onNodeTypeRegistered(type, base_class); |
|
} |
|
if (prev && LiteGraph.onNodeTypeReplaced) { |
|
LiteGraph.onNodeTypeReplaced(type, base_class, prev); |
|
} |
|
|
|
//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" |
|
); |
|
} |
|
|
|
//used to know which nodes create when dragging files to the canvas |
|
if (base_class.supported_extensions) { |
|
for (var i=0; i < base_class.supported_extensions.length; i++) { |
|
var ext = base_class.supported_extensions[i]; |
|
if(ext && ext.constructor === String) |
|
this.node_types_by_file_extension[ ext.toLowerCase() ] = base_class; |
|
} |
|
} |
|
|
|
// TODO one would want to know input and ouput :: this would allow trought registerNodeAndSlotType to get all the slots types |
|
//console.debug("Registering "+type); |
|
if (this.auto_load_slot_types) nodeTmp = new base_class(base_class.title || "tmpnode"); |
|
}, |
|
|
|
/** |
|
* removes a node type from the system |
|
* @method unregisterNodeType |
|
* @param {String|Object} type name of the node or the node constructor itself |
|
*/ |
|
unregisterNodeType: function(type) { |
|
var base_class = type.constructor === String ? this.registered_node_types[type] : type; |
|
if(!base_class) |
|
throw("node type not found: " + type ); |
|
delete this.registered_node_types[base_class.type]; |
|
if(base_class.constructor.name) |
|
delete this.Nodes[base_class.constructor.name]; |
|
}, |
|
|
|
/** |
|
* Save a slot type and his node |
|
* @method registerSlotType |
|
* @param {String|Object} type name of the node or the node constructor itself |
|
* @param {String} slot_type name of the slot type (variable type), eg. string, number, array, boolean, .. |
|
*/ |
|
registerNodeAndSlotType: function(type,slot_type,out){ |
|
out = out || false; |
|
var base_class = type.constructor === String && this.registered_node_types[type] !== "anonymous" ? this.registered_node_types[type] : type; |
|
|
|
var sCN = base_class.constructor.type; |
|
|
|
if (typeof slot_type == "string"){ |
|
var aTypes = slot_type.split(","); |
|
}else if (slot_type == this.EVENT || slot_type == this.ACTION){ |
|
var aTypes = ["_event_"]; |
|
}else{ |
|
var aTypes = ["*"]; |
|
} |
|
|
|
for (var i = 0; i < aTypes.length; ++i) { |
|
var sT = aTypes[i]; //.toLowerCase(); |
|
if (sT === ""){ |
|
sT = "*"; |
|
} |
|
var registerTo = out ? "registered_slot_out_types" : "registered_slot_in_types"; |
|
if (typeof this[registerTo][sT] == "undefined") this[registerTo][sT] = {nodes: []}; |
|
this[registerTo][sT].nodes.push(sCN); |
|
|
|
// check if is a new type |
|
if (!out){ |
|
if (!this.slot_types_in.includes(sT.toLowerCase())){ |
|
this.slot_types_in.push(sT.toLowerCase()); |
|
this.slot_types_in.sort(); |
|
} |
|
}else{ |
|
if (!this.slot_types_out.includes(sT.toLowerCase())){ |
|
this.slot_types_out.push(sT.toLowerCase()); |
|
this.slot_types_out.sort(); |
|
} |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* Create a new nodetype by passing a function, it wraps it with a proper 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 |
|
* @param {Object} properties [optional] properties to be configurable |
|
*/ |
|
wrapFunctionAsNode: function( |
|
name, |
|
func, |
|
param_types, |
|
return_type, |
|
properties |
|
) { |
|
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"; |
|
if (properties) { |
|
code += |
|
"this.properties = " + JSON.stringify(properties) + ";\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); |
|
}, |
|
|
|
/** |
|
* Removes all previously registered node's types |
|
*/ |
|
clearRegisteredTypes: function() { |
|
this.registered_node_types = {}; |
|
this.node_types_by_file_extension = {}; |
|
this.Nodes = {}; |
|
this.searchbox_extras = {}; |
|
}, |
|
|
|
/** |
|
* 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 = null; |
|
|
|
if (LiteGraph.catch_exceptions) { |
|
try { |
|
node = new base_class(title); |
|
} catch (err) { |
|
console.error(err); |
|
return null; |
|
} |
|
} else { |
|
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(); |
|
//call onresize? |
|
} |
|
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]; |
|
} |
|
} |
|
|
|
// callback |
|
if ( node.onNodeCreated ) { |
|
node.onNodeCreated(); |
|
} |
|
|
|
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 (type.filter != filter) { |
|
continue; |
|
} |
|
|
|
if (category == "") { |
|
if (type.category == null) { |
|
r.push(type); |
|
} |
|
} else if (type.category == category) { |
|
r.push(type); |
|
} |
|
} |
|
|
|
if (this.auto_sort_node_types) { |
|
r.sort(function(a,b){return a.title.localeCompare(b.title)}); |
|
} |
|
|
|
return r; |
|
}, |
|
|
|
/** |
|
* Returns a list with all the node type categories |
|
* @method getNodeTypesCategories |
|
* @param {String} filter only nodes with ctor.filter equal can be shown |
|
* @return {Array} array with all the names of the categories |
|
*/ |
|
getNodeTypesCategories: function( filter ) { |
|
var categories = { "": 1 }; |
|
for (var i in this.registered_node_types) { |
|
var type = this.registered_node_types[i]; |
|
if ( type.category && !type.skip_list ) |
|
{ |
|
if(type.filter != filter) |
|
continue; |
|
categories[type.category] = 1; |
|
} |
|
} |
|
var result = []; |
|
for (var i in categories) { |
|
result.push(i); |
|
} |
|
return this.auto_sort_node_types ? result.sort() : result; |
|
}, |
|
|
|
//debug purposes: reloads all the js scripts that matches a wildcard |
|
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=0; i < tmp.length; i++) { |
|
script_files.push(tmp[i]); |
|
} |
|
|
|
var docHeadObj = document.getElementsByTagName("head")[0]; |
|
folder_wildcard = document.location.href + folder_wildcard; |
|
|
|
for (var i=0; i < script_files.length; i++) { |
|
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 doesn't 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; |
|
}, |
|
|
|
/** |
|
* Returns if the types of two slots are compatible (taking into account wildcards, etc) |
|
* @method isValidConnection |
|
* @param {String} type_a |
|
* @param {String} type_b |
|
* @return {Boolean} true if they can be connected |
|
*/ |
|
isValidConnection: function(type_a, type_b) { |
|
if (type_a=="" || type_a==="*") type_a = 0; |
|
if (type_b=="" || type_b==="*") type_b = 0; |
|
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(this.isValidConnection(supported_types_a[i],supported_types_b[j])){ |
|
//if (supported_types_a[i] == supported_types_b[j]) { |
|
return true; |
|
} |
|
} |
|
} |
|
|
|
return false; |
|
}, |
|
|
|
/** |
|
* Register a string in the search box so when the user types it it will recommend this node |
|
* @method registerSearchboxExtra |
|
* @param {String} node_type the node recommended |
|
* @param {String} description text to show next to it |
|
* @param {Object} data it could contain info of how the node should be configured |
|
* @return {Boolean} true if they can be connected |
|
*/ |
|
registerSearchboxExtra: function(node_type, description, data) { |
|
this.searchbox_extras[description.toLowerCase()] = { |
|
type: node_type, |
|
desc: description, |
|
data: data |
|
}; |
|
}, |
|
|
|
/** |
|
* Wrapper to load files (from url using fetch or from file using FileReader) |
|
* @method fetchFile |
|
* @param {String|File|Blob} url the url of the file (or the file itself) |
|
* @param {String} type an string to know how to fetch it: "text","arraybuffer","json","blob" |
|
* @param {Function} on_complete callback(data) |
|
* @param {Function} on_error in case of an error |
|
* @return {FileReader|Promise} returns the object used to |
|
*/ |
|
fetchFile: function( url, type, on_complete, on_error ) { |
|
var that = this; |
|
if(!url) |
|
return null; |
|
|
|
type = type || "text"; |
|
if( url.constructor === String ) |
|
{ |
|
if (url.substr(0, 4) == "http" && LiteGraph.proxy) { |
|
url = LiteGraph.proxy + url.substr(url.indexOf(":") + 3); |
|
} |
|
return fetch(url) |
|
.then(function(response) { |
|
if(!response.ok) |
|
throw new Error("File not found"); //it will be catch below |
|
if(type == "arraybuffer") |
|
return response.arrayBuffer(); |
|
else if(type == "text" || type == "string") |
|
return response.text(); |
|
else if(type == "json") |
|
return response.json(); |
|
else if(type == "blob") |
|
return response.blob(); |
|
}) |
|
.then(function(data) { |
|
if(on_complete) |
|
on_complete(data); |
|
}) |
|
.catch(function(error) { |
|
console.error("error fetching file:",url); |
|
if(on_error) |
|
on_error(error); |
|
}); |
|
} |
|
else if( url.constructor === File || url.constructor === Blob) |
|
{ |
|
var reader = new FileReader(); |
|
reader.onload = function(e) |
|
{ |
|
var v = e.target.result; |
|
if( type == "json" ) |
|
v = JSON.parse(v); |
|
if(on_complete) |
|
on_complete(v); |
|
} |
|
if(type == "arraybuffer") |
|
return reader.readAsArrayBuffer(url); |
|
else if(type == "text" || type == "json") |
|
return reader.readAsText(url); |
|
else if(type == "blob") |
|
return reader.readAsBinaryString(url); |
|
} |
|
return null; |
|
} |
|
}); |
|
|
|
//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. |
|
* supported callbacks: |
|
+ onNodeAdded: when a new node is added to the graph |
|
+ onNodeRemoved: when a node inside this graph is removed |
|
+ onNodeConnectionChange: some connection has changed in the graph (connected or disconnected) |
|
* |
|
* @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 |
|
|
|
//safe clear |
|
if (this._nodes) { |
|
for (var i = 0; i < this._nodes.length; ++i) { |
|
var node = this._nodes[i]; |
|
if (node.onRemoved) { |
|
node.onRemoved(); |
|
} |
|
} |
|
} |
|
|
|
//nodes |
|
this._nodes = []; |
|
this._nodes_by_id = {}; |
|
this._nodes_in_order = []; //nodes sorted in execution order |
|
this._nodes_executable = null; //nodes that contain onExecute sorted in execution order |
|
|
|
//other scene stuff |
|
this._groups = []; |
|
|
|
//links |
|
this.links = {}; //container with all the links |
|
|
|
//iterations |
|
this.iteration = 0; |
|
|
|
//custom data |
|
this.config = {}; |
|
this.vars = {}; |
|
this.extra = {}; //to store custom data |
|
|
|
//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; |
|
|
|
this.nodes_executing = []; |
|
this.nodes_actioning = []; |
|
this.nodes_executedAction = []; |
|
|
|
//subgraph_data |
|
this.inputs = {}; |
|
this.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, if 0 then it renders to the monitor refresh rate |
|
*/ |
|
|
|
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 || 0; |
|
var that = this; |
|
|
|
//execute once per frame |
|
if ( interval == 0 && typeof window != "undefined" && window.requestAnimationFrame ) { |
|
function on_frame() { |
|
if (that.execution_timer_id != -1) { |
|
return; |
|
} |
|
window.requestAnimationFrame(on_frame); |
|
if(that.onBeforeStep) |
|
that.onBeforeStep(); |
|
that.runStep(1, !that.catch_errors); |
|
if(that.onAfterStep) |
|
that.onAfterStep(); |
|
} |
|
this.execution_timer_id = -1; |
|
on_frame(); |
|
} else { //execute every 'interval' ms |
|
this.execution_timer_id = setInterval(function() { |
|
//execute |
|
if(that.onBeforeStep) |
|
that.onBeforeStep(); |
|
that.runStep(1, !that.catch_errors); |
|
if(that.onAfterStep) |
|
that.onAfterStep(); |
|
}, 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) { |
|
if (this.execution_timer_id != -1) { |
|
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 |
|
* @param {Boolean} do_not_catch_errors [optional] if you want to try/catch errors |
|
* @param {number} limit max number of nodes to execute (used to execute from start to a node) |
|
*/ |
|
|
|
LGraph.prototype.runStep = function(num, do_not_catch_errors, limit ) { |
|
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; |
|
} |
|
|
|
limit = limit || nodes.length; |
|
|
|
if (do_not_catch_errors) { |
|
//iterations |
|
for (var i = 0; i < num; i++) { |
|
for (var j = 0; j < limit; ++j) { |
|
var node = nodes[j]; |
|
if (node.mode == LiteGraph.ALWAYS && node.onExecute) { |
|
//wrap node.onExecute(); |
|
node.doExecute(); |
|
} |
|
} |
|
|
|
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; j < limit; ++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; |
|
this.nodes_executing = []; |
|
this.nodes_actioning = []; |
|
this.nodes_executedAction = []; |
|
}; |
|
|
|
/** |
|
* 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 executable nodes in 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; |
|
} |
|
} //num of input links |
|
else { |
|
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) { |
|
//if same priority, sort by order |
|
return A.order - B.order; |
|
} |
|
return Ap - Bp; //sort by priority |
|
}); |
|
|
|
//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 doesn't 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 || 100; |
|
|
|
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 + LiteGraph.NODE_TITLE_HEIGHT; |
|
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 + LiteGraph.NODE_TITLE_HEIGHT; |
|
} |
|
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.constructor === LiteGraph.Subgraph && |
|
eventname != "onExecute" |
|
) { |
|
if (node.mode == mode) { |
|
node.sendEventToAllNodes(eventname, params, mode); |
|
} |
|
continue; |
|
} |
|
|
|
if (!node[eventname] || node.mode != mode) { |
|
continue; |
|
} |
|
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 instance 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 |
|
|
|
this.beforeChange(); //sure? - almost sure is wrong |
|
|
|
//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); |
|
} |
|
|
|
//close panels |
|
this.sendActionToCanvas("checkPanels"); |
|
|
|
this.setDirtyCanvas(true, true); |
|
this.afterChange(); //sure? - almost sure is wrong |
|
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, result) { |
|
result = result || []; |
|
result.length = 0; |
|
for (var i = 0, l = this._nodes.length; i < l; ++i) { |
|
if (this._nodes[i].constructor === classObject) { |
|
result.push(this._nodes[i]); |
|
} |
|
} |
|
return result; |
|
}; |
|
|
|
/** |
|
* 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, result) { |
|
var type = type.toLowerCase(); |
|
result = result || []; |
|
result.length = 0; |
|
for (var i = 0, l = this._nodes.length; i < l; ++i) { |
|
if (this._nodes[i].type.toLowerCase() == type) { |
|
result.push(this._nodes[i]); |
|
} |
|
} |
|
return result; |
|
}; |
|
|
|
/** |
|
* Returns the first node that matches a name in its title |
|
* @method findNodeByTitle |
|
* @param {String} name the name of the node to search |
|
* @return {Node} the node or null |
|
*/ |
|
LGraph.prototype.findNodeByTitle = function(title) { |
|
for (var i = 0, l = this._nodes.length; i < l; ++i) { |
|
if (this._nodes[i].title == title) { |
|
return this._nodes[i]; |
|
} |
|
} |
|
return null; |
|
}; |
|
|
|
/** |
|
* 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, margin) { |
|
nodes_list = nodes_list || this._nodes; |
|
var nRet = null; |
|
for (var i = nodes_list.length - 1; i >= 0; i--) { |
|
var n = nodes_list[i]; |
|
if (n.isPointInside(x, y, margin)) { |
|
// check for lesser interest nodes (TODO check for overlapping, use the top) |
|
/*if (typeof n == "LGraphGroup"){ |
|
nRet = n; |
|
}else{*/ |
|
return n; |
|
/*}*/ |
|
} |
|
} |
|
return nRet; |
|
}; |
|
|
|
/** |
|
* 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, true)) { |
|
return g; |
|
} |
|
} |
|
return null; |
|
}; |
|
|
|
/** |
|
* Checks that the node type matches the node type registered, used when replacing a nodetype by a newer version during execution |
|
* this replaces the ones using the old version with the new version |
|
* @method checkNodeTypes |
|
*/ |
|
LGraph.prototype.checkNodeTypes = function() { |
|
var changes = false; |
|
for (var i = 0; i < this._nodes.length; i++) { |
|
var node = this._nodes[i]; |
|
var ctor = LiteGraph.registered_node_types[node.type]; |
|
if (node.constructor == ctor) { |
|
continue; |
|
} |
|
console.log("node being replaced by newer version: " + node.type); |
|
var newnode = LiteGraph.createNode(node.type); |
|
changes = true; |
|
this._nodes[i] = newnode; |
|
newnode.configure(node.serialize()); |
|
newnode.graph = this; |
|
this._nodes_by_id[newnode.id] = newnode; |
|
if (node.inputs) { |
|
newnode.inputs = node.inputs.concat(); |
|
} |
|
if (node.outputs) { |
|
newnode.outputs = node.outputs.concat(); |
|
} |
|
} |
|
this.updateExecutionOrder(); |
|
}; |
|
|
|
// ********** GLOBALS ***************** |
|
|
|
LGraph.prototype.onAction = function(action, param, options) { |
|
this._input_nodes = this.findNodesByClass( |
|
LiteGraph.GraphInput, |
|
this._input_nodes |
|
); |
|
for (var i = 0; i < this._input_nodes.length; ++i) { |
|
var node = this._input_nodes[i]; |
|
if (node.properties.name != action) { |
|
continue; |
|
} |
|
//wrap node.onAction(action, param); |
|
node.actionDo(action, param, options); |
|
break; |
|
} |
|
}; |
|
|
|
LGraph.prototype.trigger = function(action, param) { |
|
if (this.onTrigger) { |
|
this.onTrigger(action, param); |
|
} |
|
}; |
|
|
|
/** |
|
* 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.addInput = function(name, type, value) { |
|
var input = this.inputs[name]; |
|
if (input) { |
|
//already exist |
|
return; |
|
} |
|
|
|
this.beforeChange(); |
|
this.inputs[name] = { name: name, type: type, value: value }; |
|
this._version++; |
|
this.afterChange(); |
|
|
|
if (this.onInputAdded) { |
|
this.onInputAdded(name, type); |
|
} |
|
|
|
if (this.onInputsOutputsChange) { |
|
this.onInputsOutputsChange(); |
|
} |
|
}; |
|
|
|
/** |
|
* Assign a data to the global graph input |
|
* @method setGlobalInputData |
|
* @param {String} name |
|
* @param {*} data |
|
*/ |
|
LGraph.prototype.setInputData = function(name, data) { |
|
var input = this.inputs[name]; |
|
if (!input) { |
|
return; |
|
} |
|
input.value = data; |
|
}; |
|
|
|
/** |
|
* Returns the current value of a global graph input |
|
* @method getInputData |
|
* @param {String} name |
|
* @return {*} the data |
|
*/ |
|
LGraph.prototype.getInputData = function(name) { |
|
var input = this.inputs[name]; |
|
if (!input) { |
|
return null; |
|
} |
|
return input.value; |
|
}; |
|
|
|
/** |
|
* Changes the name of a global graph input |
|
* @method renameInput |
|
* @param {String} old_name |
|
* @param {String} new_name |
|
*/ |
|
LGraph.prototype.renameInput = function(old_name, name) { |
|
if (name == old_name) { |
|
return; |
|
} |
|
|
|
if (!this.inputs[old_name]) { |
|
return false; |
|
} |
|
|
|
if (this.inputs[name]) { |
|
console.error("there is already one input with that name"); |
|
return false; |
|
} |
|
|
|
this.inputs[name] = this.inputs[old_name]; |
|
delete this.inputs[old_name]; |
|
this._version++; |
|
|
|
if (this.onInputRenamed) { |
|
this.onInputRenamed(old_name, name); |
|
} |
|
|
|
if (this.onInputsOutputsChange) { |
|
this.onInputsOutputsChange(); |
|
} |
|
}; |
|
|
|
/** |
|
* Changes the type of a global graph input |
|
* @method changeInputType |
|
* @param {String} name |
|
* @param {String} type |
|
*/ |
|
LGraph.prototype.changeInputType = function(name, type) { |
|
if (!this.inputs[name]) { |
|
return false; |
|
} |
|
|
|
if ( |
|
this.inputs[name].type && |
|
String(this.inputs[name].type).toLowerCase() == |
|
String(type).toLowerCase() |
|
) { |
|
return; |
|
} |
|
|
|
this.inputs[name].type = type; |
|
this._version++; |
|
if (this.onInputTypeChanged) { |
|
this.onInputTypeChanged(name, type); |
|
} |
|
}; |
|
|
|
/** |
|
* Removes a global graph input |
|
* @method removeInput |
|
* @param {String} name |
|
* @param {String} type |
|
*/ |
|
LGraph.prototype.removeInput = function(name) { |
|
if (!this.inputs[name]) { |
|
return false; |
|
} |
|
|
|
delete this.inputs[name]; |
|
this._version++; |
|
|
|
if (this.onInputRemoved) { |
|
this.onInputRemoved(name); |
|
} |
|
|
|
if (this.onInputsOutputsChange) { |
|
this.onInputsOutputsChange(); |
|
} |
|
return true; |
|
}; |
|
|
|
/** |
|
* Creates a global graph output |
|
* @method addOutput |
|
* @param {String} name |
|
* @param {String} type |
|
* @param {*} value |
|
*/ |
|
LGraph.prototype.addOutput = function(name, type, value) { |
|
this.outputs[name] = { name: name, type: type, value: value }; |
|
this._version++; |
|
|
|
if (this.onOutputAdded) { |
|
this.onOutputAdded(name, type); |
|
} |
|
|
|
if (this.onInputsOutputsChange) { |
|
this.onInputsOutputsChange(); |
|
} |
|
}; |
|
|
|
/** |
|
* Assign a data to the global output |
|
* @method setOutputData |
|
* @param {String} name |
|
* @param {String} value |
|
*/ |
|
LGraph.prototype.setOutputData = function(name, value) { |
|
var output = this.outputs[name]; |
|
if (!output) { |
|
return; |
|
} |
|
output.value = value; |
|
}; |
|
|
|
/** |
|
* Returns the current value of a global graph output |
|
* @method getOutputData |
|
* @param {String} name |
|
* @return {*} the data |
|
*/ |
|
LGraph.prototype.getOutputData = function(name) { |
|
var output = this.outputs[name]; |
|
if (!output) { |
|
return null; |
|
} |
|
return output.value; |
|
}; |
|
|
|
/** |
|
* Renames a global graph output |
|
* @method renameOutput |
|
* @param {String} old_name |
|
* @param {String} new_name |
|
*/ |
|
LGraph.prototype.renameOutput = function(old_name, name) { |
|
if (!this.outputs[old_name]) { |
|
return false; |
|
} |
|
|
|
if (this.outputs[name]) { |
|
console.error("there is already one output with that name"); |
|
return false; |
|
} |
|
|
|
this.outputs[name] = this.outputs[old_name]; |
|
delete this.outputs[old_name]; |
|
this._version++; |
|
|
|
if (this.onOutputRenamed) { |
|
this.onOutputRenamed(old_name, name); |
|
} |
|
|
|
if (this.onInputsOutputsChange) { |
|
this.onInputsOutputsChange(); |
|
} |
|
}; |
|
|
|
/** |
|
* Changes the type of a global graph output |
|
* @method changeOutputType |
|
* @param {String} name |
|
* @param {String} type |
|
*/ |
|
LGraph.prototype.changeOutputType = function(name, type) { |
|
if (!this.outputs[name]) { |
|
return false; |
|
} |
|
|
|
if ( |
|
this.outputs[name].type && |
|
String(this.outputs[name].type).toLowerCase() == |
|
String(type).toLowerCase() |
|
) { |
|
return; |
|
} |
|
|
|
this.outputs[name].type = type; |
|
this._version++; |
|
if (this.onOutputTypeChanged) { |
|
this.onOutputTypeChanged(name, type); |
|
} |
|
}; |
|
|
|
/** |
|
* Removes a global graph output |
|
* @method removeOutput |
|
* @param {String} name |
|
*/ |
|
LGraph.prototype.removeOutput = function(name) { |
|
if (!this.outputs[name]) { |
|
return false; |
|
} |
|
delete this.outputs[name]; |
|
this._version++; |
|
|
|
if (this.onOutputRemoved) { |
|
this.onOutputRemoved(name); |
|
} |
|
|
|
if (this.onInputsOutputsChange) { |
|
this.onInputsOutputsChange(); |
|
} |
|
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); |
|
} |
|
}; |
|
|
|
//used for undo, called before any change is made to the graph |
|
LGraph.prototype.beforeChange = function(info) { |
|
if (this.onBeforeChange) { |
|
this.onBeforeChange(this,info); |
|
} |
|
this.sendActionToCanvas("onBeforeChange", this); |
|
}; |
|
|
|
//used to resend actions, called after any change is made to the graph |
|
LGraph.prototype.afterChange = function(info) { |
|
if (this.onAfterChange) { |
|
this.onAfterChange(this,info); |
|
} |
|
this.sendActionToCanvas("onAfterChange", this); |
|
}; |
|
|
|
LGraph.prototype.connectionChange = function(node, link_info) { |
|
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; |
|
}; |
|
|
|
/** |
|
* clears the triggered slot animation in all links (stop visual animation) |
|
* @method clearTriggeredSlots |
|
*/ |
|
LGraph.prototype.clearTriggeredSlots = function() { |
|
for (var i in this.links) { |
|
var link_info = this.links[i]; |
|
if (!link_info) { |
|
continue; |
|
} |
|
if (link_info._last_time) { |
|
link_info._last_time = 0; |
|
} |
|
} |
|
}; |
|
|
|
/* 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]); |
|
}; |
|
|
|
/** |
|
* Destroys a link |
|
* @method removeLink |
|
* @param {Number} link_id |
|
*/ |
|
LGraph.prototype.removeLink = function(link_id) { |
|
var link = this.links[link_id]; |
|
if (!link) { |
|
return; |
|
} |
|
var node = this.getNodeById(link.target_id); |
|
if (node) { |
|
node.disconnectInput(link.target_slot); |
|
} |
|
}; |
|
|
|
//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]; |
|
if (!link.serialize) { |
|
//weird bug I havent solved yet |
|
console.warn( |
|
"weird LLink bug, link info is not a LLink but a regular object" |
|
); |
|
var link2 = new LLink(); |
|
for (var j in link) { |
|
link2[j] = link[j]; |
|
} |
|
this.links[i] = link2; |
|
link = link2; |
|
} |
|
|
|
links.push(link.serialize()); |
|
} |
|
|
|
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, |
|
extra: this.extra, |
|
version: LiteGraph.VERSION |
|
}; |
|
|
|
if(this.onSerialize) |
|
this.onSerialize(data); |
|
|
|
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 = data.links[i]; |
|
if(!link_data) //weird bug |
|
{ |
|
console.warn("serialized graph link data contains errors, skipping."); |
|
continue; |
|
} |
|
var link = new LLink(); |
|
link.configure(link_data); |
|
links[link.id] = link; |
|
} |
|
data.links = links; |
|
} |
|
|
|
//copy all stored fields |
|
for (var i in data) { |
|
if(i == "nodes" || i == "groups" ) //links must be accepted |
|
continue; |
|
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 or has errors: " + n_info.type |
|
); |
|
} |
|
|
|
//in case of error we create a replacement node to avoid losing info |
|
node = new LGraphNode(); |
|
node.last_serialization = n_info; |
|
node.has_errors = true; |
|
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.extra = data.extra || {}; |
|
|
|
if(this.onConfigure) |
|
this.onConfigure(data); |
|
|
|
this._version++; |
|
this.setDirtyCanvas(true, true); |
|
return error; |
|
}; |
|
|
|
LGraph.prototype.load = function(url, callback) { |
|
var that = this; |
|
|
|
//from file |
|
if(url.constructor === File || url.constructor === Blob) |
|
{ |
|
var reader = new FileReader(); |
|
reader.addEventListener('load', function(event) { |
|
var data = JSON.parse(event.target.result); |
|
that.configure(data); |
|
if(callback) |
|
callback(); |
|
}); |
|
|
|
reader.readAsText(url); |
|
return; |
|
} |
|
|
|
//is a string, then an URL |
|
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); |
|
if(callback) |
|
callback(); |
|
}; |
|
req.onerror = function(err) { |
|
console.error("Error loading graph:", err); |
|
}; |
|
}; |
|
|
|
LGraph.prototype.onNodeTrace = function(node, msg, color) { |
|
//TODO |
|
}; |
|
|
|
//this is the class in charge of storing link information |
|
function LLink(id, type, origin_id, origin_slot, target_id, target_slot) { |
|
this.id = id; |
|
this.type = type; |
|
this.origin_id = origin_id; |
|
this.origin_slot = origin_slot; |
|
this.target_id = target_id; |
|
this.target_slot = target_slot; |
|
|
|
this._data = null; |
|
this._pos = new Float32Array(2); //center |
|
} |
|
|
|
LLink.prototype.configure = function(o) { |
|
if (o.constructor === Array) { |
|
this.id = o[0]; |
|
this.origin_id = o[1]; |
|
this.origin_slot = o[2]; |
|
this.target_id = o[3]; |
|
this.target_slot = o[4]; |
|
this.type = o[5]; |
|
} else { |
|
this.id = o.id; |
|
this.type = o.type; |
|
this.origin_id = o.origin_id; |
|
this.origin_slot = o.origin_slot; |
|
this.target_id = o.target_id; |
|
this.target_slot = o.target_slot; |
|
} |
|
}; |
|
|
|
LLink.prototype.serialize = function() { |
|
return [ |
|
this.id, |
|
this.origin_id, |
|
this.origin_slot, |
|
this.target_id, |
|
this.target_slot, |
|
this.type |
|
]; |
|
}; |
|
|
|
LiteGraph.LLink = LLink; |
|
|
|
// ************************************************************* |
|
// 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 }); |
|
|
|
general properties: |
|
+ clip_area: if you render outside the node, it will be clipped |
|
+ 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 |
|
+ horizontal: slots are distributed horizontally |
|
+ widgets_start_y: widgets start at y distance from the top of the node |
|
|
|
flags object: |
|
+ collapsed: if it is collapsed |
|
|
|
supported callbacks: |
|
+ onAdded: when added to graph (warning: this is called BEFORE the node is configured when loading) |
|
+ 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 |
|
+ onBounding: in case this node has a bigger bounding than the node itself (the callback receives the bounding as [x,y,w,h]) |
|
+ onDblClick: double clicked in the node |
|
+ onInputDblClick: input slot double clicked (can be used to automatically create a node connected) |
|
+ onOutputDblClick: output slot double clicked (can be used to automatically create a node connected) |
|
+ onConfigure: called after the node has been configured |
|
+ onSerialize: to add extra info when serializing (the callback receives the object that should be filled with the data) |
|
+ 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 ) |
|
+ onAction: action slot triggered |
|
+ getExtraMenuOptions: to add option to context menu |
|
*/ |
|
|
|
/** |
|
* 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.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 == "properties") { |
|
//i don't 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]); |
|
} |
|
} //value |
|
else { |
|
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 |
|
} |
|
} |
|
} |
|
} |
|
|
|
if( this.widgets ) |
|
{ |
|
for (var i = 0; i < this.widgets.length; ++i) |
|
{ |
|
var w = this.widgets[i]; |
|
if(!w) |
|
continue; |
|
if(w.options && w.options.property && this.properties[ w.options.property ]) |
|
w.value = JSON.parse( JSON.stringify( this.properties[ w.options.property ] ) ); |
|
} |
|
if (info.widgets_values) { |
|
for (var i = 0; i < info.widgets_values.length; ++i) { |
|
if (this.widgets[i]) { |
|
this.widgets[i].value = info.widgets_values[i]; |
|
} |
|
} |
|
} |
|
} |
|
|
|
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, |
|
flags: LiteGraph.cloneObject(this.flags), |
|
order: this.order, |
|
mode: this.mode |
|
}; |
|
|
|
//special case for when there were errors |
|
if (this.constructor === LGraphNode && this.last_serialization) { |
|
return this.last_serialization; |
|
} |
|
|
|
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 (this.widgets && this.serialize_widgets) { |
|
o.widgets_values = []; |
|
for (var i = 0; i < this.widgets.length; ++i) { |
|
if(this.widgets[i]) |
|
o.widgets_values[i] = this.widgets[i].value; |
|
else |
|
o.widgets_values[i] = null; |
|
} |
|
} |
|
|
|
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) { |
|
if (this.onSerialize(o)) { |
|
console.warn( |
|
"node onSerialize shouldnt return anything, data should be stored in the object pass in the first parameter" |
|
); |
|
} |
|
} |
|
|
|
return o; |
|
}; |
|
|
|
/* Creates a clone of this node */ |
|
LGraphNode.prototype.clone = function() { |
|
var node = LiteGraph.createNode(this.type); |
|
if (!node) { |
|
return null; |
|
} |
|
|
|
//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.deserialize = 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; |
|
}; |
|
|
|
/** |
|
* sets the value of a property |
|
* @method setProperty |
|
* @param {String} name |
|
* @param {*} value |
|
*/ |
|
LGraphNode.prototype.setProperty = function(name, value) { |
|
if (!this.properties) { |
|
this.properties = {}; |
|
} |
|
if( value === this.properties[name] ) |
|
return; |
|
var prev_value = this.properties[name]; |
|
this.properties[name] = value; |
|
if (this.onPropertyChanged) { |
|
if( this.onPropertyChanged(name, value, prev_value) === false ) //abort change |
|
this.properties[name] = prev_value; |
|
} |
|
if(this.widgets) //widgets could be linked to properties |
|
for(var i = 0; i < this.widgets.length; ++i) |
|
{ |
|
var w = this.widgets[i]; |
|
if(!w) |
|
continue; |
|
if(w.options.property == name) |
|
{ |
|
w.value = value; |
|
break; |
|
} |
|
} |
|
}; |
|
|
|
// 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]; |
|
var link = this.graph.links[link_id]; |
|
if(link) |
|
link.data = data; |
|
} |
|
} |
|
}; |
|
|
|
/** |
|
* sets the output data type, useful when you want to be able to overwrite the data type |
|
* @method setOutputDataType |
|
* @param {number} slot |
|
* @param {String} datatype |
|
*/ |
|
LGraphNode.prototype.setOutputDataType = function(slot, type) { |
|
if (!this.outputs) { |
|
return; |
|
} |
|
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.type = type; |
|
|
|
//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].type = type; |
|
} |
|
} |
|
}; |
|
|
|
/** |
|
* 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 incoming 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 type (in case this supports multiple input types) |
|
* @method getInputDataType |
|
* @param {number} slot |
|
* @return {String} datatype in string format |
|
*/ |
|
LGraphNode.prototype.getInputDataType = function(slot) { |
|
if (!this.inputs) { |
|
return null; |
|
} //undefined; |
|
|
|
if (slot >= this.inputs.length || this.inputs[slot].link == null) { |
|
return null; |
|
} |
|
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; |
|
} |
|
var node = this.graph.getNodeById(link.origin_id); |
|
if (!node) { |
|
return link.type; |
|
} |
|
var output_info = node.outputs[link.origin_slot]; |
|
if (output_info) { |
|
return output_info.type; |
|
} |
|
return null; |
|
}; |
|
|
|
/** |
|
* 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 link info in the connection of an input slot |
|
* @method getInputLink |
|
* @param {number} slot |
|
* @return {LLink} object or null |
|
*/ |
|
LGraphNode.prototype.getInputLink = function(slot) { |
|
if (!this.inputs) { |
|
return null; |
|
} |
|
if (slot < this.inputs.length) { |
|
var slot_info = this.inputs[slot]; |
|
return this.graph.links[ slot_info.link ]; |
|
} |
|
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) { |
|
var input_info = this.inputs[i]; |
|
if (name == input_info.name && input_info.link != null) { |
|
var link = this.graph.links[input_info.link]; |
|
if (link) { |
|
return link.data; |
|
} |
|
} |
|
} |
|
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; |
|
}; |
|
|
|
LGraphNode.prototype.addOnTriggerInput = function(){ |
|
var trigS = this.findInputSlot("onTrigger"); |
|
if (trigS == -1){ //!trigS || |
|
var input = this.addInput("onTrigger", LiteGraph.EVENT, {optional: true, nameLocked: true}); |
|
return this.findInputSlot("onTrigger"); |
|
} |
|
return trigS; |
|
} |
|
|
|
LGraphNode.prototype.addOnExecutedOutput = function(){ |
|
var trigS = this.findOutputSlot("onExecuted"); |
|
if (trigS == -1){ //!trigS || |
|
var output = this.addOutput("onExecuted", LiteGraph.ACTION, {optional: true, nameLocked: true}); |
|
return this.findOutputSlot("onExecuted"); |
|
} |
|
return trigS; |
|
} |
|
|
|
LGraphNode.prototype.onAfterExecuteNode = function(param, options){ |
|
var trigS = this.findOutputSlot("onExecuted"); |
|
if (trigS != -1){ |
|
|
|
//console.debug(this.id+":"+this.order+" triggering slot onAfterExecute"); |
|
//console.debug(param); |
|
//console.debug(options); |
|
this.triggerSlot(trigS, param, null, options); |
|
|
|
} |
|
} |
|
|
|
LGraphNode.prototype.changeMode = function(modeTo){ |
|
switch(modeTo){ |
|
case LiteGraph.ON_EVENT: |
|
// this.addOnExecutedOutput(); |
|
break; |
|
|
|
case LiteGraph.ON_TRIGGER: |
|
this.addOnTriggerInput(); |
|
this.addOnExecutedOutput(); |
|
break; |
|
|
|
case LiteGraph.NEVER: |
|
break; |
|
|
|
case LiteGraph.ALWAYS: |
|
break; |
|
|
|
case LiteGraph.ON_REQUEST: |
|
break; |
|
|
|
default: |
|
return false; |
|
break; |
|
} |
|
this.mode = modeTo; |
|
return true; |
|
}; |
|
|
|
/** |
|
* Triggers the node code execution, place a boolean/counter to mark the node as being executed |
|
* @method execute |
|
* @param {*} param |
|
* @param {*} options |
|
*/ |
|
LGraphNode.prototype.doExecute = function(param, options) { |
|
options = options || {}; |
|
if (this.onExecute){ |
|
|
|
// enable this to give the event an ID |
|
if (!options.action_call) options.action_call = this.id+"_exec_"+Math.floor(Math.random()*9999); |
|
|
|
this.graph.nodes_executing[this.id] = true; //.push(this.id); |
|
|
|
this.onExecute(param, options); |
|
|
|
this.graph.nodes_executing[this.id] = false; //.pop(); |
|
|
|
// save execution/action ref |
|
this.exec_version = this.graph.iteration; |
|
if(options && options.action_call){ |
|
this.action_call = options.action_call; // if (param) |
|
this.graph.nodes_executedAction[this.id] = options.action_call; |
|
} |
|
} |
|
this.execute_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event |
|
if(this.onAfterExecuteNode) this.onAfterExecuteNode(param, options); // callback |
|
}; |
|
|
|
/** |
|
* Triggers an action, wrapped by logics to control execution flow |
|
* @method actionDo |
|
* @param {String} action name |
|
* @param {*} param |
|
*/ |
|
LGraphNode.prototype.actionDo = function(action, param, options) { |
|
options = options || {}; |
|
if (this.onAction){ |
|
|
|
// enable this to give the event an ID |
|
if (!options.action_call) options.action_call = this.id+"_"+(action?action:"action")+"_"+Math.floor(Math.random()*9999); |
|
|
|
this.graph.nodes_actioning[this.id] = (action?action:"actioning"); //.push(this.id); |
|
|
|
this.onAction(action, param, options); |
|
|
|
this.graph.nodes_actioning[this.id] = false; //.pop(); |
|
|
|
// save execution/action ref |
|
if(options && options.action_call){ |
|
this.action_call = options.action_call; // if (param) |
|
this.graph.nodes_executedAction[this.id] = options.action_call; |
|
} |
|
} |
|
this.action_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event |
|
if(this.onAfterExecuteNode) this.onAfterExecuteNode(param, options); |
|
}; |
|
|
|
/** |
|
* 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, options) { |
|
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, null, options); |
|
} |
|
}; |
|
|
|
/** |
|
* Triggers a slot event in this node: cycle output slots and launch execute/action on connected nodes |
|
* @method triggerSlot |
|
* @param {Number} slot the index of the output slot |
|
* @param {*} param |
|
* @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot |
|
*/ |
|
LGraphNode.prototype.triggerSlot = function(slot, param, link_id, options) { |
|
options = options || {}; |
|
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 id = links[k]; |
|
if (link_id != null && link_id != id) { |
|
//to skip links |
|
continue; |
|
} |
|
var link_info = this.graph.links[links[k]]; |
|
if (!link_info) { |
|
//not connected |
|
continue; |
|
} |
|
link_info._last_time = LiteGraph.getTime(); |
|
var node = this.graph.getNodeById(link_info.target_id); |
|
if (!node) { |
|
//node not found? |
|
continue; |
|
} |
|
|
|
//used to mark events in graph |
|
var target_connection = node.inputs[link_info.target_slot]; |
|
|
|
if (node.mode === LiteGraph.ON_TRIGGER) |
|
{ |
|
// generate unique trigger ID if not present |
|
if (!options.action_call) options.action_call = this.id+"_trigg_"+Math.floor(Math.random()*9999); |
|
if (node.onExecute) { |
|
// -- wrapping node.onExecute(param); -- |
|
node.doExecute(param, options); |
|
} |
|
} |
|
else if (node.onAction) { |
|
// generate unique action ID if not present |
|
if (!options.action_call) options.action_call = this.id+"_act_"+Math.floor(Math.random()*9999); |
|
//pass the action name |
|
var target_connection = node.inputs[link_info.target_slot]; |
|
// wrap node.onAction(target_connection.name, param); |
|
node.actionDo(target_connection.name, param, options); |
|
} |
|
} |
|
}; |
|
|
|
/** |
|
* clears the trigger slot animation |
|
* @method clearTriggeredSlot |
|
* @param {Number} slot the index of the output slot |
|
* @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot |
|
*/ |
|
LGraphNode.prototype.clearTriggeredSlot = function(slot, link_id) { |
|
if (!this.outputs) { |
|
return; |
|
} |
|
|
|
var output = this.outputs[slot]; |
|
if (!output) { |
|
return; |
|
} |
|
|
|
var links = output.links; |
|
if (!links || !links.length) { |
|
return; |
|
} |
|
|
|
//for every link attached here |
|
for (var k = 0; k < links.length; ++k) { |
|
var id = links[k]; |
|
if (link_id != null && link_id != id) { |
|
//to skip links |
|
continue; |
|
} |
|
var link_info = this.graph.links[links[k]]; |
|
if (!link_info) { |
|
//not connected |
|
continue; |
|
} |
|
link_info._last_time = 0; |
|
} |
|
}; |
|
|
|
/** |
|
* changes node size and triggers callback |
|
* @method setSize |
|
* @param {vec2} size |
|
*/ |
|
LGraphNode.prototype.setSize = function(size) |
|
{ |
|
this.size = size; |
|
if(this.onResize) |
|
this.onResize(this.size); |
|
} |
|
|
|
/** |
|
* 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); |
|
} |
|
|
|
if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this,type,true); |
|
|
|
this.setSize( this.computeSize() ); |
|
this.setDirtyCanvas(true, true); |
|
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); |
|
} |
|
|
|
if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this,info[1],true); |
|
|
|
} |
|
|
|
this.setSize( this.computeSize() ); |
|
this.setDirtyCanvas(true, true); |
|
}; |
|
|
|
/** |
|
* remove an existing output slot |
|
* @method removeOutput |
|
* @param {number} slot |
|
*/ |
|
LGraphNode.prototype.removeOutput = function(slot) { |
|
this.disconnectOutput(slot); |
|
this.outputs.splice(slot, 1); |
|
for (var i = slot; i < this.outputs.length; ++i) { |
|
if (!this.outputs[i] || !this.outputs[i].links) { |
|
continue; |
|
} |
|
var links = this.outputs[i].links; |
|
for (var j = 0; j < links.length; ++j) { |
|
var link = this.graph.links[links[j]]; |
|
if (!link) { |
|
continue; |
|
} |
|
link.origin_slot -= 1; |
|
} |
|
} |
|
|
|
this.setSize( this.computeSize() ); |
|
if (this.onOutputRemoved) { |
|
this.onOutputRemoved(slot); |
|
} |
|
this.setDirtyCanvas(true, true); |
|
}; |
|
|
|
/** |
|
* 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.setSize( this.computeSize() ); |
|
|
|
if (this.onInputAdded) { |
|
this.onInputAdded(o); |
|
} |
|
|
|
LiteGraph.registerNodeAndSlotType(this,type); |
|
|
|
this.setDirtyCanvas(true, true); |
|
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); |
|
} |
|
|
|
LiteGraph.registerNodeAndSlotType(this,info[1]); |
|
} |
|
|
|
this.setSize( this.computeSize() ); |
|
this.setDirtyCanvas(true, true); |
|
}; |
|
|
|
/** |
|
* remove an existing input slot |
|
* @method removeInput |
|
* @param {number} slot |
|
*/ |
|
LGraphNode.prototype.removeInput = function(slot) { |
|
this.disconnectInput(slot); |
|
var slot_info = this.inputs.splice(slot, 1); |
|
for (var i = slot; i < this.inputs.length; ++i) { |
|
if (!this.inputs[i]) { |
|
continue; |
|
} |
|
var link = this.graph.links[this.inputs[i].link]; |
|
if (!link) { |
|
continue; |
|
} |
|
link.target_slot -= 1; |
|
} |
|
this.setSize( this.computeSize() ); |
|
if (this.onInputRemoved) { |
|
this.onInputRemoved(slot, slot_info[0] ); |
|
} |
|
this.setDirtyCanvas(true, true); |
|
}; |
|
|
|
/** |
|
* 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 minimum 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(out) { |
|
if (this.constructor.size) { |
|
return this.constructor.size.concat(); |
|
} |
|
|
|
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 |
|
|
|
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.widgets && this.widgets.length) { |
|
size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH * 1.5); |
|
} |
|
|
|
size[1] = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT; |
|
|
|
var widgets_height = 0; |
|
if (this.widgets && this.widgets.length) { |
|
for (var i = 0, l = this.widgets.length; i < l; ++i) { |
|
if (this.widgets[i].computeSize) |
|
widgets_height += this.widgets[i].computeSize(size[0])[1] + 4; |
|
else |
|
widgets_height += LiteGraph.NODE_WIDGET_HEIGHT + 4; |
|
} |
|
widgets_height += 8; |
|
} |
|
|
|
//compute height using widgets height |
|
if( this.widgets_up ) |
|
size[1] = Math.max( size[1], widgets_height ); |
|
else if( this.widgets_start_y != null ) |
|
size[1] = Math.max( size[1], widgets_height + this.widgets_start_y ); |
|
else |
|
size[1] += widgets_height; |
|
|
|
function compute_text_size(text) { |
|
if (!text) { |
|
return 0; |
|
} |
|
return font_size * text.length * 0.6; |
|
} |
|
|
|
if ( |
|
this.constructor.min_height && |
|
size[1] < this.constructor.min_height |
|
) { |
|
size[1] = this.constructor.min_height; |
|
} |
|
|
|
size[1] += 6; //margin |
|
|
|
return size; |
|
}; |
|
|
|
/** |
|
* returns all the info available about a property of this node. |
|
* |
|
* @method getPropertyInfo |
|
* @param {String} property name of the property |
|
* @return {Object} the object with all the available info |
|
*/ |
|
LGraphNode.prototype.getPropertyInfo = function( property ) |
|
{ |
|
var info = null; |
|
|
|
//there are several ways to define info about a property |
|
//legacy mode |
|
if (this.properties_info) { |
|
for (var i = 0; i < this.properties_info.length; ++i) { |
|
if (this.properties_info[i].name == property) { |
|
info = this.properties_info[i]; |
|
break; |
|
} |
|
} |
|
} |
|
//litescene mode using the constructor |
|
if(this.constructor["@" + property]) |
|
info = this.constructor["@" + property]; |
|
|
|
if(this.constructor.widgets_info && this.constructor.widgets_info[property]) |
|
info = this.constructor.widgets_info[property]; |
|
|
|
//litescene mode using the constructor |
|
if (!info && this.onGetPropertyInfo) { |
|
info = this.onGetPropertyInfo(property); |
|
} |
|
|
|
if (!info) |
|
info = {}; |
|
if(!info.type) |
|
info.type = typeof this.properties[property]; |
|
if(info.widget == "combo") |
|
info.type = "enum"; |
|
|
|
return info; |
|
} |
|
|
|
/** |
|
* Defines a widget inside the node, it will be rendered on top of the node, you can control lots of properties |
|
* |
|
* @method addWidget |
|
* @param {String} type the widget type (could be "number","string","combo" |
|
* @param {String} name the text to show on the widget |
|
* @param {String} value the default value |
|
* @param {Function|String} callback function to call when it changes (optionally, it can be the name of the property to modify) |
|
* @param {Object} options the object that contains special properties of this widget |
|
* @return {Object} the created widget object |
|
*/ |
|
LGraphNode.prototype.addWidget = function( type, name, value, callback, options ) |
|
{ |
|
if (!this.widgets) { |
|
this.widgets = []; |
|
} |
|
|
|
if(!options && callback && callback.constructor === Object) |
|
{ |
|
options = callback; |
|
callback = null; |
|
} |
|
|
|
if(options && options.constructor === String) //options can be the property name |
|
options = { property: options }; |
|
|
|
if(callback && callback.constructor === String) //callback can be the property name |
|
{ |
|
if(!options) |
|
options = {}; |
|
options.property = callback; |
|
callback = null; |
|
} |
|
|
|
if(callback && callback.constructor !== Function) |
|
{ |
|
console.warn("addWidget: callback must be a function"); |
|
callback = null; |
|
} |
|
|
|
var w = { |
|
type: type.toLowerCase(), |
|
name: name, |
|
value: value, |
|
callback: callback, |
|
options: options || {} |
|
}; |
|
|
|
if (w.options.y !== undefined) { |
|
w.y = w.options.y; |
|
} |
|
|
|
if (!callback && !w.options.callback && !w.options.property) { |
|
console.warn("LiteGraph addWidget(...) without a callback or property assigned"); |
|
} |
|
if (type == "combo" && !w.options.values) { |
|
throw "LiteGraph addWidget('combo',...) requires to pass values in options: { values:['red','blue'] }"; |
|
} |
|
this.widgets.push(w); |
|
this.setSize( this.computeSize() ); |
|
return w; |
|
}; |
|
|
|
LGraphNode.prototype.addCustomWidget = function(custom_widget) { |
|
if (!this.widgets) { |
|
this.widgets = []; |
|
} |
|
this.widgets.push(custom_widget); |
|
return custom_widget; |
|
}; |
|
|
|
/** |
|
* 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.flags.collapsed ? LiteGraph.NODE_TITLE_HEIGHT : this.size[1] + LiteGraph.NODE_TITLE_HEIGHT; |
|
|
|
if (this.onBounding) { |
|
this.onBounding(out); |
|
} |
|
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, skip_title) { |
|
margin = margin || 0; |
|
|
|
var margin_top = this.graph && this.graph.isLive() ? 0 : LiteGraph.NODE_TITLE_HEIGHT; |
|
if (skip_title) { |
|
margin_top = 0; |
|
} |
|
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 |
|
var link_pos = new Float32Array(2); |
|
if (this.inputs) { |
|
for (var i = 0, l = this.inputs.length; i < l; ++i) { |
|
var input = this.inputs[i]; |
|
this.getConnectionPos(true, i, link_pos); |
|
if ( |
|
isInsideRectangle( |
|
x, |
|
y, |
|
link_pos[0] - 10, |
|
link_pos[1] - 5, |
|
20, |
|
10 |
|
) |
|
) { |
|
return { input: input, slot: i, link_pos: link_pos }; |
|
} |
|
} |
|
} |
|
|
|
if (this.outputs) { |
|
for (var i = 0, l = this.outputs.length; i < l; ++i) { |
|
var output = this.outputs[i]; |
|
this.getConnectionPos(false, i, link_pos); |
|
if ( |
|
isInsideRectangle( |
|
x, |
|
y, |
|
link_pos[0] - 10, |
|
link_pos[1] - 5, |
|
20, |
|
10 |
|
) |
|
) { |
|
return { output: output, slot: i, link_pos: link_pos }; |
|
} |
|
} |
|
} |
|
|
|
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 |
|
* @param {boolean} returnObj if the obj itself wanted |
|
* @return {number_or_object} the slot (-1 if not found) |
|
*/ |
|
LGraphNode.prototype.findInputSlot = function(name, returnObj) { |
|
if (!this.inputs) { |
|
return -1; |
|
} |
|
for (var i = 0, l = this.inputs.length; i < l; ++i) { |
|
if (name == this.inputs[i].name) { |
|
return !returnObj ? i : this.inputs[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 |
|
* @param {boolean} returnObj if the obj itself wanted |
|
* @return {number_or_object} the slot (-1 if not found) |
|
*/ |
|
LGraphNode.prototype.findOutputSlot = function(name, returnObj) { |
|
returnObj = returnObj || false; |
|
if (!this.outputs) { |
|
return -1; |
|
} |
|
for (var i = 0, l = this.outputs.length; i < l; ++i) { |
|
if (name == this.outputs[i].name) { |
|
return !returnObj ? i : this.outputs[i]; |
|
} |
|
} |
|
return -1; |
|
}; |
|
|
|
// TODO refactor: USE SINGLE findInput/findOutput functions! :: merge options |
|
|
|
/** |
|
* returns the first free input slot |
|
* @method findInputSlotFree |
|
* @param {object} options |
|
* @return {number_or_object} the slot (-1 if not found) |
|
*/ |
|
LGraphNode.prototype.findInputSlotFree = function(optsIn) { |
|
var optsIn = optsIn || {}; |
|
var optsDef = {returnObj: false |
|
,typesNotAccepted: [] |
|
}; |
|
var opts = Object.assign(optsDef,optsIn); |
|
if (!this.inputs) { |
|
return -1; |
|
} |
|
for (var i = 0, l = this.inputs.length; i < l; ++i) { |
|
if (this.inputs[i].link && this.inputs[i].link != null) { |
|
continue; |
|
} |
|
if (opts.typesNotAccepted && opts.typesNotAccepted.includes && opts.typesNotAccepted.includes(this.inputs[i].type)){ |
|
continue; |
|
} |
|
return !opts.returnObj ? i : this.inputs[i]; |
|
} |
|
return -1; |
|
}; |
|
|
|
/** |
|
* returns the first output slot free |
|
* @method findOutputSlotFree |
|
* @param {object} options |
|
* @return {number_or_object} the slot (-1 if not found) |
|
*/ |
|
LGraphNode.prototype.findOutputSlotFree = function(optsIn) { |
|
var optsIn = optsIn || {}; |
|
var optsDef = { returnObj: false |
|
,typesNotAccepted: [] |
|
}; |
|
var opts = Object.assign(optsDef,optsIn); |
|
if (!this.outputs) { |
|
return -1; |
|
} |
|
for (var i = 0, l = this.outputs.length; i < l; ++i) { |
|
if (this.outputs[i].links && this.outputs[i].links != null) { |
|
continue; |
|
} |
|
if (opts.typesNotAccepted && opts.typesNotAccepted.includes && opts.typesNotAccepted.includes(this.outputs[i].type)){ |
|
continue; |
|
} |
|
return !opts.returnObj ? i : this.outputs[i]; |
|
} |
|
return -1; |
|
}; |
|
|
|
/** |
|
* findSlotByType for INPUTS |
|
*/ |
|
LGraphNode.prototype.findInputSlotByType = function(type, returnObj, preferFreeSlot, doNotUseOccupied) { |
|
return this.findSlotByType(true, type, returnObj, preferFreeSlot, doNotUseOccupied); |
|
}; |
|
|
|
/** |
|
* findSlotByType for OUTPUTS |
|
*/ |
|
LGraphNode.prototype.findOutputSlotByType = function(type, returnObj, preferFreeSlot, doNotUseOccupied) { |
|
return this.findSlotByType(false, type, returnObj, preferFreeSlot, doNotUseOccupied); |
|
}; |
|
|
|
/** |
|
* returns the output (or input) slot with a given type, -1 if not found |
|
* @method findSlotByType |
|
* @param {boolean} input uise inputs instead of outputs |
|
* @param {string} type the type of the slot |
|
* @param {boolean} returnObj if the obj itself wanted |
|
* @param {boolean} preferFreeSlot if we want a free slot (if not found, will return the first of the type anyway) |
|
* @return {number_or_object} the slot (-1 if not found) |
|
*/ |
|
LGraphNode.prototype.findSlotByType = function(input, type, returnObj, preferFreeSlot, doNotUseOccupied) { |
|
input = input || false; |
|
returnObj = returnObj || false; |
|
preferFreeSlot = preferFreeSlot || false; |
|
doNotUseOccupied = doNotUseOccupied || false; |
|
var aSlots = input ? this.inputs : this.outputs; |
|
if (!aSlots) { |
|
return -1; |
|
} |
|
// !! empty string type is considered 0, * !! |
|
if (type == "" || type == "*") type = 0; |
|
for (var i = 0, l = aSlots.length; i < l; ++i) { |
|
var tFound = false; |
|
var aSource = (type+"").toLowerCase().split(","); |
|
var aDest = aSlots[i].type=="0"||aSlots[i].type=="*"?"0":aSlots[i].type; |
|
aDest = (aDest+"").toLowerCase().split(","); |
|
for(sI=0;sI<aSource.length;sI++){ |
|
for(dI=0;dI<aDest.length;dI++){ |
|
if (aSource[sI]=="_event_") aSource[sI] = LiteGraph.EVENT; |
|
if (aDest[sI]=="_event_") aDest[sI] = LiteGraph.EVENT; |
|
if (aSource[sI]=="*") aSource[sI] = 0; |
|
if (aDest[sI]=="*") aDest[sI] = 0; |
|
if (aSource[sI] == aDest[dI]) { |
|
if (preferFreeSlot && aSlots[i].links && aSlots[i].links !== null) continue; |
|
return !returnObj ? i : aSlots[i]; |
|
} |
|
} |
|
} |
|
} |
|
// if didnt find some, stop checking for free slots |
|
if (preferFreeSlot && !doNotUseOccupied){ |
|
for (var i = 0, l = aSlots.length; i < l; ++i) { |
|
var tFound = false; |
|
var aSource = (type+"").toLowerCase().split(","); |
|
var aDest = aSlots[i].type=="0"||aSlots[i].type=="*"?"0":aSlots[i].type; |
|
aDest = (aDest+"").toLowerCase().split(","); |
|
for(sI=0;sI<aSource.length;sI++){ |
|
for(dI=0;dI<aDest.length;dI++){ |
|
if (aSource[sI]=="*") aSource[sI] = 0; |
|
if (aDest[sI]=="*") aDest[sI] = 0; |
|
if (aSource[sI] == aDest[dI]) { |
|
return !returnObj ? i : aSlots[i]; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
return -1; |
|
}; |
|
|
|
/** |
|
* connect this node output to the input of another node BY TYPE |
|
* @method connectByType |
|
* @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 {string} target_type the input slot type of the target node |
|
* @return {Object} the link_info is created, otherwise null |
|
*/ |
|
LGraphNode.prototype.connectByType = function(slot, target_node, target_slotType, optsIn) { |
|
var optsIn = optsIn || {}; |
|
var optsDef = { createEventInCase: true |
|
,firstFreeIfOutputGeneralInCase: true |
|
,generalTypeInCase: true |
|
}; |
|
var opts = Object.assign(optsDef,optsIn); |
|
if (target_node && target_node.constructor === Number) { |
|
target_node = this.graph.getNodeById(target_node); |
|
} |
|
target_slot = target_node.findInputSlotByType(target_slotType, false, true); |
|
if (target_slot >= 0 && target_slot !== null){ |
|
//console.debug("CONNbyTYPE type "+target_slotType+" for "+target_slot) |
|
return this.connect(slot, target_node, target_slot); |
|
}else{ |
|
//console.log("type "+target_slotType+" not found or not free?") |
|
if (opts.createEventInCase && target_slotType == LiteGraph.EVENT){ |
|
// WILL CREATE THE onTrigger IN SLOT |
|
//console.debug("connect WILL CREATE THE onTrigger "+target_slotType+" to "+target_node); |
|
return this.connect(slot, target_node, -1); |
|
} |
|
// connect to the first general output slot if not found a specific type and |
|
if (opts.generalTypeInCase){ |
|
var target_slot = target_node.findInputSlotByType(0, false, true, true); |
|
//console.debug("connect TO a general type (*, 0), if not found the specific type ",target_slotType," to ",target_node,"RES_SLOT:",target_slot); |
|
if (target_slot >= 0){ |
|
return this.connect(slot, target_node, target_slot); |
|
} |
|
} |
|
// connect to the first free input slot if not found a specific type and this output is general |
|
if (opts.firstFreeIfOutputGeneralInCase && (target_slotType == 0 || target_slotType == "*" || target_slotType == "")){ |
|
var target_slot = target_node.findInputSlotFree({typesNotAccepted: [LiteGraph.EVENT] }); |
|
//console.debug("connect TO TheFirstFREE ",target_slotType," to ",target_node,"RES_SLOT:",target_slot); |
|
if (target_slot >= 0){ |
|
return this.connect(slot, target_node, target_slot); |
|
} |
|
} |
|
|
|
console.debug("no way to connect type: ",target_slotType," to targetNODE ",target_node); |
|
//TODO filter |
|
|
|
return null; |
|
} |
|
} |
|
|
|
/** |
|
* connect this node input to the output of another node BY TYPE |
|
* @method connectByType |
|
* @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 {string} target_type the output slot type of the target node |
|
* @return {Object} the link_info is created, otherwise null |
|
*/ |
|
LGraphNode.prototype.connectByTypeOutput = function(slot, source_node, source_slotType, optsIn) { |
|
var optsIn = optsIn || {}; |
|
var optsDef = { createEventInCase: true |
|
,firstFreeIfInputGeneralInCase: true |
|
,generalTypeInCase: true |
|
}; |
|
var opts = Object.assign(optsDef,optsIn); |
|
if (source_node && source_node.constructor === Number) { |
|
source_node = this.graph.getNodeById(source_node); |
|
} |
|
source_slot = source_node.findOutputSlotByType(source_slotType, false, true); |
|
if (source_slot >= 0 && source_slot !== null){ |
|
//console.debug("CONNbyTYPE OUT! type "+source_slotType+" for "+source_slot) |
|
return source_node.connect(source_slot, this, slot); |
|
}else{ |
|
|
|
// connect to the first general output slot if not found a specific type and |
|
if (opts.generalTypeInCase){ |
|
var source_slot = source_node.findOutputSlotByType(0, false, true, true); |
|
if (source_slot >= 0){ |
|
return source_node.connect(source_slot, this, slot); |
|
} |
|
} |
|
|
|
if (opts.createEventInCase && source_slotType == LiteGraph.EVENT){ |
|
// WILL CREATE THE onExecuted OUT SLOT |
|
if (LiteGraph.do_add_triggers_slots){ |
|
var source_slot = source_node.addOnExecutedOutput(); |
|
return source_node.connect(source_slot, this, slot); |
|
} |
|
} |
|
// connect to the first free output slot if not found a specific type and this input is general |
|
if (opts.firstFreeIfInputGeneralInCase && (source_slotType == 0 || source_slotType == "*" || source_slotType == "")){ |
|
var source_slot = source_node.findOutputSlotFree({typesNotAccepted: [LiteGraph.EVENT] }); |
|
if (source_slot >= 0){ |
|
return source_node.connect(source_slot, this, slot); |
|
} |
|
} |
|
|
|
console.debug("no way to connect byOUT type: ",source_slotType," to sourceNODE ",source_node); |
|
//TODO filter |
|
|
|
//console.log("type OUT! "+source_slotType+" not found or not free?") |
|
return null; |
|
} |
|
} |
|
|
|
/** |
|
* 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 {Object} the link_info is created, otherwise null |
|
*/ |
|
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 doesn't belong to any graph. Nodes must be added first to a graph before connecting them." |
|
); //due to link ids being associated with graphs |
|
return null; |
|
} |
|
|
|
//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 null; |
|
} |
|
} else if (!this.outputs || slot >= this.outputs.length) { |
|
if (LiteGraph.debug) { |
|
console.log("Connect: Error, slot number not found"); |
|
} |
|
return null; |
|
} |
|
|
|
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 null; |
|
} |
|
|
|
//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 null; |
|
} |
|
} else if (target_slot === LiteGraph.EVENT) { |
|
|
|
if (LiteGraph.do_add_triggers_slots){ |
|
//search for first slot with event? :: NO this is done outside |
|
//console.log("Connect: Creating triggerEvent"); |
|
// force mode |
|
target_node.changeMode(LiteGraph.ON_TRIGGER); |
|
target_slot = target_node.findInputSlot("onTrigger"); |
|
}else{ |
|
return null; // -- break -- |
|
} |
|
} else if ( |
|
!target_node.inputs || |
|
target_slot >= target_node.inputs.length |
|
) { |
|
if (LiteGraph.debug) { |
|
console.log("Connect: Error, slot number not found"); |
|
} |
|
return null; |
|
} |
|
|
|
var changed = false; |
|
|
|
var input = target_node.inputs[target_slot]; |
|
var link_info = null; |
|
var output = this.outputs[slot]; |
|
|
|
if (!this.outputs[slot]){ |
|
/*console.debug("Invalid slot passed: "+slot); |
|
console.debug(this.outputs);*/ |
|
return null; |
|
} |
|
|
|
// allow target node to change slot |
|
if (target_node.onBeforeConnectInput) { |
|
// This way node can choose another slot (or make a new one?) |
|
target_slot = target_node.onBeforeConnectInput(target_slot); //callback |
|
} |
|
|
|
//check target_slot and check connection types |
|
if (target_slot===false || target_slot===null || !LiteGraph.isValidConnection(output.type, input.type)) |
|
{ |
|
this.setDirtyCanvas(false, true); |
|
if(changed) |
|
this.graph.connectionChange(this, link_info); |
|
return null; |
|
}else{ |
|
//console.debug("valid connection",output.type, input.type); |
|
} |
|
|
|
//allows nodes to block connection, callback |
|
if (target_node.onConnectInput) { |
|
if ( target_node.onConnectInput(target_slot, output.type, output, this, slot) === false ) { |
|
return null; |
|
} |
|
} |
|
if (this.onConnectOutput) { // callback |
|
if ( this.onConnectOutput(slot, input.type, input, target_node, target_slot) === false ) { |
|
return null; |
|
} |
|
} |
|
|
|
//if there is something already plugged there, disconnect |
|
if (target_node.inputs[target_slot] && target_node.inputs[target_slot].link != null) { |
|
this.graph.beforeChange(); |
|
target_node.disconnectInput(target_slot, {doProcessChange: false}); |
|
changed = true; |
|
} |
|
if (output.links !== null && output.links.length){ |
|
switch(output.type){ |
|
case LiteGraph.EVENT: |
|
if (!LiteGraph.allow_multi_output_for_events){ |
|
this.graph.beforeChange(); |
|
this.disconnectOutput(slot, false, {doProcessChange: false}); // Input(target_slot, {doProcessChange: false}); |
|
changed = true; |
|
} |
|
break; |
|
default: |
|
break; |
|
} |
|
} |
|
|
|
//create link class |
|
link_info = new LLink( |
|
++this.graph.last_link_id, |
|
input.type || output.type, |
|
this.id, |
|
slot, |
|
target_node.id, |
|
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.INPUT, |
|
target_node, |
|
target_slot, |
|
this, |
|
slot |
|
); |
|
this.graph.onNodeConnectionChange( |
|
LiteGraph.OUTPUT, |
|
this, |
|
slot, |
|
target_node, |
|
target_slot |
|
); |
|
} |
|
|
|
this.setDirtyCanvas(false, true); |
|
this.graph.afterChange(); |
|
this.graph.connectionChange(this, link_info); |
|
|
|
return link_info; |
|
}; |
|
|
|
/** |
|
* 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 successfully |
|
*/ |
|
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 || !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 hasn't 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.OUTPUT, |
|
this, |
|
slot |
|
); |
|
this.graph.onNodeConnectionChange( |
|
LiteGraph.INPUT, |
|
target_node, |
|
link_info.target_slot |
|
); |
|
} |
|
break; |
|
} |
|
} |
|
} //all the links in this output slot |
|
else { |
|
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 hasn't 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 |
|
); |
|
this.graph.onNodeConnectionChange( |
|
LiteGraph.INPUT, |
|
target_node, |
|
link_info.target_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 successfully |
|
*/ |
|
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; |
|
if(link_id != null) |
|
{ |
|
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 |
|
); |
|
} |
|
if (this.graph && this.graph.onNodeConnectionChange) { |
|
this.graph.onNodeConnectionChange( |
|
LiteGraph.OUTPUT, |
|
target_node, |
|
i |
|
); |
|
this.graph.onNodeConnectionChange(LiteGraph.INPUT, this, slot); |
|
} |
|
} |
|
} //link != null |
|
|
|
this.setDirtyCanvas(false, true); |
|
if(this.graph) |
|
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) |
|
* @param {vec2} out [optional] a place to store the output, to free garbage |
|
* @return {[x,y]} the position |
|
**/ |
|
LGraphNode.prototype.getConnectionPos = function( |
|
is_input, |
|
slot_number, |
|
out |
|
) { |
|
out = out || new Float32Array(2); |
|
var num_slots = 0; |
|
if (is_input && this.inputs) { |
|
num_slots = this.inputs.length; |
|
} |
|
if (!is_input && this.outputs) { |
|
num_slots = this.outputs.length; |
|
} |
|
|
|
var offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5; |
|
|
|
if (this.flags.collapsed) { |
|
var w = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH; |
|
if (this.horizontal) { |
|
out[0] = this.pos[0] + w * 0.5; |
|
if (is_input) { |
|
out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT; |
|
} else { |
|
out[1] = this.pos[1]; |
|
} |
|
} else { |
|
if (is_input) { |
|
out[0] = this.pos[0]; |
|
} else { |
|
out[0] = this.pos[0] + w; |
|
} |
|
out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5; |
|
} |
|
return out; |
|
} |
|
|
|
//weird feature that never got finished |
|
if (is_input && slot_number == -1) { |
|
out[0] = this.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * 0.5; |
|
out[1] = this.pos[1] + LiteGraph.NODE_TITLE_HEIGHT * 0.5; |
|
return out; |
|
} |
|
|
|
//hard-coded pos |
|
if ( |
|
is_input && |
|
num_slots > slot_number && |
|
this.inputs[slot_number].pos |
|
) { |
|
out[0] = this.pos[0] + this.inputs[slot_number].pos[0]; |
|
out[1] = this.pos[1] + this.inputs[slot_number].pos[1]; |
|
return out; |
|
} else if ( |
|
!is_input && |
|
num_slots > slot_number && |
|
this.outputs[slot_number].pos |
|
) { |
|
out[0] = this.pos[0] + this.outputs[slot_number].pos[0]; |
|
out[1] = this.pos[1] + this.outputs[slot_number].pos[1]; |
|
return out; |
|
} |
|
|
|
//horizontal distributed slots |
|
if (this.horizontal) { |
|
out[0] = |
|
this.pos[0] + (slot_number + 0.5) * (this.size[0] / num_slots); |
|
if (is_input) { |
|
out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT; |
|
} else { |
|
out[1] = this.pos[1] + this.size[1]; |
|
} |
|
return out; |
|
} |
|
|
|
//default vertical slots |
|
if (is_input) { |
|
out[0] = this.pos[0] + offset; |
|
} else { |
|
out[0] = this.pos[0] + this.size[0] + 1 - offset; |
|
} |
|
out[1] = |
|
this.pos[1] + |
|
(slot_number + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + |
|
(this.constructor.slot_start_y || 0); |
|
return out; |
|
}; |
|
|
|
/* 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(); |
|
} |
|
|
|
if(this.graph.onNodeTrace) |
|
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.font_size = 24; |
|
this.color = LGraphCanvas.node_colors.pale_blue |
|
? LGraphCanvas.node_colors.pale_blue.groupcolor |
|
: "#AAA"; |
|
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.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; |
|
this.font = o.font; |
|
}; |
|
|
|
LGraphGroup.prototype.serialize = function() { |
|
var b = this._bounding; |
|
return { |
|
title: this.title, |
|
bounding: [ |
|
Math.round(b[0]), |
|
Math.round(b[1]), |
|
Math.round(b[2]), |
|
Math.round(b[3]) |
|
], |
|
color: this.color, |
|
font: this.font |
|
}; |
|
}; |
|
|
|
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; |
|
|
|
//**************************************** |
|
|
|
//Scale and Offset |
|
function DragAndScale(element, skip_events) { |
|
this.offset = new Float32Array([0, 0]); |
|
this.scale = 1; |
|
this.max_scale = 10; |
|
this.min_scale = 0.1; |
|
this.onredraw = null; |
|
this.enabled = true; |
|
this.last_mouse = [0, 0]; |
|
this.element = null; |
|
this.visible_area = new Float32Array(4); |
|
|
|
if (element) { |
|
this.element = element; |
|
if (!skip_events) { |
|
this.bindEvents(element); |
|
} |
|
} |
|
} |
|
|
|
LiteGraph.DragAndScale = DragAndScale; |
|
|
|
DragAndScale.prototype.bindEvents = function(element) { |
|
this.last_mouse = new Float32Array(2); |
|
|
|
this._binded_mouse_callback = this.onMouse.bind(this); |
|
|
|
LiteGraph.pointerListenerAdd(element,"down", this._binded_mouse_callback); |
|
LiteGraph.pointerListenerAdd(element,"move", this._binded_mouse_callback); |
|
LiteGraph.pointerListenerAdd(element,"up", this._binded_mouse_callback); |
|
|
|
element.addEventListener( |
|
"mousewheel", |
|
this._binded_mouse_callback, |
|
false |
|
); |
|
element.addEventListener("wheel", this._binded_mouse_callback, false); |
|
}; |
|
|
|
DragAndScale.prototype.computeVisibleArea = function( viewport ) { |
|
if (!this.element) { |
|
this.visible_area[0] = this.visible_area[1] = this.visible_area[2] = this.visible_area[3] = 0; |
|
return; |
|
} |
|
var width = this.element.width; |
|
var height = this.element.height; |
|
var startx = -this.offset[0]; |
|
var starty = -this.offset[1]; |
|
if( viewport ) |
|
{ |
|
startx += viewport[0] / this.scale; |
|
starty += viewport[1] / this.scale; |
|
width = viewport[2]; |
|
height = viewport[3]; |
|
} |
|
var endx = startx + width / this.scale; |
|
var endy = starty + height / this.scale; |
|
this.visible_area[0] = startx; |
|
this.visible_area[1] = starty; |
|
this.visible_area[2] = endx - startx; |
|
this.visible_area[3] = endy - starty; |
|
}; |
|
|
|
DragAndScale.prototype.onMouse = function(e) { |
|
if (!this.enabled) { |
|
return; |
|
} |
|
|
|
var canvas = this.element; |
|
var rect = canvas.getBoundingClientRect(); |
|
var x = e.clientX - rect.left; |
|
var y = e.clientY - rect.top; |
|
e.canvasx = x; |
|
e.canvasy = y; |
|
e.dragging = this.dragging; |
|
|
|
var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) ); |
|
|
|
//console.log("pointerevents: DragAndScale onMouse "+e.type+" "+is_inside); |
|
|
|
var ignore = false; |
|
if (this.onmouse) { |
|
ignore = this.onmouse(e); |
|
} |
|
|
|
if (e.type == LiteGraph.pointerevents_method+"down" && is_inside) { |
|
this.dragging = true; |
|
LiteGraph.pointerListenerRemove(canvas,"move",this._binded_mouse_callback); |
|
LiteGraph.pointerListenerAdd(document,"move",this._binded_mouse_callback); |
|
LiteGraph.pointerListenerAdd(document,"up",this._binded_mouse_callback); |
|
} else if (e.type == LiteGraph.pointerevents_method+"move") { |
|
if (!ignore) { |
|
var deltax = x - this.last_mouse[0]; |
|
var deltay = y - this.last_mouse[1]; |
|
if (this.dragging) { |
|
this.mouseDrag(deltax, deltay); |
|
} |
|
} |
|
} else if (e.type == LiteGraph.pointerevents_method+"up") { |
|
this.dragging = false; |
|
LiteGraph.pointerListenerRemove(document,"move",this._binded_mouse_callback); |
|
LiteGraph.pointerListenerRemove(document,"up",this._binded_mouse_callback); |
|
LiteGraph.pointerListenerAdd(canvas,"move",this._binded_mouse_callback); |
|
} else if ( is_inside && |
|
(e.type == "mousewheel" || |
|
e.type == "wheel" || |
|
e.type == "DOMMouseScroll") |
|
) { |
|
e.eventType = "mousewheel"; |
|
if (e.type == "wheel") { |
|
e.wheel = -e.deltaY; |
|
} else { |
|
e.wheel = |
|
e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60; |
|
} |
|
|
|
//from stack overflow |
|
e.delta = e.wheelDelta |
|
? e.wheelDelta / 40 |
|
: e.deltaY |
|
? -e.deltaY / 3 |
|
: 0; |
|
this.changeDeltaScale(1.0 + e.delta * 0.05); |
|
} |
|
|
|
this.last_mouse[0] = x; |
|
this.last_mouse[1] = y; |
|
|
|
if(is_inside) |
|
{ |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
return false; |
|
} |
|
}; |
|
|
|
DragAndScale.prototype.toCanvasContext = function(ctx) { |
|
ctx.scale(this.scale, this.scale); |
|
ctx.translate(this.offset[0], this.offset[1]); |
|
}; |
|
|
|
DragAndScale.prototype.convertOffsetToCanvas = function(pos) { |
|
//return [pos[0] / this.scale - this.offset[0], pos[1] / this.scale - this.offset[1]]; |
|
return [ |
|
(pos[0] + this.offset[0]) * this.scale, |
|
(pos[1] + this.offset[1]) * this.scale |
|
]; |
|
}; |
|
|
|
DragAndScale.prototype.convertCanvasToOffset = function(pos, out) { |
|
out = out || [0, 0]; |
|
out[0] = pos[0] / this.scale - this.offset[0]; |
|
out[1] = pos[1] / this.scale - this.offset[1]; |
|
return out; |
|
}; |
|
|
|
DragAndScale.prototype.mouseDrag = function(x, y) { |
|
this.offset[0] += x / this.scale; |
|
this.offset[1] += y / this.scale; |
|
|
|
if (this.onredraw) { |
|
this.onredraw(this); |
|
} |
|
}; |
|
|
|
DragAndScale.prototype.changeScale = function(value, zooming_center) { |
|
if (value < this.min_scale) { |
|
value = this.min_scale; |
|
} else if (value > this.max_scale) { |
|
value = this.max_scale; |
|
} |
|
|
|
if (value == this.scale) { |
|
return; |
|
} |
|
|
|
if (!this.element) { |
|
return; |
|
} |
|
|
|
var rect = this.element.getBoundingClientRect(); |
|
if (!rect) { |
|
return; |
|
} |
|
|
|
zooming_center = zooming_center || [ |
|
rect.width * 0.5, |
|
rect.height * 0.5 |
|
]; |
|
var center = this.convertCanvasToOffset(zooming_center); |
|
this.scale = value; |
|
if (Math.abs(this.scale - 1) < 0.01) { |
|
this.scale = 1; |
|
} |
|
|
|
var new_center = this.convertCanvasToOffset(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]; |
|
|
|
if (this.onredraw) { |
|
this.onredraw(this); |
|
} |
|
}; |
|
|
|
DragAndScale.prototype.changeDeltaScale = function(value, zooming_center) { |
|
this.changeScale(this.scale * value, zooming_center); |
|
}; |
|
|
|
DragAndScale.prototype.reset = function() { |
|
this.scale = 1; |
|
this.offset[0] = 0; |
|
this.offset[1] = 0; |
|
}; |
|
|
|
//********************************************************************************* |
|
// 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, viewport } |
|
*/ |
|
function LGraphCanvas(canvas, graph, options) { |
|
this.options = options = options || {}; |
|
|
|
//if(graph === undefined) |
|
// throw ("No graph assigned"); |
|
this.background_image = LGraphCanvas.DEFAULT_BACKGROUND_IMAGE; |
|
|
|
if (canvas && canvas.constructor === String) { |
|
canvas = document.querySelector(canvas); |
|
} |
|
|
|
this.ds = new DragAndScale(); |
|
this.zoom_modify_alpha = true; //otherwise it generates ugly patterns when scaling down too much |
|
|
|
this.title_text_font = "" + 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 = LiteGraph.LINK_COLOR; |
|
this.default_connection_color = { |
|
input_off: "#778", |
|
input_on: "#7F7", //"#BBD" |
|
output_off: "#778", |
|
output_on: "#7F7" //"#BBD" |
|
}; |
|
this.default_connection_color_byType = { |
|
/*number: "#7F7", |
|
string: "#77F", |
|
boolean: "#F77",*/ |
|
} |
|
this.default_connection_color_byTypeOff = { |
|
/*number: "#474", |
|
string: "#447", |
|
boolean: "#744",*/ |
|
}; |
|
|
|
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.clear_background = true; |
|
|
|
this.read_only = false; //if set to true users cannot modify the graph |
|
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.allow_reconnect_links = true; //allows to change a connection with having to redo it again |
|
this.align_to_grid = false; //snap to grid |
|
|
|
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.set_canvas_dirty_on_mouse_event = true; //forces to redraw the canvas if the mouse does anything |
|
this.always_render_background = false; |
|
this.render_shadows = true; |
|
this.render_canvas_border = true; |
|
this.render_connections_shadows = false; //too much cpu |
|
this.render_connections_border = true; |
|
this.render_curved_connections = false; |
|
this.render_connection_arrows = false; |
|
this.render_collapsed_slots = true; |
|
this.render_execution_order = false; |
|
this.render_title_colored = true; |
|
this.render_link_tooltip = true; |
|
|
|
this.links_render_mode = LiteGraph.SPLINE_LINK; |
|
|
|
this.mouse = [0, 0]; //mouse in canvas coordinates, where 0,0 is the top-left corner of the blue rectangle |
|
this.graph_mouse = [0, 0]; //mouse in graph coordinates, where 0,0 is the top-left corner of the blue rectangle |
|
this.canvas_mouse = this.graph_mouse; //LEGACY: REMOVE THIS, USE GRAPH_MOUSE INSTEAD |
|
|
|
//to personalize the search box |
|
this.onSearchBox = null; |
|
this.onSearchBoxSelection = null; |
|
|
|
//callbacks |
|
this.onMouse = null; |
|
this.onDrawBackground = null; //to render background objects (behind nodes and connections) in the canvas affected by transform |
|
this.onDrawForeground = null; //to render foreground objects (above nodes and connections) in the canvas affected by transform |
|
this.onDrawOverlay = null; //to render foreground objects not affected by transform (for GUIs) |
|
this.onDrawLinkTooltip = null; //called when rendering a tooltip |
|
this.onNodeMoved = null; //called after moving a node |
|
this.onSelectionChange = null; //called if the selection changes |
|
this.onConnectingChange = null; //called before any link changes |
|
this.onBeforeChange = null; //called before modifying the graph |
|
this.onAfterChange = null; //called after modifying the graph |
|
|
|
this.connections_width = 3; |
|
this.round_radius = 8; |
|
|
|
this.current_node = null; |
|
this.node_widget = null; //used for widgets |
|
this.over_link_center = null; |
|
this.last_mouse_position = [0, 0]; |
|
this.visible_area = this.ds.visible_area; |
|
this.visible_links = []; |
|
|
|
this.viewport = options.viewport || null; //to constraint render area to a portion of the canvas |
|
|
|
//link canvas and graph |
|
if (graph) { |
|
graph.attachCanvas(this); |
|
} |
|
|
|
this.setCanvas(canvas,options.skip_events); |
|
this.clear(); |
|
|
|
if (!options.skip_render) { |
|
this.startRendering(); |
|
} |
|
|
|
this.autoresize = options.autoresize; |
|
} |
|
|
|
global.LGraphCanvas = LiteGraph.LGraphCanvas = LGraphCanvas; |
|
|
|
LGraphCanvas.DEFAULT_BACKGROUND_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII="; |
|
|
|
LGraphCanvas.link_type_colors = { |
|
"-1": LiteGraph.EVENT_LINK_COLOR, |
|
number: "#AAA", |
|
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.dragging_canvas = false; |
|
|
|
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; |
|
this.pointer_is_down = false; |
|
this.pointer_is_double = false; |
|
this.visible_area.set([0, 0, 0, 0]); |
|
|
|
if (this.onClear) { |
|
this.onClear(); |
|
} |
|
}; |
|
|
|
/** |
|
* assigns a graph, you can reassign 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; |
|
} |
|
|
|
graph.attachCanvas(this); |
|
|
|
//remove the graph stack in case a subgraph was open |
|
if (this._graph_stack) |
|
this._graph_stack = null; |
|
|
|
this.setDirty(true, true); |
|
}; |
|
|
|
/** |
|
* returns the top level graph (in case there are subgraphs open on the canvas) |
|
* |
|
* @method getTopGraph |
|
* @return {LGraph} graph |
|
*/ |
|
LGraphCanvas.prototype.getTopGraph = function() |
|
{ |
|
if(this._graph_stack.length) |
|
return this._graph_stack[0]; |
|
return this.graph; |
|
} |
|
|
|
/** |
|
* 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.checkPanels(); |
|
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 subgraph_node = this.graph._subgraph_node; |
|
var graph = this._graph_stack.pop(); |
|
this.selected_nodes = {}; |
|
this.highlighted_links = {}; |
|
graph.attachCanvas(this); |
|
this.setDirty(true, true); |
|
if (subgraph_node) { |
|
this.centerOnNode(subgraph_node); |
|
this.selectNodes([subgraph_node]); |
|
} |
|
// when close sub graph back to offset [0, 0] scale 1 |
|
this.ds.offset = [0, 0] |
|
this.ds.scale = 1 |
|
}; |
|
|
|
/** |
|
* returns the visualy active graph (in case there are more in the stack) |
|
* @method getCurrentGraph |
|
* @return {LGraph} the active graph |
|
*/ |
|
LGraphCanvas.prototype.getCurrentGraph = function() { |
|
return this.graph; |
|
}; |
|
|
|
/** |
|
* 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; |
|
this.ds.element = canvas; |
|
|
|
if (!canvas) { |
|
return; |
|
} |
|
|
|
//this.canvas.tabindex = "1000"; |
|
canvas.className += " lgraphcanvas"; |
|
canvas.data = this; |
|
canvas.tabindex = "1"; //to allow key events |
|
|
|
//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 doesn't 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) |
|
// why here? this._mousemove_callback = this.processMouseMove.bind(this); |
|
// why here? 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) { |
|
//console.log("pointerevents: _doNothing "+e.type); |
|
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; |
|
} |
|
|
|
//console.log("pointerevents: bindEvents"); |
|
|
|
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); |
|
// why mousemove and mouseup were not binded here? |
|
this._mousemove_callback = this.processMouseMove.bind(this); |
|
this._mouseup_callback = this.processMouseUp.bind(this); |
|
|
|
//touch events -- TODO IMPLEMENT |
|
//this._touch_callback = this.touchHandler.bind(this); |
|
|
|
LiteGraph.pointerListenerAdd(canvas,"down", this._mousedown_callback, true); //down do not need to store the binded |
|
canvas.addEventListener("mousewheel", this._mousewheel_callback, false); |
|
|
|
LiteGraph.pointerListenerAdd(canvas,"up", this._mouseup_callback, true); // CHECK: ??? binded or not |
|
LiteGraph.pointerListenerAdd(canvas,"move", this._mousemove_callback); |
|
|
|
canvas.addEventListener("contextmenu", this._doNothing); |
|
canvas.addEventListener( |
|
"DOMMouseScroll", |
|
this._mousewheel_callback, |
|
false |
|
); |
|
|
|
//touch events -- THIS WAY DOES NOT WORK, finish implementing pointerevents, than clean the touchevents |
|
/*if( 'touchstart' in document.documentElement ) |
|
{ |
|
canvas.addEventListener("touchstart", this._touch_callback, true); |
|
canvas.addEventListener("touchmove", this._touch_callback, true); |
|
canvas.addEventListener("touchend", this._touch_callback, true); |
|
canvas.addEventListener("touchcancel", this._touch_callback, 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 doesn't fire keyup |
|
|
|
//Dropping 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; |
|
} |
|
|
|
//console.log("pointerevents: unbindEvents"); |
|
|
|
var ref_window = this.getCanvasWindow(); |
|
var document = ref_window.document; |
|
|
|
LiteGraph.pointerListenerRemove(this.canvas,"move", this._mousedown_callback); |
|
LiteGraph.pointerListenerRemove(this.canvas,"up", this._mousedown_callback); |
|
LiteGraph.pointerListenerRemove(this.canvas,"down", 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); |
|
|
|
//touch events -- THIS WAY DOES NOT WORK, finish implementing pointerevents, than clean the touchevents |
|
/*this.canvas.removeEventListener("touchstart", this._touch_callback ); |
|
this.canvas.removeEventListener("touchmove", this._touch_callback ); |
|
this.canvas.removeEventListener("touchend", this._touch_callback ); |
|
this.canvas.removeEventListener("touchcancel", this._touch_callback );*/ |
|
|
|
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 */ |
|
|
|
//used to block future mouse events (because of im gui) |
|
LGraphCanvas.prototype.blockClick = function() |
|
{ |
|
this.block_click = true; |
|
this.last_mouseclick = 0; |
|
} |
|
|
|
LGraphCanvas.prototype.processMouseDown = function(e) { |
|
|
|
if( this.set_canvas_dirty_on_mouse_event ) |
|
this.dirty_canvas = true; |
|
|
|
if (!this.graph) { |
|
return; |
|
} |
|
|
|
this.adjustMouseEvent(e); |
|
|
|
var ref_window = this.getCanvasWindow(); |
|
var document = ref_window.document; |
|
LGraphCanvas.active_canvas = this; |
|
var that = this; |
|
|
|
var x = e.clientX; |
|
var y = e.clientY; |
|
//console.log(y,this.viewport); |
|
//console.log("pointerevents: processMouseDown pointerId:"+e.pointerId+" which:"+e.which+" isPrimary:"+e.isPrimary+" :: x y "+x+" "+y); |
|
|
|
this.ds.viewport = this.viewport; |
|
var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) ); |
|
|
|
//move mouse move event to the window in case it drags outside of the canvas |
|
if(!this.options.skip_events) |
|
{ |
|
LiteGraph.pointerListenerRemove(this.canvas,"move", this._mousemove_callback); |
|
LiteGraph.pointerListenerAdd(ref_window.document,"move", this._mousemove_callback,true); //catch for the entire window |
|
LiteGraph.pointerListenerAdd(ref_window.document,"up", this._mouseup_callback,true); |
|
} |
|
|
|
if(!is_inside){ |
|
return; |
|
} |
|
|
|
var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes, 5 ); |
|
var skip_dragging = false; |
|
var skip_action = false; |
|
var now = LiteGraph.getTime(); |
|
var is_primary = (e.isPrimary === undefined || !e.isPrimary); |
|
var is_double_click = (now - this.last_mouseclick < 300) && is_primary; |
|
this.mouse[0] = e.clientX; |
|
this.mouse[1] = e.clientY; |
|
this.graph_mouse[0] = e.canvasX; |
|
this.graph_mouse[1] = e.canvasY; |
|
this.last_click_position = [this.mouse[0],this.mouse[1]]; |
|
|
|
if (this.pointer_is_down && is_primary ){ |
|
this.pointer_is_double = true; |
|
//console.log("pointerevents: pointer_is_double start"); |
|
}else{ |
|
this.pointer_is_double = false; |
|
} |
|
this.pointer_is_down = true; |
|
|
|
|
|
this.canvas.focus(); |
|
|
|
LiteGraph.closeAllContextMenus(ref_window); |
|
|
|
if (this.onMouse) |
|
{ |
|
if (this.onMouse(e) == true) |
|
return; |
|
} |
|
|
|
//left button mouse / single finger |
|
if (e.which == 1 && !this.pointer_is_double) |
|
{ |
|
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; |
|
} |
|
|
|
// clone node ALT dragging |
|
if (LiteGraph.alt_drag_do_clone_nodes && e.altKey && node && this.allow_interaction && !skip_action && !this.read_only) |
|
{ |
|
if (cloned = node.clone()){ |
|
cloned.pos[0] += 5; |
|
cloned.pos[1] += 5; |
|
this.graph.add(cloned,false,{doCalcSize: false}); |
|
node = cloned; |
|
skip_action = true; |
|
if (!block_drag_node) { |
|
if (this.allow_dragnodes) { |
|
this.graph.beforeChange(); |
|
this.node_dragged = node; |
|
} |
|
if (!this.selected_nodes[node.id]) { |
|
this.processNodeSelected(node, e); |
|
} |
|
} |
|
} |
|
} |
|
|
|
var clicking_canvas_bg = false; |
|
|
|
//when clicked on top of a node |
|
//and it is not interactive |
|
if (node && this.allow_interaction && !skip_action && !this.read_only) { |
|
if (!this.live_mode && !node.flags.pinned) { |
|
this.bringToFront(node); |
|
} //if it wasn't selected? |
|
|
|
//not dragging mouse to connect two slots |
|
if ( !this.connecting_node && !node.flags.collapsed && !this.live_mode ) { |
|
//Search for corner for resize |
|
if ( !skip_action && |
|
node.resizable !== false && |
|
isInsideRectangle( e.canvasX, |
|
e.canvasY, |
|
node.pos[0] + node.size[0] - 5, |
|
node.pos[1] + node.size[1] - 5, |
|
10, |
|
10 |
|
) |
|
) { |
|
this.graph.beforeChange(); |
|
this.resizing_node = node; |
|
this.canvas.style.cursor = "se-resize"; |
|
skip_action = true; |
|
} else { |
|
//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] - 15, |
|
link_pos[1] - 10, |
|
30, |
|
20 |
|
) |
|
) { |
|
this.connecting_node = node; |
|
this.connecting_output = output; |
|
this.connecting_output.slot_index = i; |
|
this.connecting_pos = node.getConnectionPos( false, i ); |
|
this.connecting_slot = i; |
|
|
|
if (LiteGraph.shift_click_do_break_link_from){ |
|
if (e.shiftKey) { |
|
node.disconnectOutput(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] - 15, |
|
link_pos[1] - 10, |
|
30, |
|
20 |
|
) |
|
) { |
|
if (is_double_click) { |
|
if (node.onInputDblClick) { |
|
node.onInputDblClick(i, e); |
|
} |
|
} else { |
|
if (node.onInputClick) { |
|
node.onInputClick(i, e); |
|
} |
|
} |
|
|
|
if (input.link !== null) { |
|
var link_info = this.graph.links[ |
|
input.link |
|
]; //before disconnecting |
|
if (LiteGraph.click_do_break_link_to){ |
|
node.disconnectInput(i); |
|
this.dirty_bgcanvas = true; |
|
skip_action = true; |
|
}else{ |
|
// do same action as has not node ? |
|
} |
|
|
|
if ( |
|
this.allow_reconnect_links || |
|
//this.move_destination_link_without_shift || |
|
e.shiftKey |
|
) { |
|
if (!LiteGraph.click_do_break_link_to){ |
|
node.disconnectInput(i); |
|
} |
|
this.connecting_node = this.graph._nodes_by_id[ |
|
link_info.origin_id |
|
]; |
|
this.connecting_slot = |
|
link_info.origin_slot; |
|
this.connecting_output = this.connecting_node.outputs[ |
|
this.connecting_slot |
|
]; |
|
this.connecting_pos = this.connecting_node.getConnectionPos( false, this.connecting_slot ); |
|
|
|
this.dirty_bgcanvas = true; |
|
skip_action = true; |
|
} |
|
|
|
|
|
}else{ |
|
// has not node |
|
} |
|
|
|
if (!skip_action){ |
|
// connect from in to out, from to to from |
|
this.connecting_node = node; |
|
this.connecting_input = input; |
|
this.connecting_input.slot_index = i; |
|
this.connecting_pos = node.getConnectionPos( true, i ); |
|
this.connecting_slot = i; |
|
|
|
this.dirty_bgcanvas = true; |
|
skip_action = true; |
|
} |
|
} |
|
} |
|
} |
|
} //not resizing |
|
} |
|
|
|
//it wasn't clicked on the links boxes |
|
if (!skip_action) { |
|
var block_drag_node = false; |
|
var pos = [e.canvasX - node.pos[0], e.canvasY - node.pos[1]]; |
|
|
|
//widgets |
|
var widget = this.processNodeWidgets( node, this.graph_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, pos, this ); |
|
} |
|
this.processNodeDblClicked(node); |
|
block_drag_node = true; |
|
} |
|
|
|
//if do not capture mouse |
|
if ( node.onMouseDown && node.onMouseDown( e, pos, this ) ) { |
|
block_drag_node = true; |
|
} else { |
|
//open subgraph button |
|
if(node.subgraph && !node.skip_subgraph_button) |
|
{ |
|
if ( !node.flags.collapsed && pos[0] > node.size[0] - LiteGraph.NODE_TITLE_HEIGHT && pos[1] < 0 ) { |
|
var that = this; |
|
setTimeout(function() { |
|
that.openSubgraph(node.subgraph); |
|
}, 10); |
|
} |
|
} |
|
|
|
if (this.live_mode) { |
|
clicking_canvas_bg = true; |
|
block_drag_node = true; |
|
} |
|
} |
|
|
|
if (!block_drag_node) { |
|
if (this.allow_dragnodes) { |
|
this.graph.beforeChange(); |
|
this.node_dragged = node; |
|
} |
|
if (!this.selected_nodes[node.id]) { |
|
this.processNodeSelected(node, e); |
|
} |
|
} |
|
|
|
this.dirty_canvas = true; |
|
} |
|
} //clicked outside of nodes |
|
else { |
|
if (!skip_action){ |
|
//search for link connector |
|
if(!this.read_only) { |
|
for (var i = 0; i < this.visible_links.length; ++i) { |
|
var link = this.visible_links[i]; |
|
var center = link._pos; |
|
if ( |
|
!center || |
|
e.canvasX < center[0] - 4 || |
|
e.canvasX > center[0] + 4 || |
|
e.canvasY < center[1] - 4 || |
|
e.canvasY > center[1] + 4 |
|
) { |
|
continue; |
|
} |
|
//link clicked |
|
this.showLinkMenu(link, e); |
|
this.over_link_center = null; //clear tooltip |
|
break; |
|
} |
|
} |
|
|
|
this.selected_group = this.graph.getGroupOnPos( e.canvasX, e.canvasY ); |
|
this.selected_group_resizing = false; |
|
if (this.selected_group && !this.read_only ) { |
|
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.ds.scale < 10) { |
|
this.selected_group_resizing = true; |
|
} else { |
|
this.selected_group.recomputeInsideNodes(); |
|
} |
|
} |
|
|
|
if (is_double_click && !this.read_only && this.allow_searchbox) { |
|
this.showSearchBox(e); |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
} |
|
|
|
clicking_canvas_bg = true; |
|
} |
|
} |
|
|
|
if (!skip_action && clicking_canvas_bg && this.allow_dragcanvas) { |
|
//console.log("pointerevents: dragging_canvas start"); |
|
this.dragging_canvas = true; |
|
} |
|
|
|
} else if (e.which == 2) { |
|
//middle button |
|
|
|
if (LiteGraph.middle_click_slot_add_default_node){ |
|
if (node && this.allow_interaction && !skip_action && !this.read_only){ |
|
//not dragging mouse to connect two slots |
|
if ( |
|
!this.connecting_node && |
|
!node.flags.collapsed && |
|
!this.live_mode |
|
) { |
|
var mClikSlot = false; |
|
var mClikSlot_index = false; |
|
var mClikSlot_isOut = false; |
|
//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] - 15,link_pos[1] - 10,30,20)) { |
|
mClikSlot = output; |
|
mClikSlot_index = i; |
|
mClikSlot_isOut = 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] - 15,link_pos[1] - 10,30,20)) { |
|
mClikSlot = input; |
|
mClikSlot_index = i; |
|
mClikSlot_isOut = false; |
|
break; |
|
} |
|
} |
|
} |
|
//console.log("middleClickSlots? "+mClikSlot+" & "+(mClikSlot_index!==false)); |
|
if (mClikSlot && mClikSlot_index!==false){ |
|
|
|
var alphaPosY = 0.5-((mClikSlot_index+1)/((mClikSlot_isOut?node.outputs.length:node.inputs.length))); |
|
var node_bounding = node.getBounding(); |
|
// estimate a position: this is a bad semi-bad-working mess .. REFACTOR with a correct autoplacement that knows about the others slots and nodes |
|
var posRef = [ (!mClikSlot_isOut?node_bounding[0]:node_bounding[0]+node_bounding[2])// + node_bounding[0]/this.canvas.width*150 |
|
,e.canvasY-80// + node_bounding[0]/this.canvas.width*66 // vertical "derive" |
|
]; |
|
var nodeCreated = this.createDefaultNodeForSlot({ nodeFrom: !mClikSlot_isOut?null:node |
|
,slotFrom: !mClikSlot_isOut?null:mClikSlot_index |
|
,nodeTo: !mClikSlot_isOut?node:null |
|
,slotTo: !mClikSlot_isOut?mClikSlot_index:null |
|
,position: posRef //,e: e |
|
,nodeType: "AUTO" //nodeNewType |
|
,posAdd:[!mClikSlot_isOut?-30:30, -alphaPosY*130] //-alphaPosY*30] |
|
,posSizeFix:[!mClikSlot_isOut?-1:0, 0] //-alphaPosY*2*/ |
|
}); |
|
|
|
} |
|
} |
|
} |
|
} |
|
|
|
} else if (e.which == 3 || this.pointer_is_double) { |
|
|
|
//right button |
|
if (this.allow_interaction && !skip_action && !this.read_only){ |
|
|
|
// is it hover a node ? |
|
if (node){ |
|
if(Object.keys(this.selected_nodes).length |
|
&& (this.selected_nodes[node.id] || e.shiftKey || e.ctrlKey || e.metaKey) |
|
){ |
|
// is multiselected or using shift to include the now node |
|
if (!this.selected_nodes[node.id]) this.selectNodes([node],true); // add this if not present |
|
}else{ |
|
// update selection |
|
this.selectNodes([node]); |
|
} |
|
} |
|
|
|
// show menu on this node |
|
this.processContextMenu(node, e); |
|
} |
|
|
|
} |
|
|
|
//TODO |
|
//if(this.node_selected != prev_selected) |
|
// this.onNodeSelectionChange(this.node_selected); |
|
|
|
this.last_mouse[0] = e.clientX; |
|
this.last_mouse[1] = e.clientY; |
|
this.last_mouseclick = LiteGraph.getTime(); |
|
this.last_mouse_dragging = true; |
|
|
|
/* |
|
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.set_canvas_dirty_on_mouse_event ) |
|
this.dirty_canvas = true; |
|
|
|
if (!this.graph) { |
|
return; |
|
} |
|
|
|
LGraphCanvas.active_canvas = this; |
|
this.adjustMouseEvent(e); |
|
var mouse = [e.clientX, e.clientY]; |
|
this.mouse[0] = mouse[0]; |
|
this.mouse[1] = mouse[1]; |
|
var delta = [ |
|
mouse[0] - this.last_mouse[0], |
|
mouse[1] - this.last_mouse[1] |
|
]; |
|
this.last_mouse = mouse; |
|
this.graph_mouse[0] = e.canvasX; |
|
this.graph_mouse[1] = e.canvasY; |
|
|
|
//console.log("pointerevents: processMouseMove "+e.pointerId+" "+e.isPrimary); |
|
|
|
if(this.block_click) |
|
{ |
|
//console.log("pointerevents: processMouseMove block_click"); |
|
e.preventDefault(); |
|
return false; |
|
} |
|
|
|
e.dragging = this.last_mouse_dragging; |
|
|
|
if (this.node_widget) { |
|
this.processNodeWidgets( |
|
this.node_widget[0], |
|
this.graph_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 && !this.read_only) |
|
{ |
|
//moving/resizing a 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.ds.scale; |
|
var deltay = delta[1] / this.ds.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) { |
|
////console.log("pointerevents: processMouseMove is dragging_canvas"); |
|
this.ds.offset[0] += delta[0] / this.ds.scale; |
|
this.ds.offset[1] += delta[1] / this.ds.scale; |
|
this.dirty_canvas = true; |
|
this.dirty_bgcanvas = true; |
|
} else if (this.allow_interaction && !this.read_only) { |
|
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) { |
|
|
|
if(node.redraw_on_mouse) |
|
this.dirty_canvas = true; |
|
|
|
//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, [e.canvasX - node.pos[0], e.canvasY - node.pos[1]], this ); |
|
} |
|
|
|
//if dragging a link |
|
if (this.connecting_node) { |
|
|
|
if (this.connecting_output){ |
|
|
|
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, don't 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; |
|
this._highlight_input_slot = node.inputs[slot]; // XXX CHECK THIS |
|
} |
|
} else { |
|
this._highlight_input = null; |
|
this._highlight_input_slot = null; // XXX CHECK THIS |
|
} |
|
} |
|
|
|
}else if(this.connecting_input){ |
|
|
|
var pos = this._highlight_output || [0, 0]; //to store the output of isOverNodeOutput |
|
|
|
//on top of output |
|
if (this.isOverNodeBox(node, e.canvasX, e.canvasY)) { |
|
//mouse on top of the corner box, don't know what to do |
|
} else { |
|
//check if I have a slot below de mouse |
|
var slot = this.isOverNodeOutput( node, e.canvasX, e.canvasY, pos ); |
|
if (slot != -1 && node.outputs[slot]) { |
|
var slot_type = node.outputs[slot].type; |
|
if ( LiteGraph.isValidConnection( this.connecting_input.type, slot_type ) ) { |
|
this._highlight_output = pos; |
|
} |
|
} else { |
|
this._highlight_output = 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 = "crosshair"; |
|
} |
|
} |
|
} else { //not over a node |
|
|
|
//search for link connector |
|
var over_link = null; |
|
for (var i = 0; i < this.visible_links.length; ++i) { |
|
var link = this.visible_links[i]; |
|
var center = link._pos; |
|
if ( |
|
!center || |
|
e.canvasX < center[0] - 4 || |
|
e.canvasX > center[0] + 4 || |
|
e.canvasY < center[1] - 4 || |
|
e.canvasY > center[1] + 4 |
|
) { |
|
continue; |
|
} |
|
over_link = link; |
|
break; |
|
} |
|
if( over_link != this.over_link_center ) |
|
{ |
|
this.over_link_center = over_link; |
|
this.dirty_canvas = true; |
|
} |
|
|
|
if (this.canvas) { |
|
this.canvas.style.cursor = ""; |
|
} |
|
} //end |
|
|
|
//send event to node if capturing input (used with widgets that allow drag outside of the area of the node) |
|
if ( this.node_capturing_input && this.node_capturing_input != node && this.node_capturing_input.onMouseMove ) { |
|
this.node_capturing_input.onMouseMove(e,[e.canvasX - this.node_capturing_input.pos[0],e.canvasY - this.node_capturing_input.pos[1]], this); |
|
} |
|
|
|
//node being dragged |
|
if (this.node_dragged && !this.live_mode) { |
|
//console.log("draggin!",this.selected_nodes); |
|
for (var i in this.selected_nodes) { |
|
var n = this.selected_nodes[i]; |
|
n.pos[0] += delta[0] / this.ds.scale; |
|
n.pos[1] += delta[1] / this.ds.scale; |
|
} |
|
|
|
this.dirty_canvas = true; |
|
this.dirty_bgcanvas = true; |
|
} |
|
|
|
if (this.resizing_node && !this.live_mode) { |
|
//convert mouse to node space |
|
var desired_size = [ e.canvasX - this.resizing_node.pos[0], e.canvasY - this.resizing_node.pos[1] ]; |
|
var min_size = this.resizing_node.computeSize(); |
|
desired_size[0] = Math.max( min_size[0], desired_size[0] ); |
|
desired_size[1] = Math.max( min_size[1], desired_size[1] ); |
|
this.resizing_node.setSize( desired_size ); |
|
|
|
this.canvas.style.cursor = "se-resize"; |
|
this.dirty_canvas = true; |
|
this.dirty_bgcanvas = true; |
|
} |
|
} |
|
|
|
e.preventDefault(); |
|
return false; |
|
}; |
|
|
|
/** |
|
* Called when a mouse up event has to be processed |
|
* @method processMouseUp |
|
**/ |
|
LGraphCanvas.prototype.processMouseUp = function(e) { |
|
|
|
var is_primary = ( e.isPrimary === undefined || e.isPrimary ); |
|
|
|
//early exit for extra pointer |
|
if(!is_primary){ |
|
/*e.stopPropagation(); |
|
e.preventDefault();*/ |
|
//console.log("pointerevents: processMouseUp pointerN_stop "+e.pointerId+" "+e.isPrimary); |
|
return false; |
|
} |
|
|
|
//console.log("pointerevents: processMouseUp "+e.pointerId+" "+e.isPrimary+" :: "+e.clientX+" "+e.clientY); |
|
|
|
if( this.set_canvas_dirty_on_mouse_event ) |
|
this.dirty_canvas = true; |
|
|
|
if (!this.graph) |
|
return; |
|
|
|
var window = this.getCanvasWindow(); |
|
var document = window.document; |
|
LGraphCanvas.active_canvas = this; |
|
|
|
//restore the mousemove event back to the canvas |
|
if(!this.options.skip_events) |
|
{ |
|
//console.log("pointerevents: processMouseUp adjustEventListener"); |
|
LiteGraph.pointerListenerRemove(document,"move", this._mousemove_callback,true); |
|
LiteGraph.pointerListenerAdd(this.canvas,"move", this._mousemove_callback,true); |
|
LiteGraph.pointerListenerRemove(document,"up", this._mouseup_callback,true); |
|
} |
|
|
|
this.adjustMouseEvent(e); |
|
var now = LiteGraph.getTime(); |
|
e.click_time = now - this.last_mouseclick; |
|
this.last_mouse_dragging = false; |
|
this.last_click_position = null; |
|
|
|
if(this.block_click) |
|
{ |
|
//console.log("pointerevents: processMouseUp block_clicks"); |
|
this.block_click = false; //used to avoid sending twice a click in a immediate button |
|
} |
|
|
|
//console.log("pointerevents: processMouseUp which: "+e.which); |
|
|
|
if (e.which == 1) { |
|
|
|
if( this.node_widget ) |
|
{ |
|
this.processNodeWidgets( this.node_widget[0], this.graph_mouse, e ); |
|
} |
|
|
|
//left button |
|
this.node_widget = null; |
|
|
|
if (this.selected_group) { |
|
var diffx = |
|
this.selected_group.pos[0] - |
|
Math.round(this.selected_group.pos[0]); |
|
var diffy = |
|
this.selected_group.pos[1] - |
|
Math.round(this.selected_group.pos[1]); |
|
this.selected_group.move(diffx, diffy, e.ctrlKey); |
|
this.selected_group.pos[0] = Math.round( |
|
this.selected_group.pos[0] |
|
); |
|
this.selected_group.pos[1] = Math.round( |
|
this.selected_group.pos[1] |
|
); |
|
if (this.selected_group._nodes.length) { |
|
this.dirty_canvas = true; |
|
} |
|
this.selected_group = null; |
|
} |
|
this.selected_group_resizing = false; |
|
|
|
var node = this.graph.getNodeOnPos( |
|
e.canvasX, |
|
e.canvasY, |
|
this.visible_nodes |
|
); |
|
|
|
if (this.dragging_rectangle) { |
|
if (this.graph) { |
|
var nodes = this.graph._nodes; |
|
var node_bounding = new Float32Array(4); |
|
|
|
//compute bounding and flip if left to right |
|
var w = Math.abs(this.dragging_rectangle[2]); |
|
var h = Math.abs(this.dragging_rectangle[3]); |
|
var startx = |
|
this.dragging_rectangle[2] < 0 |
|
? this.dragging_rectangle[0] - w |
|
: this.dragging_rectangle[0]; |
|
var starty = |
|
this.dragging_rectangle[3] < 0 |
|
? this.dragging_rectangle[1] - h |
|
: this.dragging_rectangle[1]; |
|
this.dragging_rectangle[0] = startx; |
|
this.dragging_rectangle[1] = starty; |
|
this.dragging_rectangle[2] = w; |
|
this.dragging_rectangle[3] = h; |
|
|
|
// test dragging rect size, if minimun simulate a click |
|
if (!node || (w > 10 && h > 10 )){ |
|
//test against all nodes (not visible because the rectangle maybe start outside |
|
var to_select = []; |
|
for (var i = 0; i < nodes.length; ++i) { |
|
var nodeX = nodes[i]; |
|
nodeX.getBounding(node_bounding); |
|
if ( |
|
!overlapBounding( |
|
this.dragging_rectangle, |
|
node_bounding |
|
) |
|
) { |
|
continue; |
|
} //out of the visible area |
|
to_select.push(nodeX); |
|
} |
|
if (to_select.length) { |
|
this.selectNodes(to_select,e.shiftKey); // add to selection with shift |
|
} |
|
}else{ |
|
// will select of update selection |
|
this.selectNodes([node],e.shiftKey||e.ctrlKey); // add to selection add to selection with ctrlKey or shiftKey |
|
} |
|
|
|
} |
|
this.dragging_rectangle = null; |
|
} else if (this.connecting_node) { |
|
//dragging a connection |
|
this.dirty_canvas = true; |
|
this.dirty_bgcanvas = true; |
|
|
|
var connInOrOut = this.connecting_output || this.connecting_input; |
|
var connType = connInOrOut.type; |
|
|
|
//node below mouse |
|
if (node) { |
|
|
|
/* no need to condition on event type.. just another type |
|
if ( |
|
connType == LiteGraph.EVENT && |
|
this.isOverNodeBox(node, e.canvasX, e.canvasY) |
|
) { |
|
|
|
this.connecting_node.connect( |
|
this.connecting_slot, |
|
node, |
|
LiteGraph.EVENT |
|
); |
|
|
|
} else {*/ |
|
|
|
//slot below mouse? connect |
|
|
|
if (this.connecting_output){ |
|
|
|
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 |
|
// look for a good slot |
|
this.connecting_node.connectByType(this.connecting_slot,node,connType); |
|
} |
|
|
|
}else if (this.connecting_input){ |
|
|
|
var slot = this.isOverNodeOutput( |
|
node, |
|
e.canvasX, |
|
e.canvasY |
|
); |
|
|
|
if (slot != -1) { |
|
node.connect(slot, this.connecting_node, this.connecting_slot); // this is inverted has output-input nature like |
|
} else { |
|
//not on top of an input |
|
// look for a good slot |
|
this.connecting_node.connectByTypeOutput(this.connecting_slot,node,connType); |
|
} |
|
|
|
} |
|
|
|
|
|
//} |
|
|
|
}else{ |
|
|
|
// add menu when releasing link in empty space |
|
if (LiteGraph.release_link_on_empty_shows_menu){ |
|
if (e.shiftKey && this.allow_searchbox){ |
|
if(this.connecting_output){ |
|
this.showSearchBox(e,{node_from: this.connecting_node, slot_from: this.connecting_output, type_filter_in: this.connecting_output.type}); |
|
}else if(this.connecting_input){ |
|
this.showSearchBox(e,{node_to: this.connecting_node, slot_from: this.connecting_input, type_filter_out: this.connecting_input.type}); |
|
} |
|
}else{ |
|
if(this.connecting_output){ |
|
this.showConnectionMenu({nodeFrom: this.connecting_node, slotFrom: this.connecting_output, e: e}); |
|
}else if(this.connecting_input){ |
|
this.showConnectionMenu({nodeTo: this.connecting_node, slotTo: this.connecting_input, e: e}); |
|
} |
|
} |
|
} |
|
} |
|
|
|
this.connecting_output = null; |
|
this.connecting_input = 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.graph.afterChange(this.resizing_node); |
|
this.resizing_node = null; |
|
} else if (this.node_dragged) { |
|
//node being dragged? |
|
var node = this.node_dragged; |
|
if ( |
|
node && |
|
e.click_time < 300 && |
|
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(); |
|
} |
|
|
|
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.align_to_grid ) { |
|
this.node_dragged.alignToGrid(); |
|
} |
|
if( this.onNodeMoved ) |
|
this.onNodeMoved( this.node_dragged ); |
|
this.graph.afterChange(this.node_dragged); |
|
this.node_dragged = null; |
|
} //no node being dragged |
|
else { |
|
//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] ], this ); |
|
} |
|
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(); |
|
*/ |
|
|
|
if (is_primary) |
|
{ |
|
this.pointer_is_down = false; |
|
this.pointer_is_double = false; |
|
} |
|
|
|
this.graph.change(); |
|
|
|
//console.log("pointerevents: processMouseUp stopPropagation"); |
|
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 x = e.clientX; |
|
var y = e.clientY; |
|
var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) ); |
|
if(!is_inside) |
|
return; |
|
|
|
var scale = this.ds.scale; |
|
|
|
if (delta > 0) { |
|
scale *= 1.1; |
|
} else if (delta < 0) { |
|
scale *= 1 / 1.1; |
|
} |
|
|
|
//this.setZoom( scale, [ e.clientX, e.clientY ] ); |
|
this.ds.changeScale(scale, [e.clientX, e.clientY]); |
|
|
|
this.graph.change(); |
|
|
|
e.preventDefault(); |
|
return false; // prevent default |
|
}; |
|
|
|
/** |
|
* returns 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; |
|
}; |
|
|
|
/** |
|
* returns the INDEX 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); |
|
var is_inside = false; |
|
if (node.horizontal) { |
|
is_inside = isInsideRectangle( |
|
canvasx, |
|
canvasy, |
|
link_pos[0] - 5, |
|
link_pos[1] - 10, |
|
10, |
|
20 |
|
); |
|
} else { |
|
is_inside = isInsideRectangle( |
|
canvasx, |
|
canvasy, |
|
link_pos[0] - 10, |
|
link_pos[1] - 5, |
|
40, |
|
10 |
|
); |
|
} |
|
if (is_inside) { |
|
if (slot_pos) { |
|
slot_pos[0] = link_pos[0]; |
|
slot_pos[1] = link_pos[1]; |
|
} |
|
return i; |
|
} |
|
} |
|
} |
|
return -1; |
|
}; |
|
|
|
/** |
|
* returns the INDEX if a position (in graph space) is on top of a node output slot |
|
* @method isOverNodeOuput |
|
**/ |
|
LGraphCanvas.prototype.isOverNodeOutput = function( |
|
node, |
|
canvasx, |
|
canvasy, |
|
slot_pos |
|
) { |
|
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); |
|
var is_inside = false; |
|
if (node.horizontal) { |
|
is_inside = isInsideRectangle( |
|
canvasx, |
|
canvasy, |
|
link_pos[0] - 5, |
|
link_pos[1] - 10, |
|
10, |
|
20 |
|
); |
|
} else { |
|
is_inside = isInsideRectangle( |
|
canvasx, |
|
canvasy, |
|
link_pos[0] - 10, |
|
link_pos[1] - 5, |
|
40, |
|
10 |
|
); |
|
} |
|
if (is_inside) { |
|
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) { |
|
//space |
|
this.dragging_canvas = true; |
|
block_default = true; |
|
} |
|
|
|
if (e.keyCode == 27) { |
|
//esc |
|
if(this.node_panel) this.node_panel.close(); |
|
if(this.options_panel) this.options_panel.close(); |
|
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) { |
|
if ( |
|
e.target.localName != "input" && |
|
e.target.localName != "textarea" |
|
) { |
|
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) { |
|
// space |
|
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(); |
|
e.stopImmediatePropagation(); |
|
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]; |
|
var cloned = node.clone(); |
|
if(!cloned) |
|
{ |
|
console.warn("node type not found: " + node.type ); |
|
continue; |
|
} |
|
clipboard_info.nodes.push(cloned.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, |
|
link_info.origin_slot, //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; |
|
} |
|
|
|
this.graph.beforeChange(); |
|
|
|
//create nodes |
|
var clipboard_info = JSON.parse(data); |
|
// calculate top-left node, could work without this processing but using diff with last node pos :: clipboard_info.nodes[clipboard_info.nodes.length-1].pos |
|
var posMin = false; |
|
var posMinIndexes = false; |
|
for (var i = 0; i < clipboard_info.nodes.length; ++i) { |
|
if (posMin){ |
|
if(posMin[0]>clipboard_info.nodes[i].pos[0]){ |
|
posMin[0] = clipboard_info.nodes[i].pos[0]; |
|
posMinIndexes[0] = i; |
|
} |
|
if(posMin[1]>clipboard_info.nodes[i].pos[1]){ |
|
posMin[1] = clipboard_info.nodes[i].pos[1]; |
|
posMinIndexes[1] = i; |
|
} |
|
} |
|
else{ |
|
posMin = [clipboard_info.nodes[i].pos[0], clipboard_info.nodes[i].pos[1]]; |
|
posMinIndexes = [i, i]; |
|
} |
|
} |
|
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); |
|
|
|
//paste in last known mouse position |
|
node.pos[0] += this.graph_mouse[0] - posMin[0]; //+= 5; |
|
node.pos[1] += this.graph_mouse[1] - posMin[1]; //+= 5; |
|
|
|
this.graph.add(node,{doProcessChange:false}); |
|
|
|
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]]; |
|
if( origin_node && target_node ) |
|
origin_node.connect(link_info[1], target_node, link_info[3]); |
|
else |
|
console.warn("Warning, nodes missing on pasting"); |
|
} |
|
|
|
this.selectNodes(nodes); |
|
|
|
this.graph.afterChange(); |
|
}; |
|
|
|
/** |
|
* process a item drop event on top the canvas |
|
* @method processDrop |
|
**/ |
|
LGraphCanvas.prototype.processDrop = function(e) { |
|
e.preventDefault(); |
|
this.adjustMouseEvent(e); |
|
var x = e.clientX; |
|
var y = e.clientY; |
|
var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) ); |
|
if(!is_inside){ |
|
return; |
|
// --- BREAK --- |
|
} |
|
|
|
var pos = [e.canvasX, e.canvasY]; |
|
|
|
|
|
var node = this.graph ? this.graph.getNodeOnPos(pos[0], pos[1]) : null; |
|
|
|
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 doesn't 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) { |
|
this.graph.beforeChange(); |
|
var node = LiteGraph.createNode(nodetype.type); |
|
node.pos = [e.canvasX, e.canvasY]; |
|
this.graph.add(node); |
|
if (node.onDropFile) { |
|
node.onDropFile(file); |
|
} |
|
this.graph.afterChange(); |
|
} |
|
} |
|
}; |
|
|
|
LGraphCanvas.prototype.processNodeDblClicked = function(n) { |
|
if (this.onShowNodePanel) { |
|
this.onShowNodePanel(n); |
|
} |
|
else |
|
{ |
|
this.showShowNodePanel(n); |
|
} |
|
|
|
if (this.onNodeDblClicked) { |
|
this.onNodeDblClicked(n); |
|
} |
|
|
|
this.setDirty(true); |
|
}; |
|
|
|
LGraphCanvas.prototype.processNodeSelected = function(node, e) { |
|
this.selectNode(node, e && (e.shiftKey||e.ctrlKey)); |
|
if (this.onNodeSelected) { |
|
this.onNodeSelected(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; |
|
if (typeof nodes == "string") nodes = [nodes]; |
|
for (var i in nodes) { |
|
var node = nodes[i]; |
|
if (node.is_selected) { |
|
continue; |
|
} |
|
|
|
if (!node.is_selected && node.onSelected) { |
|
node.onSelected(); |
|
} |
|
node.is_selected = true; |
|
this.selected_nodes[node.id] = node; |
|
|
|
if (node.inputs) { |
|
for (var j = 0; j < node.inputs.length; ++j) { |
|
this.highlighted_links[node.inputs[j].link] = true; |
|
} |
|
} |
|
if (node.outputs) { |
|
for (var j = 0; j < node.outputs.length; ++j) { |
|
var out = node.outputs[j]; |
|
if (out.links) { |
|
for (var k = 0; k < out.links.length; ++k) { |
|
this.highlighted_links[out.links[k]] = true; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
if( this.onSelectionChange ) |
|
this.onSelectionChange( this.selected_nodes ); |
|
|
|
this.setDirty(true); |
|
}; |
|
|
|
/** |
|
* removes a node from the current selection |
|
* @method deselectNode |
|
**/ |
|
LGraphCanvas.prototype.deselectNode = function(node) { |
|
if (!node.is_selected) { |
|
return; |
|
} |
|
if (node.onDeselected) { |
|
node.onDeselected(); |
|
} |
|
node.is_selected = false; |
|
|
|
if (this.onNodeDeselected) { |
|
this.onNodeDeselected(node); |
|
} |
|
|
|
//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.is_selected) { |
|
continue; |
|
} |
|
if (node.onDeselected) { |
|
node.onDeselected(); |
|
} |
|
node.is_selected = false; |
|
if (this.onNodeDeselected) { |
|
this.onNodeDeselected(node); |
|
} |
|
} |
|
this.selected_nodes = {}; |
|
this.current_node = null; |
|
this.highlighted_links = {}; |
|
if( this.onSelectionChange ) |
|
this.onSelectionChange( this.selected_nodes ); |
|
this.setDirty(true); |
|
}; |
|
|
|
/** |
|
* deletes all nodes in the current selection from the graph |
|
* @method deleteSelectedNodes |
|
**/ |
|
LGraphCanvas.prototype.deleteSelectedNodes = function() { |
|
|
|
this.graph.beforeChange(); |
|
|
|
for (var i in this.selected_nodes) { |
|
var node = this.selected_nodes[i]; |
|
|
|
if(node.block_delete) |
|
continue; |
|
|
|
//autoconnect when possible (very basic, only takes into account first input-output) |
|
if(node.inputs && node.inputs.length && node.outputs && node.outputs.length && LiteGraph.isValidConnection( node.inputs[0].type, node.outputs[0].type ) && node.inputs[0].link && node.outputs[0].links && node.outputs[0].links.length ) |
|
{ |
|
var input_link = node.graph.links[ node.inputs[0].link ]; |
|
var output_link = node.graph.links[ node.outputs[0].links[0] ]; |
|
var input_node = node.getInputNode(0); |
|
var output_node = node.getOutputNodes(0)[0]; |
|
if(input_node && output_node) |
|
input_node.connect( input_link.origin_slot, output_node, output_link.target_slot ); |
|
} |
|
this.graph.remove(node); |
|
if (this.onNodeDeselected) { |
|
this.onNodeDeselected(node); |
|
} |
|
} |
|
this.selected_nodes = {}; |
|
this.current_node = null; |
|
this.highlighted_links = {}; |
|
this.setDirty(true); |
|
this.graph.afterChange(); |
|
}; |
|
|
|
/** |
|
* centers the camera on a given node |
|
* @method centerOnNode |
|
**/ |
|
LGraphCanvas.prototype.centerOnNode = function(node) { |
|
this.ds.offset[0] = |
|
-node.pos[0] - |
|
node.size[0] * 0.5 + |
|
(this.canvas.width * 0.5) / this.ds.scale; |
|
this.ds.offset[1] = |
|
-node.pos[1] - |
|
node.size[1] * 0.5 + |
|
(this.canvas.height * 0.5) / this.ds.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) { |
|
var clientX_rel = 0; |
|
var clientY_rel = 0; |
|
|
|
if (this.canvas) { |
|
var b = this.canvas.getBoundingClientRect(); |
|
clientX_rel = e.clientX - b.left; |
|
clientY_rel = e.clientY - b.top; |
|
} else { |
|
clientX_rel = e.clientX; |
|
clientY_rel = e.clientY; |
|
} |
|
|
|
e.deltaX = clientX_rel - this.last_mouse_position[0]; |
|
e.deltaY = clientY_rel- this.last_mouse_position[1]; |
|
|
|
this.last_mouse_position[0] = clientX_rel; |
|
this.last_mouse_position[1] = clientY_rel; |
|
|
|
e.canvasX = clientX_rel / this.ds.scale - this.ds.offset[0]; |
|
e.canvasY = clientY_rel / this.ds.scale - this.ds.offset[1]; |
|
|
|
//console.log("pointerevents: adjustMouseEvent "+e.clientX+":"+e.clientY+" "+clientX_rel+":"+clientY_rel+" "+e.canvasX+":"+e.canvasY); |
|
}; |
|
|
|
/** |
|
* 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) { |
|
this.ds.changeScale(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.ds.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 from graph coordinates to canvas2D coordinates |
|
* @method convertOffsetToCanvas |
|
**/ |
|
LGraphCanvas.prototype.convertOffsetToCanvas = function(pos, out) { |
|
return this.ds.convertOffsetToCanvas(pos, out); |
|
}; |
|
|
|
/** |
|
* converts a coordinate from Canvas2D coordinates to graph space |
|
* @method convertCanvasToOffset |
|
**/ |
|
LGraphCanvas.prototype.convertCanvasToOffset = function(pos, out) { |
|
return this.ds.convertCanvasToOffset(pos, out); |
|
}; |
|
|
|
//converts event coordinates from canvas2D to graph coordinates |
|
LGraphCanvas.prototype.convertEventToCanvasOffset = function(e) { |
|
var rect = this.canvas.getBoundingClientRect(); |
|
return this.convertCanvasToOffset([ |
|
e.clientX - rect.left, |
|
e.clientY - 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 || this.canvas.width == 0 || this.canvas.height == 0) { |
|
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) { |
|
this.ds.computeVisibleArea(this.viewport); |
|
} |
|
|
|
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; |
|
} |
|
|
|
var canvas = this.canvas; |
|
if ( ctx.start2D && !this.viewport ) { |
|
ctx.start2D(); |
|
ctx.restore(); |
|
ctx.setTransform(1, 0, 0, 1, 0, 0); |
|
} |
|
|
|
//clip dirty area if there is one, otherwise work in full canvas |
|
var area = this.viewport || this.dirty_area; |
|
if (area) { |
|
ctx.save(); |
|
ctx.beginPath(); |
|
ctx.rect( area[0],area[1],area[2],area[3] ); |
|
ctx.clip(); |
|
} |
|
|
|
//clear |
|
//canvas.width = canvas.width; |
|
if (this.clear_background) { |
|
if(area) |
|
ctx.clearRect( area[0],area[1],area[2],area[3] ); |
|
else |
|
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, area ? area[0] : 0, area ? area[1] : 0 ); |
|
} |
|
|
|
if (this.graph) { |
|
//apply transformations |
|
ctx.save(); |
|
this.ds.toCanvasContext(ctx); |
|
|
|
//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(); |
|
} |
|
|
|
//on top (debug) |
|
if (this.render_execution_order) { |
|
this.drawExecutionOrder(ctx); |
|
} |
|
|
|
//connections ontop? |
|
if (this.graph.config.links_ontop) { |
|
if (!this.live_mode) { |
|
this.drawConnections(ctx); |
|
} |
|
} |
|
|
|
//current connection (the one being dragged by the mouse) |
|
if (this.connecting_pos != null) { |
|
ctx.lineWidth = this.connections_width; |
|
var link_color = null; |
|
|
|
var connInOrOut = this.connecting_output || this.connecting_input; |
|
|
|
var connType = connInOrOut.type; |
|
var connDir = connInOrOut.dir; |
|
if(connDir == null) |
|
{ |
|
if (this.connecting_output) |
|
connDir = this.connecting_node.horizontal ? LiteGraph.DOWN : LiteGraph.RIGHT; |
|
else |
|
connDir = this.connecting_node.horizontal ? LiteGraph.UP : LiteGraph.LEFT; |
|
} |
|
var connShape = connInOrOut.shape; |
|
|
|
switch (connType) { |
|
case LiteGraph.EVENT: |
|
link_color = LiteGraph.EVENT_LINK_COLOR; |
|
break; |
|
default: |
|
link_color = LiteGraph.CONNECTING_LINK_COLOR; |
|
} |
|
|
|
//the connection being dragged by the mouse |
|
this.renderLink( |
|
ctx, |
|
this.connecting_pos, |
|
[this.graph_mouse[0], this.graph_mouse[1]], |
|
null, |
|
false, |
|
null, |
|
link_color, |
|
connDir, |
|
LiteGraph.CENTER |
|
); |
|
|
|
ctx.beginPath(); |
|
if ( |
|
connType === LiteGraph.EVENT || |
|
connShape === LiteGraph.BOX_SHAPE |
|
) { |
|
ctx.rect( |
|
this.connecting_pos[0] - 6 + 0.5, |
|
this.connecting_pos[1] - 5 + 0.5, |
|
14, |
|
10 |
|
); |
|
ctx.fill(); |
|
ctx.beginPath(); |
|
ctx.rect( |
|
this.graph_mouse[0] - 6 + 0.5, |
|
this.graph_mouse[1] - 5 + 0.5, |
|
14, |
|
10 |
|
); |
|
} else if (connShape === LiteGraph.ARROW_SHAPE) { |
|
ctx.moveTo(this.connecting_pos[0] + 8, this.connecting_pos[1] + 0.5); |
|
ctx.lineTo(this.connecting_pos[0] - 4, this.connecting_pos[1] + 6 + 0.5); |
|
ctx.lineTo(this.connecting_pos[0] - 4, this.connecting_pos[1] - 6 + 0.5); |
|
ctx.closePath(); |
|
} |
|
else { |
|
ctx.arc( |
|
this.connecting_pos[0], |
|
this.connecting_pos[1], |
|
4, |
|
0, |
|
Math.PI * 2 |
|
); |
|
ctx.fill(); |
|
ctx.beginPath(); |
|
ctx.arc( |
|
this.graph_mouse[0], |
|
this.graph_mouse[1], |
|
4, |
|
0, |
|
Math.PI * 2 |
|
); |
|
} |
|
ctx.fill(); |
|
|
|
ctx.fillStyle = "#ffcc00"; |
|
if (this._highlight_input) { |
|
ctx.beginPath(); |
|
var shape = this._highlight_input_slot.shape; |
|
if (shape === LiteGraph.ARROW_SHAPE) { |
|
ctx.moveTo(this._highlight_input[0] + 8, this._highlight_input[1] + 0.5); |
|
ctx.lineTo(this._highlight_input[0] - 4, this._highlight_input[1] + 6 + 0.5); |
|
ctx.lineTo(this._highlight_input[0] - 4, this._highlight_input[1] - 6 + 0.5); |
|
ctx.closePath(); |
|
} else { |
|
ctx.arc( |
|
this._highlight_input[0], |
|
this._highlight_input[1], |
|
6, |
|
0, |
|
Math.PI * 2 |
|
); |
|
} |
|
ctx.fill(); |
|
} |
|
if (this._highlight_output) { |
|
ctx.beginPath(); |
|
if (shape === LiteGraph.ARROW_SHAPE) { |
|
ctx.moveTo(this._highlight_output[0] + 8, this._highlight_output[1] + 0.5); |
|
ctx.lineTo(this._highlight_output[0] - 4, this._highlight_output[1] + 6 + 0.5); |
|
ctx.lineTo(this._highlight_output[0] - 4, this._highlight_output[1] - 6 + 0.5); |
|
ctx.closePath(); |
|
} else { |
|
ctx.arc( |
|
this._highlight_output[0], |
|
this._highlight_output[1], |
|
6, |
|
0, |
|
Math.PI * 2 |
|
); |
|
} |
|
ctx.fill(); |
|
} |
|
} |
|
|
|
//the selection rectangle |
|
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] |
|
); |
|
} |
|
|
|
//on top of link center |
|
if(this.over_link_center && this.render_link_tooltip) |
|
this.drawLinkTooltip( ctx, this.over_link_center ); |
|
else |
|
if(this.onDrawLinkTooltip) //to remove |
|
this.onDrawLinkTooltip(ctx,null); |
|
|
|
//custom info |
|
if (this.onDrawForeground) { |
|
this.onDrawForeground(ctx, this.visible_rect); |
|
} |
|
|
|
ctx.restore(); |
|
} |
|
|
|
//draws panel in the corner |
|
if (this._graph_stack && this._graph_stack.length) { |
|
this.drawSubgraphPanel( ctx ); |
|
} |
|
|
|
|
|
if (this.onDrawOverlay) { |
|
this.onDrawOverlay(ctx); |
|
} |
|
|
|
if (area){ |
|
ctx.restore(); |
|
} |
|
|
|
if (ctx.finish2D) { |
|
//this is a function I use in webgl renderer |
|
ctx.finish2D(); |
|
} |
|
}; |
|
|
|
/** |
|
* draws the panel in the corner that shows subgraph properties |
|
* @method drawSubgraphPanel |
|
**/ |
|
LGraphCanvas.prototype.drawSubgraphPanel = function (ctx) { |
|
var subgraph = this.graph; |
|
var subnode = subgraph._subgraph_node; |
|
if (!subnode) { |
|
console.warn("subgraph without subnode"); |
|
return; |
|
} |
|
this.drawSubgraphPanelLeft(subgraph, subnode, ctx) |
|
this.drawSubgraphPanelRight(subgraph, subnode, ctx) |
|
} |
|
|
|
LGraphCanvas.prototype.drawSubgraphPanelLeft = function (subgraph, subnode, ctx) { |
|
var num = subnode.inputs ? subnode.inputs.length : 0; |
|
var w = 200; |
|
var h = Math.floor(LiteGraph.NODE_SLOT_HEIGHT * 1.6); |
|
|
|
ctx.fillStyle = "#111"; |
|
ctx.globalAlpha = 0.8; |
|
ctx.beginPath(); |
|
ctx.roundRect(10, 10, w, (num + 1) * h + 50, [8]); |
|
ctx.fill(); |
|
ctx.globalAlpha = 1; |
|
|
|
ctx.fillStyle = "#888"; |
|
ctx.font = "14px Arial"; |
|
ctx.textAlign = "left"; |
|
ctx.fillText("Graph Inputs", 20, 34); |
|
// var pos = this.mouse; |
|
|
|
if (this.drawButton(w - 20, 20, 20, 20, "X", "#151515")) { |
|
this.closeSubgraph(); |
|
return; |
|
} |
|
|
|
var y = 50; |
|
ctx.font = "14px Arial"; |
|
if (subnode.inputs) |
|
for (var i = 0; i < subnode.inputs.length; ++i) { |
|
var input = subnode.inputs[i]; |
|
if (input.not_subgraph_input) |
|
continue; |
|
|
|
//input button clicked |
|
if (this.drawButton(20, y + 2, w - 20, h - 2)) { |
|
var type = subnode.constructor.input_node_type || "graph/input"; |
|
this.graph.beforeChange(); |
|
var newnode = LiteGraph.createNode(type); |
|
if (newnode) { |
|
subgraph.add(newnode); |
|
this.block_click = false; |
|
this.last_click_position = null; |
|
this.selectNodes([newnode]); |
|
this.node_dragged = newnode; |
|
this.dragging_canvas = false; |
|
newnode.setProperty("name", input.name); |
|
newnode.setProperty("type", input.type); |
|
this.node_dragged.pos[0] = this.graph_mouse[0] - 5; |
|
this.node_dragged.pos[1] = this.graph_mouse[1] - 5; |
|
this.graph.afterChange(); |
|
} |
|
else |
|
console.error("graph input node not found:", type); |
|
} |
|
ctx.fillStyle = "#9C9"; |
|
ctx.beginPath(); |
|
ctx.arc(w - 16, y + h * 0.5, 5, 0, 2 * Math.PI); |
|
ctx.fill(); |
|
ctx.fillStyle = "#AAA"; |
|
ctx.fillText(input.name, 30, y + h * 0.75); |
|
// var tw = ctx.measureText(input.name); |
|
ctx.fillStyle = "#777"; |
|
ctx.fillText(input.type, 130, y + h * 0.75); |
|
y += h; |
|
} |
|
//add + button |
|
if (this.drawButton(20, y + 2, w - 20, h - 2, "+", "#151515", "#222")) { |
|
this.showSubgraphPropertiesDialog(subnode); |
|
} |
|
} |
|
LGraphCanvas.prototype.drawSubgraphPanelRight = function (subgraph, subnode, ctx) { |
|
var num = subnode.outputs ? subnode.outputs.length : 0; |
|
var canvas_w = this.bgcanvas.width |
|
var w = 200; |
|
var h = Math.floor(LiteGraph.NODE_SLOT_HEIGHT * 1.6); |
|
|
|
ctx.fillStyle = "#111"; |
|
ctx.globalAlpha = 0.8; |
|
ctx.beginPath(); |
|
ctx.roundRect(canvas_w - w - 10, 10, w, (num + 1) * h + 50, [8]); |
|
ctx.fill(); |
|
ctx.globalAlpha = 1; |
|
|
|
ctx.fillStyle = "#888"; |
|
ctx.font = "14px Arial"; |
|
ctx.textAlign = "left"; |
|
var title_text = "Graph Outputs" |
|
var tw = ctx.measureText(title_text).width |
|
ctx.fillText(title_text, (canvas_w - tw) - 20, 34); |
|
// var pos = this.mouse; |
|
if (this.drawButton(canvas_w - w, 20, 20, 20, "X", "#151515")) { |
|
this.closeSubgraph(); |
|
return; |
|
} |
|
|
|
var y = 50; |
|
ctx.font = "14px Arial"; |
|
if (subnode.outputs) |
|
for (var i = 0; i < subnode.outputs.length; ++i) { |
|
var output = subnode.outputs[i]; |
|
if (output.not_subgraph_input) |
|
continue; |
|
|
|
//output button clicked |
|
if (this.drawButton(canvas_w - w, y + 2, w - 20, h - 2)) { |
|
var type = subnode.constructor.output_node_type || "graph/output"; |
|
this.graph.beforeChange(); |
|
var newnode = LiteGraph.createNode(type); |
|
if (newnode) { |
|
subgraph.add(newnode); |
|
this.block_click = false; |
|
this.last_click_position = null; |
|
this.selectNodes([newnode]); |
|
this.node_dragged = newnode; |
|
this.dragging_canvas = false; |
|
newnode.setProperty("name", output.name); |
|
newnode.setProperty("type", output.type); |
|
this.node_dragged.pos[0] = this.graph_mouse[0] - 5; |
|
this.node_dragged.pos[1] = this.graph_mouse[1] - 5; |
|
this.graph.afterChange(); |
|
} |
|
else |
|
console.error("graph input node not found:", type); |
|
} |
|
ctx.fillStyle = "#9C9"; |
|
ctx.beginPath(); |
|
ctx.arc(canvas_w - w + 16, y + h * 0.5, 5, 0, 2 * Math.PI); |
|
ctx.fill(); |
|
ctx.fillStyle = "#AAA"; |
|
ctx.fillText(output.name, canvas_w - w + 30, y + h * 0.75); |
|
// var tw = ctx.measureText(input.name); |
|
ctx.fillStyle = "#777"; |
|
ctx.fillText(output.type, canvas_w - w + 130, y + h * 0.75); |
|
y += h; |
|
} |
|
//add + button |
|
if (this.drawButton(canvas_w - w, y + 2, w - 20, h - 2, "+", "#151515", "#222")) { |
|
this.showSubgraphPropertiesDialogRight(subnode); |
|
} |
|
} |
|
//Draws a button into the canvas overlay and computes if it was clicked using the immediate gui paradigm |
|
LGraphCanvas.prototype.drawButton = function( x,y,w,h, text, bgcolor, hovercolor, textcolor ) |
|
{ |
|
var ctx = this.ctx; |
|
bgcolor = bgcolor || LiteGraph.NODE_DEFAULT_COLOR; |
|
hovercolor = hovercolor || "#555"; |
|
textcolor = textcolor || LiteGraph.NODE_TEXT_COLOR; |
|
var yFix = y + LiteGraph.NODE_TITLE_HEIGHT + 2; // fix the height with the title |
|
var pos = this.mouse; |
|
var hover = LiteGraph.isInsideRectangle( pos[0], pos[1], x,yFix,w,h ); |
|
pos = this.last_click_position; |
|
var clicked = pos && LiteGraph.isInsideRectangle( pos[0], pos[1], x,yFix,w,h ); |
|
|
|
ctx.fillStyle = hover ? hovercolor : bgcolor; |
|
if(clicked) |
|
ctx.fillStyle = "#AAA"; |
|
ctx.beginPath(); |
|
ctx.roundRect(x,y,w,h,[4] ); |
|
ctx.fill(); |
|
|
|
if(text != null) |
|
{ |
|
if(text.constructor == String) |
|
{ |
|
ctx.fillStyle = textcolor; |
|
ctx.textAlign = "center"; |
|
ctx.font = ((h * 0.65)|0) + "px Arial"; |
|
ctx.fillText( text, x + w * 0.5,y + h * 0.75 ); |
|
ctx.textAlign = "left"; |
|
} |
|
} |
|
|
|
var was_clicked = clicked && !this.block_click; |
|
if(clicked) |
|
this.blockClick(); |
|
return was_clicked; |
|
} |
|
|
|
LGraphCanvas.prototype.isAreaClicked = function( x,y,w,h, hold_click ) |
|
{ |
|
var pos = this.mouse; |
|
var hover = LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h ); |
|
pos = this.last_click_position; |
|
var clicked = pos && LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h ); |
|
var was_clicked = clicked && !this.block_click; |
|
if(clicked && hold_click) |
|
this.blockClick(); |
|
return was_clicked; |
|
} |
|
|
|
/** |
|
* draws some useful stats in the corner of the canvas |
|
* @method renderInfo |
|
**/ |
|
LGraphCanvas.prototype.renderInfo = function(ctx, x, y) { |
|
x = x || 10; |
|
y = y || this.canvas.height - 80; |
|
|
|
ctx.save(); |
|
ctx.translate(x, y); |
|
|
|
ctx.font = "10px Arial"; |
|
ctx.fillStyle = "#888"; |
|
ctx.textAlign = "left"; |
|
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("N: " + this.graph._nodes.length + " [" + this.visible_nodes.length + "]", 5, 13 * 3 ); |
|
ctx.fillText("V: " + this.graph._version, 5, 13 * 4); |
|
ctx.fillText("FPS:" + this.fps.toFixed(2), 5, 13 * 5); |
|
} 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(); |
|
} |
|
|
|
var viewport = this.viewport || [0,0,ctx.canvas.width,ctx.canvas.height]; |
|
|
|
//clear |
|
if (this.clear_background) { |
|
ctx.clearRect( viewport[0], viewport[1], viewport[2], viewport[3] ); |
|
} |
|
|
|
//show subgraph stack header |
|
if (this._graph_stack && this._graph_stack.length) { |
|
ctx.save(); |
|
var parent_graph = this._graph_stack[this._graph_stack.length - 1]; |
|
var subgraph_node = this.graph._subgraph_node; |
|
ctx.strokeStyle = subgraph_node.bgcolor; |
|
ctx.lineWidth = 10; |
|
ctx.strokeRect(1, 1, canvas.width - 2, canvas.height - 2); |
|
ctx.lineWidth = 1; |
|
ctx.font = "40px Arial"; |
|
ctx.textAlign = "center"; |
|
ctx.fillStyle = subgraph_node.bgcolor || "#AAA"; |
|
var title = ""; |
|
for (var i = 1; i < this._graph_stack.length; ++i) { |
|
title += |
|
this._graph_stack[i]._subgraph_node.getTitle() + " >> "; |
|
} |
|
ctx.fillText( |
|
title + subgraph_node.getTitle(), |
|
canvas.width * 0.5, |
|
40 |
|
); |
|
ctx.restore(); |
|
} |
|
|
|
var bg_already_painted = false; |
|
if (this.onRenderBackground) { |
|
bg_already_painted = this.onRenderBackground(canvas, ctx); |
|
} |
|
|
|
//reset in case of error |
|
if ( !this.viewport ) |
|
{ |
|
ctx.restore(); |
|
ctx.setTransform(1, 0, 0, 1, 0, 0); |
|
} |
|
this.visible_links.length = 0; |
|
|
|
if (this.graph) { |
|
//apply transformations |
|
ctx.save(); |
|
this.ds.toCanvasContext(ctx); |
|
|
|
//render BG |
|
if ( |
|
this.background_image && |
|
this.ds.scale > 0.5 && |
|
!bg_already_painted |
|
) { |
|
if (this.zoom_modify_alpha) { |
|
ctx.globalAlpha = |
|
(1.0 - 0.5 / this.ds.scale) * this.editor_alpha; |
|
} else { |
|
ctx.globalAlpha = this.editor_alpha; |
|
} |
|
ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled = false; // ctx.mozImageSmoothingEnabled = |
|
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.imageSmoothingEnabled = true; //= ctx.mozImageSmoothingEnabled |
|
} |
|
|
|
//groups |
|
if (this.graph._groups.length && !this.live_mode) { |
|
this.drawGroups(canvas, ctx); |
|
} |
|
|
|
if (this.onDrawBackground) { |
|
this.onDrawBackground(ctx, this.visible_area); |
|
} |
|
if (this.onBackgroundRender) { |
|
//LEGACY |
|
console.error( |
|
"WARNING! onBackgroundRender deprecated, now is named onDrawBackground " |
|
); |
|
this.onBackgroundRender = null; |
|
} |
|
|
|
//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; |
|
} |
|
|
|
var low_quality = this.ds.scale < 0.6; //zoomed out |
|
|
|
//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, this.canvas); |
|
} |
|
} |
|
return; |
|
} |
|
|
|
var editor_alpha = this.editor_alpha; |
|
ctx.globalAlpha = editor_alpha; |
|
|
|
if (this.render_shadows && !low_quality) { |
|
ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR; |
|
ctx.shadowOffsetX = 2 * this.ds.scale; |
|
ctx.shadowOffsetY = 2 * this.ds.scale; |
|
ctx.shadowBlur = 3 * this.ds.scale; |
|
} else { |
|
ctx.shadowColor = "transparent"; |
|
} |
|
|
|
//custom draw collapsed method (draw after shadows because they are affected) |
|
if ( |
|
node.flags.collapsed && |
|
node.onDrawCollapsed && |
|
node.onDrawCollapsed(ctx, this) == true |
|
) { |
|
return; |
|
} |
|
|
|
//clip if required (mask) |
|
var shape = node._shape || LiteGraph.BOX_SHAPE; |
|
var size = temp_vec2; |
|
temp_vec2.set(node.size); |
|
var horizontal = node.horizontal; // || node.flags.horizontal; |
|
|
|
if (node.flags.collapsed) { |
|
ctx.font = this.inner_text_font; |
|
var title = node.getTitle ? node.getTitle() : node.title; |
|
if (title != null) { |
|
node._collapsed_width = Math.min( |
|
node.size[0], |
|
ctx.measureText(title).width + |
|
LiteGraph.NODE_TITLE_HEIGHT * 2 |
|
); //LiteGraph.NODE_COLLAPSED_WIDTH; |
|
size[0] = node._collapsed_width; |
|
size[1] = 0; |
|
} |
|
} |
|
|
|
if (node.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 |
|
if (node.has_errors) { |
|
bgcolor = "red"; |
|
} |
|
this.drawNodeShape( |
|
node, |
|
ctx, |
|
size, |
|
color, |
|
bgcolor, |
|
node.is_selected, |
|
node.mouseOver |
|
); |
|
ctx.shadowColor = "transparent"; |
|
|
|
//draw foreground |
|
if (node.onDrawForeground) { |
|
node.onDrawForeground(ctx, this, this.canvas); |
|
} |
|
|
|
//connection slots |
|
ctx.textAlign = horizontal ? "center" : "left"; |
|
ctx.font = this.inner_text_font; |
|
|
|
var render_text = !low_quality; |
|
|
|
var out_slot = this.connecting_output; |
|
var in_slot = this.connecting_input; |
|
ctx.lineWidth = 1; |
|
|
|
var max_y = 0; |
|
var slot_pos = new Float32Array(2); //to reuse |
|
|
|
//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]; |
|
|
|
var slot_type = slot.type; |
|
var slot_shape = slot.shape; |
|
|
|
ctx.globalAlpha = editor_alpha; |
|
//change opacity of incompatible slots when dragging a connection |
|
if ( this.connecting_output && !LiteGraph.isValidConnection( slot.type , out_slot.type) ) { |
|
ctx.globalAlpha = 0.4 * editor_alpha; |
|
} |
|
|
|
ctx.fillStyle = |
|
slot.link != null |
|
? slot.color_on || |
|
this.default_connection_color_byType[slot_type] || |
|
this.default_connection_color.input_on |
|
: slot.color_off || |
|
this.default_connection_color_byTypeOff[slot_type] || |
|
this.default_connection_color_byType[slot_type] || |
|
this.default_connection_color.input_off; |
|
|
|
var pos = node.getConnectionPos(true, i, slot_pos); |
|
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 == "array"){ |
|
slot_shape = LiteGraph.GRID_SHAPE; // place in addInput? addOutput instead? |
|
} |
|
|
|
var doStroke = true; |
|
|
|
if ( |
|
slot.type === LiteGraph.EVENT || |
|
slot.shape === LiteGraph.BOX_SHAPE |
|
) { |
|
if (horizontal) { |
|
ctx.rect( |
|
pos[0] - 5 + 0.5, |
|
pos[1] - 8 + 0.5, |
|
10, |
|
14 |
|
); |
|
} else { |
|
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 if (slot_shape === LiteGraph.GRID_SHAPE) { |
|
ctx.rect(pos[0] - 4, pos[1] - 4, 2, 2); |
|
ctx.rect(pos[0] - 1, pos[1] - 4, 2, 2); |
|
ctx.rect(pos[0] + 2, pos[1] - 4, 2, 2); |
|
ctx.rect(pos[0] - 4, pos[1] - 1, 2, 2); |
|
ctx.rect(pos[0] - 1, pos[1] - 1, 2, 2); |
|
ctx.rect(pos[0] + 2, pos[1] - 1, 2, 2); |
|
ctx.rect(pos[0] - 4, pos[1] + 2, 2, 2); |
|
ctx.rect(pos[0] - 1, pos[1] + 2, 2, 2); |
|
ctx.rect(pos[0] + 2, pos[1] + 2, 2, 2); |
|
doStroke = false; |
|
} else { |
|
if(low_quality) |
|
ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8 ); //faster |
|
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; |
|
if (horizontal || slot.dir == LiteGraph.UP) { |
|
ctx.fillText(text, pos[0], pos[1] - 10); |
|
} else { |
|
ctx.fillText(text, pos[0] + 10, pos[1] + 5); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
//output connection slots |
|
|
|
ctx.textAlign = horizontal ? "center" : "right"; |
|
ctx.strokeStyle = "black"; |
|
if (node.outputs) { |
|
for (var i = 0; i < node.outputs.length; i++) { |
|
var slot = node.outputs[i]; |
|
|
|
var slot_type = slot.type; |
|
var slot_shape = slot.shape; |
|
|
|
//change opacity of incompatible slots when dragging a connection |
|
if (this.connecting_input && !LiteGraph.isValidConnection( slot_type , in_slot.type) ) { |
|
ctx.globalAlpha = 0.4 * editor_alpha; |
|
} |
|
|
|
var pos = node.getConnectionPos(false, i, slot_pos); |
|
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.color_on || |
|
this.default_connection_color_byType[slot_type] || |
|
this.default_connection_color.output_on |
|
: slot.color_off || |
|
this.default_connection_color_byTypeOff[slot_type] || |
|
this.default_connection_color_byType[slot_type] || |
|
this.default_connection_color.output_off; |
|
ctx.beginPath(); |
|
//ctx.rect( node.size[0] - 14,i*14,10,10); |
|
|
|
if (slot_type == "array"){ |
|
slot_shape = LiteGraph.GRID_SHAPE; |
|
} |
|
|
|
var doStroke = true; |
|
|
|
if ( |
|
slot_type === LiteGraph.EVENT || |
|
slot_shape === LiteGraph.BOX_SHAPE |
|
) { |
|
if (horizontal) { |
|
ctx.rect( |
|
pos[0] - 5 + 0.5, |
|
pos[1] - 8 + 0.5, |
|
10, |
|
14 |
|
); |
|
} else { |
|
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 if (slot_shape === LiteGraph.GRID_SHAPE) { |
|
ctx.rect(pos[0] - 4, pos[1] - 4, 2, 2); |
|
ctx.rect(pos[0] - 1, pos[1] - 4, 2, 2); |
|
ctx.rect(pos[0] + 2, pos[1] - 4, 2, 2); |
|
ctx.rect(pos[0] - 4, pos[1] - 1, 2, 2); |
|
ctx.rect(pos[0] - 1, pos[1] - 1, 2, 2); |
|
ctx.rect(pos[0] + 2, pos[1] - 1, 2, 2); |
|
ctx.rect(pos[0] - 4, pos[1] + 2, 2, 2); |
|
ctx.rect(pos[0] - 1, pos[1] + 2, 2, 2); |
|
ctx.rect(pos[0] + 2, pos[1] + 2, 2, 2); |
|
doStroke = false; |
|
} else { |
|
if(low_quality) |
|
ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8 ); |
|
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(); |
|
if(!low_quality && doStroke) |
|
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; |
|
if (horizontal || slot.dir == LiteGraph.DOWN) { |
|
ctx.fillText(text, pos[0], pos[1] - 8); |
|
} else { |
|
ctx.fillText(text, pos[0] - 10, pos[1] + 5); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
ctx.textAlign = "left"; |
|
ctx.globalAlpha = 1; |
|
|
|
if (node.widgets) { |
|
var widgets_y = max_y; |
|
if (horizontal || node.widgets_up) { |
|
widgets_y = 2; |
|
} |
|
if( node.widgets_start_y != null ) |
|
widgets_y = node.widgets_start_y; |
|
this.drawNodeWidgets( |
|
node, |
|
widgets_y, |
|
ctx, |
|
this.node_widget && this.node_widget[0] == node |
|
? this.node_widget[1] |
|
: null |
|
); |
|
} |
|
} else if (this.render_collapsed_slots) { |
|
//if collapsed |
|
var input_slot = null; |
|
var output_slot = null; |
|
|
|
//get first connected slot to render |
|
if (node.inputs) { |
|
for (var i = 0; i < node.inputs.length; i++) { |
|
var slot = node.inputs[i]; |
|
if (slot.link == null) { |
|
continue; |
|
} |
|
input_slot = slot; |
|
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; |
|
} |
|
output_slot = slot; |
|
} |
|
} |
|
|
|
if (input_slot) { |
|
var x = 0; |
|
var y = LiteGraph.NODE_TITLE_HEIGHT * -0.5; //center |
|
if (horizontal) { |
|
x = node._collapsed_width * 0.5; |
|
y = -LiteGraph.NODE_TITLE_HEIGHT; |
|
} |
|
ctx.fillStyle = "#686"; |
|
ctx.beginPath(); |
|
if ( |
|
slot.type === LiteGraph.EVENT || |
|
slot.shape === LiteGraph.BOX_SHAPE |
|
) { |
|
ctx.rect(x - 7 + 0.5, y - 4, 14, 8); |
|
} else if (slot.shape === LiteGraph.ARROW_SHAPE) { |
|
ctx.moveTo(x + 8, y); |
|
ctx.lineTo(x + -4, y - 4); |
|
ctx.lineTo(x + -4, y + 4); |
|
ctx.closePath(); |
|
} else { |
|
ctx.arc(x, y, 4, 0, Math.PI * 2); |
|
} |
|
ctx.fill(); |
|
} |
|
|
|
if (output_slot) { |
|
var x = node._collapsed_width; |
|
var y = LiteGraph.NODE_TITLE_HEIGHT * -0.5; //center |
|
if (horizontal) { |
|
x = node._collapsed_width * 0.5; |
|
y = 0; |
|
} |
|
ctx.fillStyle = "#686"; |
|
ctx.strokeStyle = "black"; |
|
ctx.beginPath(); |
|
if ( |
|
slot.type === LiteGraph.EVENT || |
|
slot.shape === LiteGraph.BOX_SHAPE |
|
) { |
|
ctx.rect(x - 7 + 0.5, y - 4, 14, 8); |
|
} else if (slot.shape === LiteGraph.ARROW_SHAPE) { |
|
ctx.moveTo(x + 6, y); |
|
ctx.lineTo(x - 6, y - 4); |
|
ctx.lineTo(x - 6, y + 4); |
|
ctx.closePath(); |
|
} else { |
|
ctx.arc(x, y, 4, 0, Math.PI * 2); |
|
} |
|
ctx.fill(); |
|
//ctx.stroke(); |
|
} |
|
} |
|
|
|
if (node.clip_area) { |
|
ctx.restore(); |
|
} |
|
|
|
ctx.globalAlpha = 1.0; |
|
}; |
|
|
|
//used by this.over_link_center |
|
LGraphCanvas.prototype.drawLinkTooltip = function( ctx, link ) |
|
{ |
|
var pos = link._pos; |
|
ctx.fillStyle = "black"; |
|
ctx.beginPath(); |
|
ctx.arc( pos[0], pos[1], 3, 0, Math.PI * 2 ); |
|
ctx.fill(); |
|
|
|
if(link.data == null) |
|
return; |
|
|
|
if(this.onDrawLinkTooltip) |
|
if( this.onDrawLinkTooltip(ctx,link,this) == true ) |
|
return; |
|
|
|
var data = link.data; |
|
var text = null; |
|
|
|
if( data.constructor === Number ) |
|
text = data.toFixed(2); |
|
else if( data.constructor === String ) |
|
text = "\"" + data + "\""; |
|
else if( data.constructor === Boolean ) |
|
text = String(data); |
|
else if (data.toToolTip) |
|
text = data.toToolTip(); |
|
else |
|
text = "[" + data.constructor.name + "]"; |
|
|
|
if(text == null) |
|
return; |
|
text = text.substr(0,30); //avoid weird |
|
|
|
ctx.font = "14px Courier New"; |
|
var info = ctx.measureText(text); |
|
var w = info.width + 20; |
|
var h = 24; |
|
ctx.shadowColor = "black"; |
|
ctx.shadowOffsetX = 2; |
|
ctx.shadowOffsetY = 2; |
|
ctx.shadowBlur = 3; |
|
ctx.fillStyle = "#454"; |
|
ctx.beginPath(); |
|
ctx.roundRect( pos[0] - w*0.5, pos[1] - 15 - h, w, h, [3]); |
|
ctx.moveTo( pos[0] - 10, pos[1] - 15 ); |
|
ctx.lineTo( pos[0] + 10, pos[1] - 15 ); |
|
ctx.lineTo( pos[0], pos[1] - 5 ); |
|
ctx.fill(); |
|
ctx.shadowColor = "transparent"; |
|
ctx.textAlign = "center"; |
|
ctx.fillStyle = "#CEC"; |
|
ctx.fillText(text, pos[0], pos[1] - 15 - h * 0.3); |
|
} |
|
|
|
/** |
|
* draws the shape of the given node in the canvas |
|
* @method drawNodeShape |
|
**/ |
|
var tmp_area = new Float32Array(4); |
|
|
|
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; |
|
var low_quality = this.ds.scale < 0.5; |
|
|
|
//render node area depending on shape |
|
var shape = |
|
node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE; |
|
|
|
var title_mode = node.constructor.title_mode; |
|
|
|
var render_title = true; |
|
if (title_mode == LiteGraph.TRANSPARENT_TITLE || title_mode == LiteGraph.NO_TITLE) { |
|
render_title = false; |
|
} else if (title_mode == LiteGraph.AUTOHIDE_TITLE && mouse_over) { |
|
render_title = true; |
|
} |
|
|
|
var area = tmp_area; |
|
area[0] = 0; //x |
|
area[1] = render_title ? -title_height : 0; //y |
|
area[2] = size[0] + 1; //w |
|
area[3] = render_title ? size[1] + title_height : size[1]; //h |
|
|
|
var old_alpha = ctx.globalAlpha; |
|
|
|
//full node shape |
|
//if(node.flags.collapsed) |
|
{ |
|
ctx.beginPath(); |
|
if (shape == LiteGraph.BOX_SHAPE || low_quality) { |
|
ctx.fillRect(area[0], area[1], area[2], area[3]); |
|
} else if ( |
|
shape == LiteGraph.ROUND_SHAPE || |
|
shape == LiteGraph.CARD_SHAPE |
|
) { |
|
ctx.roundRect( |
|
area[0], |
|
area[1], |
|
area[2], |
|
area[3], |
|
shape == LiteGraph.CARD_SHAPE ? [this.round_radius,this.round_radius,0,0] : [this.round_radius] |
|
); |
|
} 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.fill(); |
|
|
|
//separator |
|
if(!node.flags.collapsed && render_title) |
|
{ |
|
ctx.shadowColor = "transparent"; |
|
ctx.fillStyle = "rgba(0,0,0,0.2)"; |
|
ctx.fillRect(0, -1, area[2], 2); |
|
} |
|
} |
|
ctx.shadowColor = "transparent"; |
|
|
|
if (node.onDrawBackground) { |
|
node.onDrawBackground(ctx, this, this.canvas, this.graph_mouse ); |
|
} |
|
|
|
//title bg (remember, it is rendered ABOVE the node) |
|
if (render_title || title_mode == LiteGraph.TRANSPARENT_TITLE) { |
|
//title bar |
|
if (node.onDrawTitleBar) { |
|
node.onDrawTitleBar( ctx, title_height, size, this.ds.scale, fgcolor ); |
|
} else if ( |
|
title_mode != LiteGraph.TRANSPARENT_TITLE && |
|
(node.constructor.title_color || this.render_title_colored) |
|
) { |
|
var title_color = node.constructor.title_color || fgcolor; |
|
|
|
if (node.flags.collapsed) { |
|
ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR; |
|
} |
|
|
|
//* gradient test |
|
if (this.use_gradients) { |
|
var grad = LGraphCanvas.gradients[title_color]; |
|
if (!grad) { |
|
grad = LGraphCanvas.gradients[ title_color ] = ctx.createLinearGradient(0, 0, 400, 0); |
|
grad.addColorStop(0, title_color); // TODO refactor: validate color !! prevent DOMException |
|
grad.addColorStop(1, "#000"); |
|
} |
|
ctx.fillStyle = grad; |
|
} else { |
|
ctx.fillStyle = title_color; |
|
} |
|
|
|
//ctx.globalAlpha = 0.5 * old_alpha; |
|
ctx.beginPath(); |
|
if (shape == LiteGraph.BOX_SHAPE || low_quality) { |
|
ctx.rect(0, -title_height, size[0] + 1, title_height); |
|
} else if ( shape == LiteGraph.ROUND_SHAPE || shape == LiteGraph.CARD_SHAPE ) { |
|
ctx.roundRect( |
|
0, |
|
-title_height, |
|
size[0] + 1, |
|
title_height, |
|
node.flags.collapsed ? [this.round_radius] : [this.round_radius,this.round_radius,0,0] |
|
); |
|
} |
|
ctx.fill(); |
|
ctx.shadowColor = "transparent"; |
|
} |
|
|
|
var colState = false; |
|
if (LiteGraph.node_box_coloured_by_mode){ |
|
if(LiteGraph.NODE_MODES_COLORS[node.mode]){ |
|
colState = LiteGraph.NODE_MODES_COLORS[node.mode]; |
|
} |
|
} |
|
if (LiteGraph.node_box_coloured_when_on){ |
|
colState = node.action_triggered ? "#FFF" : (node.execute_triggered ? "#AAA" : colState); |
|
} |
|
|
|
//title box |
|
var box_size = 10; |
|
if (node.onDrawTitleBox) { |
|
node.onDrawTitleBox(ctx, title_height, size, this.ds.scale); |
|
} else if ( |
|
shape == LiteGraph.ROUND_SHAPE || |
|
shape == LiteGraph.CIRCLE_SHAPE || |
|
shape == LiteGraph.CARD_SHAPE |
|
) { |
|
if (low_quality) { |
|
ctx.fillStyle = "black"; |
|
ctx.beginPath(); |
|
ctx.arc( |
|
title_height * 0.5, |
|
title_height * -0.5, |
|
box_size * 0.5 + 1, |
|
0, |
|
Math.PI * 2 |
|
); |
|
ctx.fill(); |
|
} |
|
|
|
ctx.fillStyle = node.boxcolor || colState || LiteGraph.NODE_DEFAULT_BOXCOLOR; |
|
if(low_quality) |
|
ctx.fillRect( title_height * 0.5 - box_size *0.5, title_height * -0.5 - box_size *0.5, box_size , box_size ); |
|
else |
|
{ |
|
ctx.beginPath(); |
|
ctx.arc( |
|
title_height * 0.5, |
|
title_height * -0.5, |
|
box_size * 0.5, |
|
0, |
|
Math.PI * 2 |
|
); |
|
ctx.fill(); |
|
} |
|
} else { |
|
if (low_quality) { |
|
ctx.fillStyle = "black"; |
|
ctx.fillRect( |
|
(title_height - box_size) * 0.5 - 1, |
|
(title_height + box_size) * -0.5 - 1, |
|
box_size + 2, |
|
box_size + 2 |
|
); |
|
} |
|
ctx.fillStyle = node.boxcolor || colState || LiteGraph.NODE_DEFAULT_BOXCOLOR; |
|
ctx.fillRect( |
|
(title_height - box_size) * 0.5, |
|
(title_height + box_size) * -0.5, |
|
box_size, |
|
box_size |
|
); |
|
} |
|
ctx.globalAlpha = old_alpha; |
|
|
|
//title text |
|
if (node.onDrawTitleText) { |
|
node.onDrawTitleText( |
|
ctx, |
|
title_height, |
|
size, |
|
this.ds.scale, |
|
this.title_text_font, |
|
selected |
|
); |
|
} |
|
if (!low_quality) { |
|
ctx.font = this.title_text_font; |
|
var title = String(node.getTitle()); |
|
if (title) { |
|
if (selected) { |
|
ctx.fillStyle = LiteGraph.NODE_SELECTED_TITLE_COLOR; |
|
} else { |
|
ctx.fillStyle = |
|
node.constructor.title_text_color || |
|
this.node_title_color; |
|
} |
|
if (node.flags.collapsed) { |
|
ctx.textAlign = "left"; |
|
var measure = ctx.measureText(title); |
|
ctx.fillText( |
|
title.substr(0,20), //avoid urls too long |
|
title_height,// + measure.width * 0.5, |
|
LiteGraph.NODE_TITLE_TEXT_Y - title_height |
|
); |
|
ctx.textAlign = "left"; |
|
} else { |
|
ctx.textAlign = "left"; |
|
ctx.fillText( |
|
title, |
|
title_height, |
|
LiteGraph.NODE_TITLE_TEXT_Y - title_height |
|
); |
|
} |
|
} |
|
} |
|
|
|
//subgraph box |
|
if (!node.flags.collapsed && node.subgraph && !node.skip_subgraph_button) { |
|
var w = LiteGraph.NODE_TITLE_HEIGHT; |
|
var x = node.size[0] - w; |
|
var over = LiteGraph.isInsideRectangle( this.graph_mouse[0] - node.pos[0], this.graph_mouse[1] - node.pos[1], x+2, -w+2, w-4, w-4 ); |
|
ctx.fillStyle = over ? "#888" : "#555"; |
|
if( shape == LiteGraph.BOX_SHAPE || low_quality) |
|
ctx.fillRect(x+2, -w+2, w-4, w-4); |
|
else |
|
{ |
|
ctx.beginPath(); |
|
ctx.roundRect(x+2, -w+2, w-4, w-4,[4]); |
|
ctx.fill(); |
|
} |
|
ctx.fillStyle = "#333"; |
|
ctx.beginPath(); |
|
ctx.moveTo(x + w * 0.2, -w * 0.6); |
|
ctx.lineTo(x + w * 0.8, -w * 0.6); |
|
ctx.lineTo(x + w * 0.5, -w * 0.3); |
|
ctx.fill(); |
|
} |
|
|
|
//custom title render |
|
if (node.onDrawTitle) { |
|
node.onDrawTitle(ctx); |
|
} |
|
} |
|
|
|
//render selection marker |
|
if (selected) { |
|
if (node.onBounding) { |
|
node.onBounding(area); |
|
} |
|
|
|
if (title_mode == LiteGraph.TRANSPARENT_TITLE) { |
|
area[1] -= title_height; |
|
area[3] += title_height; |
|
} |
|
ctx.lineWidth = 1; |
|
ctx.globalAlpha = 0.8; |
|
ctx.beginPath(); |
|
if (shape == LiteGraph.BOX_SHAPE) { |
|
ctx.rect( |
|
-6 + area[0], |
|
-6 + area[1], |
|
12 + area[2], |
|
12 + area[3] |
|
); |
|
} else if ( |
|
shape == LiteGraph.ROUND_SHAPE || |
|
(shape == LiteGraph.CARD_SHAPE && node.flags.collapsed) |
|
) { |
|
ctx.roundRect( |
|
-6 + area[0], |
|
-6 + area[1], |
|
12 + area[2], |
|
12 + area[3], |
|
[this.round_radius * 2] |
|
); |
|
} else if (shape == LiteGraph.CARD_SHAPE) { |
|
ctx.roundRect( |
|
-6 + area[0], |
|
-6 + area[1], |
|
12 + area[2], |
|
12 + area[3], |
|
[this.round_radius * 2,2,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 = LiteGraph.NODE_BOX_OUTLINE_COLOR; |
|
ctx.stroke(); |
|
ctx.strokeStyle = fgcolor; |
|
ctx.globalAlpha = 1; |
|
} |
|
|
|
// these counter helps in conditioning drawing based on if the node has been executed or an action occurred |
|
if (node.execute_triggered>0) node.execute_triggered--; |
|
if (node.action_triggered>0) node.action_triggered--; |
|
}; |
|
|
|
var margin_area = new Float32Array(4); |
|
var link_bounding = new Float32Array(4); |
|
var tempA = new Float32Array(2); |
|
var tempB = new Float32Array(2); |
|
|
|
/** |
|
* draws every connection visible in the canvas |
|
* OPTIMIZE THIS: pre-catch connections position instead of recomputing them every time |
|
* @method drawConnections |
|
**/ |
|
LGraphCanvas.prototype.drawConnections = function(ctx) { |
|
var now = LiteGraph.getTime(); |
|
var visible_area = this.visible_area; |
|
margin_area[0] = visible_area[0] - 20; |
|
margin_area[1] = visible_area[1] - 20; |
|
margin_area[2] = visible_area[2] + 40; |
|
margin_area[3] = visible_area[3] + 40; |
|
|
|
//draw connections |
|
ctx.lineWidth = this.connections_width; |
|
|
|
ctx.fillStyle = "#AAA"; |
|
ctx.strokeStyle = "#AAA"; |
|
ctx.globalAlpha = this.editor_alpha; |
|
//for every node |
|
var nodes = this.graph._nodes; |
|
for (var n = 0, l = nodes.length; n < l; ++n) { |
|
var node = 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) { |
|
continue; |
|
} |
|
|
|
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; |
|
} |
|
|
|
//find link info |
|
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, |
|
tempA |
|
); |
|
} |
|
var end_node_slotpos = node.getConnectionPos(true, i, tempB); |
|
|
|
//compute link bounding |
|
link_bounding[0] = start_node_slotpos[0]; |
|
link_bounding[1] = start_node_slotpos[1]; |
|
link_bounding[2] = end_node_slotpos[0] - start_node_slotpos[0]; |
|
link_bounding[3] = end_node_slotpos[1] - start_node_slotpos[1]; |
|
if (link_bounding[2] < 0) { |
|
link_bounding[0] += link_bounding[2]; |
|
link_bounding[2] = Math.abs(link_bounding[2]); |
|
} |
|
if (link_bounding[3] < 0) { |
|
link_bounding[1] += link_bounding[3]; |
|
link_bounding[3] = Math.abs(link_bounding[3]); |
|
} |
|
|
|
//skip links outside of the visible area of the canvas |
|
if (!overlapBounding(link_bounding, margin_area)) { |
|
continue; |
|
} |
|
|
|
var start_slot = start_node.outputs[start_node_slot]; |
|
var end_slot = node.inputs[i]; |
|
if (!start_slot || !end_slot) { |
|
continue; |
|
} |
|
var start_dir = |
|
start_slot.dir || |
|
(start_node.horizontal ? LiteGraph.DOWN : LiteGraph.RIGHT); |
|
var end_dir = |
|
end_slot.dir || |
|
(node.horizontal ? LiteGraph.UP : LiteGraph.LEFT); |
|
|
|
this.renderLink( |
|
ctx, |
|
start_node_slotpos, |
|
end_node_slotpos, |
|
link, |
|
false, |
|
0, |
|
null, |
|
start_dir, |
|
end_dir |
|
); |
|
|
|
//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 tmp = ctx.globalAlpha; |
|
ctx.globalAlpha = tmp * f; |
|
this.renderLink( |
|
ctx, |
|
start_node_slotpos, |
|
end_node_slotpos, |
|
link, |
|
true, |
|
f, |
|
"white", |
|
start_dir, |
|
end_dir |
|
); |
|
ctx.globalAlpha = tmp; |
|
} |
|
} |
|
} |
|
ctx.globalAlpha = 1; |
|
}; |
|
|
|
/** |
|
* draws a link between two points |
|
* @method renderLink |
|
* @param {vec2} a start pos |
|
* @param {vec2} b end pos |
|
* @param {Object} link the link object with all the link info |
|
* @param {boolean} skip_border ignore the shadow of the link |
|
* @param {boolean} flow show flow animation (for events) |
|
* @param {string} color the color for the link |
|
* @param {number} start_dir the direction enum |
|
* @param {number} end_dir the direction enum |
|
* @param {number} num_sublines number of sublines (useful to represent vec3 or rgb) |
|
**/ |
|
LGraphCanvas.prototype.renderLink = function( |
|
ctx, |
|
a, |
|
b, |
|
link, |
|
skip_border, |
|
flow, |
|
color, |
|
start_dir, |
|
end_dir, |
|
num_sublines |
|
) { |
|
if (link) { |
|
this.visible_links.push(link); |
|
} |
|
|
|
//choose color |
|
if (!color && link) { |
|
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"; |
|
} |
|
|
|
start_dir = start_dir || LiteGraph.RIGHT; |
|
end_dir = end_dir || LiteGraph.LEFT; |
|
|
|
var dist = distance(a, b); |
|
|
|
if (this.render_connections_border && this.ds.scale > 0.6) { |
|
ctx.lineWidth = this.connections_width + 4; |
|
} |
|
ctx.lineJoin = "round"; |
|
num_sublines = num_sublines || 1; |
|
if (num_sublines > 1) { |
|
ctx.lineWidth = 0.5; |
|
} |
|
|
|
//begin line shape |
|
ctx.beginPath(); |
|
for (var i = 0; i < num_sublines; i += 1) { |
|
var offsety = (i - (num_sublines - 1) * 0.5) * 5; |
|
|
|
if (this.links_render_mode == LiteGraph.SPLINE_LINK) { |
|
ctx.moveTo(a[0], a[1] + offsety); |
|
var start_offset_x = 0; |
|
var start_offset_y = 0; |
|
var end_offset_x = 0; |
|
var end_offset_y = 0; |
|
switch (start_dir) { |
|
case LiteGraph.LEFT: |
|
start_offset_x = dist * -0.25; |
|
break; |
|
case LiteGraph.RIGHT: |
|
start_offset_x = dist * 0.25; |
|
break; |
|
case LiteGraph.UP: |
|
start_offset_y = dist * -0.25; |
|
break; |
|
case LiteGraph.DOWN: |
|
start_offset_y = dist * 0.25; |
|
break; |
|
} |
|
switch (end_dir) { |
|
case LiteGraph.LEFT: |
|
end_offset_x = dist * -0.25; |
|
break; |
|
case LiteGraph.RIGHT: |
|
end_offset_x = dist * 0.25; |
|
break; |
|
case LiteGraph.UP: |
|
end_offset_y = dist * -0.25; |
|
break; |
|
case LiteGraph.DOWN: |
|
end_offset_y = dist * 0.25; |
|
break; |
|
} |
|
ctx.bezierCurveTo( |
|
a[0] + start_offset_x, |
|
a[1] + start_offset_y + offsety, |
|
b[0] + end_offset_x, |
|
b[1] + end_offset_y + offsety, |
|
b[0], |
|
b[1] + offsety |
|
); |
|
} else if (this.links_render_mode == LiteGraph.LINEAR_LINK) { |
|
ctx.moveTo(a[0], a[1] + offsety); |
|
var start_offset_x = 0; |
|
var start_offset_y = 0; |
|
var end_offset_x = 0; |
|
var end_offset_y = 0; |
|
switch (start_dir) { |
|
case LiteGraph.LEFT: |
|
start_offset_x = -1; |
|
break; |
|
case LiteGraph.RIGHT: |
|
start_offset_x = 1; |
|
break; |
|
case LiteGraph.UP: |
|
start_offset_y = -1; |
|
break; |
|
case LiteGraph.DOWN: |
|
start_offset_y = 1; |
|
break; |
|
} |
|
switch (end_dir) { |
|
case LiteGraph.LEFT: |
|
end_offset_x = -1; |
|
break; |
|
case LiteGraph.RIGHT: |
|
end_offset_x = 1; |
|
break; |
|
case LiteGraph.UP: |
|
end_offset_y = -1; |
|
break; |
|
case LiteGraph.DOWN: |
|
end_offset_y = 1; |
|
break; |
|
} |
|
var l = 15; |
|
ctx.lineTo( |
|
a[0] + start_offset_x * l, |
|
a[1] + start_offset_y * l + offsety |
|
); |
|
ctx.lineTo( |
|
b[0] + end_offset_x * l, |
|
b[1] + end_offset_y * l + offsety |
|
); |
|
ctx.lineTo(b[0], b[1] + offsety); |
|
} else if (this.links_render_mode == LiteGraph.STRAIGHT_LINK) { |
|
ctx.moveTo(a[0], a[1]); |
|
var start_x = a[0]; |
|
var start_y = a[1]; |
|
var end_x = b[0]; |
|
var end_y = b[1]; |
|
if (start_dir == LiteGraph.RIGHT) { |
|
start_x += 10; |
|
} else { |
|
start_y += 10; |
|
} |
|
if (end_dir == LiteGraph.LEFT) { |
|
end_x -= 10; |
|
} else { |
|
end_y -= 10; |
|
} |
|
ctx.lineTo(start_x, start_y); |
|
ctx.lineTo((start_x + end_x) * 0.5, start_y); |
|
ctx.lineTo((start_x + end_x) * 0.5, end_y); |
|
ctx.lineTo(end_x, end_y); |
|
ctx.lineTo(b[0], b[1]); |
|
} else { |
|
return; |
|
} //unknown |
|
} |
|
|
|
//rendering the outline of the connection can be a little bit slow |
|
if ( |
|
this.render_connections_border && |
|
this.ds.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 |
|
|
|
var pos = this.computeConnectionPoint(a, b, 0.5, start_dir, end_dir); |
|
if (link && link._pos) { |
|
link._pos[0] = pos[0]; |
|
link._pos[1] = pos[1]; |
|
} |
|
|
|
//render arrow in the middle |
|
if ( |
|
this.ds.scale >= 0.6 && |
|
this.highquality_render && |
|
end_dir != LiteGraph.CENTER |
|
) { |
|
//render arrow |
|
if (this.render_connection_arrows) { |
|
//compute two points in the connection |
|
var posA = this.computeConnectionPoint( |
|
a, |
|
b, |
|
0.25, |
|
start_dir, |
|
end_dir |
|
); |
|
var posB = this.computeConnectionPoint( |
|
a, |
|
b, |
|
0.26, |
|
start_dir, |
|
end_dir |
|
); |
|
var posC = this.computeConnectionPoint( |
|
a, |
|
b, |
|
0.75, |
|
start_dir, |
|
end_dir |
|
); |
|
var posD = this.computeConnectionPoint( |
|
a, |
|
b, |
|
0.76, |
|
start_dir, |
|
end_dir |
|
); |
|
|
|
//compute the angle between them so the arrow points in the right direction |
|
var angleA = 0; |
|
var angleB = 0; |
|
if (this.render_curved_connections) { |
|
angleA = -Math.atan2(posB[0] - posA[0], posB[1] - posA[1]); |
|
angleB = -Math.atan2(posD[0] - posC[0], posD[1] - posC[1]); |
|
} else { |
|
angleB = angleA = b[1] > a[1] ? 0 : Math.PI; |
|
} |
|
|
|
//render arrow |
|
ctx.save(); |
|
ctx.translate(posA[0], posA[1]); |
|
ctx.rotate(angleA); |
|
ctx.beginPath(); |
|
ctx.moveTo(-5, -3); |
|
ctx.lineTo(0, +7); |
|
ctx.lineTo(+5, -3); |
|
ctx.fill(); |
|
ctx.restore(); |
|
ctx.save(); |
|
ctx.translate(posC[0], posC[1]); |
|
ctx.rotate(angleB); |
|
ctx.beginPath(); |
|
ctx.moveTo(-5, -3); |
|
ctx.lineTo(0, +7); |
|
ctx.lineTo(+5, -3); |
|
ctx.fill(); |
|
ctx.restore(); |
|
} |
|
|
|
//circle |
|
ctx.beginPath(); |
|
ctx.arc(pos[0], pos[1], 5, 0, Math.PI * 2); |
|
ctx.fill(); |
|
} |
|
|
|
//render flowing points |
|
if (flow) { |
|
ctx.fillStyle = color; |
|
for (var i = 0; i < 5; ++i) { |
|
var f = (LiteGraph.getTime() * 0.001 + i * 0.2) % 1; |
|
var pos = this.computeConnectionPoint( |
|
a, |
|
b, |
|
f, |
|
start_dir, |
|
end_dir |
|
); |
|
ctx.beginPath(); |
|
ctx.arc(pos[0], pos[1], 5, 0, 2 * Math.PI); |
|
ctx.fill(); |
|
} |
|
} |
|
}; |
|
|
|
//returns the link center point based on curvature |
|
LGraphCanvas.prototype.computeConnectionPoint = function( |
|
a, |
|
b, |
|
t, |
|
start_dir, |
|
end_dir |
|
) { |
|
start_dir = start_dir || LiteGraph.RIGHT; |
|
end_dir = end_dir || LiteGraph.LEFT; |
|
|
|
var dist = distance(a, b); |
|
var p0 = a; |
|
var p1 = [a[0], a[1]]; |
|
var p2 = [b[0], b[1]]; |
|
var p3 = b; |
|
|
|
switch (start_dir) { |
|
case LiteGraph.LEFT: |
|
p1[0] += dist * -0.25; |
|
break; |
|
case LiteGraph.RIGHT: |
|
p1[0] += dist * 0.25; |
|
break; |
|
case LiteGraph.UP: |
|
p1[1] += dist * -0.25; |
|
break; |
|
case LiteGraph.DOWN: |
|
p1[1] += dist * 0.25; |
|
break; |
|
} |
|
switch (end_dir) { |
|
case LiteGraph.LEFT: |
|
p2[0] += dist * -0.25; |
|
break; |
|
case LiteGraph.RIGHT: |
|
p2[0] += dist * 0.25; |
|
break; |
|
case LiteGraph.UP: |
|
p2[1] += dist * -0.25; |
|
break; |
|
case LiteGraph.DOWN: |
|
p2[1] += dist * 0.25; |
|
break; |
|
} |
|
|
|
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.drawExecutionOrder = function(ctx) { |
|
ctx.shadowColor = "transparent"; |
|
ctx.globalAlpha = 0.25; |
|
|
|
ctx.textAlign = "center"; |
|
ctx.strokeStyle = "white"; |
|
ctx.globalAlpha = 0.75; |
|
|
|
var visible_nodes = this.visible_nodes; |
|
for (var i = 0; i < visible_nodes.length; ++i) { |
|
var node = visible_nodes[i]; |
|
ctx.fillStyle = "black"; |
|
ctx.fillRect( |
|
node.pos[0] - LiteGraph.NODE_TITLE_HEIGHT, |
|
node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT, |
|
LiteGraph.NODE_TITLE_HEIGHT, |
|
LiteGraph.NODE_TITLE_HEIGHT |
|
); |
|
if (node.order == 0) { |
|
ctx.strokeRect( |
|
node.pos[0] - LiteGraph.NODE_TITLE_HEIGHT + 0.5, |
|
node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT + 0.5, |
|
LiteGraph.NODE_TITLE_HEIGHT, |
|
LiteGraph.NODE_TITLE_HEIGHT |
|
); |
|
} |
|
ctx.fillStyle = "#FFF"; |
|
ctx.fillText( |
|
node.order, |
|
node.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * -0.5, |
|
node.pos[1] - 6 |
|
); |
|
} |
|
ctx.globalAlpha = 1; |
|
}; |
|
|
|
/** |
|
* 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.ds.scale > 0.5; |
|
ctx.save(); |
|
ctx.globalAlpha = this.editor_alpha; |
|
var outline_color = LiteGraph.WIDGET_OUTLINE_COLOR; |
|
var background_color = LiteGraph.WIDGET_BGCOLOR; |
|
var text_color = LiteGraph.WIDGET_TEXT_COLOR; |
|
var secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; |
|
var margin = 15; |
|
|
|
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 = outline_color; |
|
ctx.fillStyle = "#222"; |
|
ctx.textAlign = "left"; |
|
//ctx.lineWidth = 2; |
|
if(w.disabled) |
|
ctx.globalAlpha *= 0.5; |
|
var widget_width = w.width || width; |
|
|
|
switch (w.type) { |
|
case "button": |
|
if (w.clicked) { |
|
ctx.fillStyle = "#AAA"; |
|
w.clicked = false; |
|
this.dirty_canvas = true; |
|
} |
|
ctx.fillRect(margin, y, widget_width - margin * 2, H); |
|
if(show_text && !w.disabled) |
|
ctx.strokeRect( margin, y, widget_width - margin * 2, H ); |
|
if (show_text) { |
|
ctx.textAlign = "center"; |
|
ctx.fillStyle = text_color; |
|
ctx.fillText(w.name, widget_width * 0.5, y + H * 0.7); |
|
} |
|
break; |
|
case "toggle": |
|
ctx.textAlign = "left"; |
|
ctx.strokeStyle = outline_color; |
|
ctx.fillStyle = background_color; |
|
ctx.beginPath(); |
|
if (show_text) |
|
ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]); |
|
else |
|
ctx.rect(margin, y, widget_width - margin * 2, H ); |
|
ctx.fill(); |
|
if(show_text && !w.disabled) |
|
ctx.stroke(); |
|
ctx.fillStyle = w.value ? "#89A" : "#333"; |
|
ctx.beginPath(); |
|
ctx.arc( widget_width - margin * 2, y + H * 0.5, H * 0.36, 0, Math.PI * 2 ); |
|
ctx.fill(); |
|
if (show_text) { |
|
ctx.fillStyle = secondary_text_color; |
|
if (w.name != null) { |
|
ctx.fillText(w.name, margin * 2, y + H * 0.7); |
|
} |
|
ctx.fillStyle = w.value ? text_color : secondary_text_color; |
|
ctx.textAlign = "right"; |
|
ctx.fillText( |
|
w.value |
|
? w.options.on || "true" |
|
: w.options.off || "false", |
|
widget_width - 40, |
|
y + H * 0.7 |
|
); |
|
} |
|
break; |
|
case "slider": |
|
ctx.fillStyle = background_color; |
|
ctx.fillRect(margin, y, widget_width - margin * 2, 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(margin, y, nvalue * (widget_width - margin * 2), H); |
|
if(show_text && !w.disabled) |
|
ctx.strokeRect(margin, y, widget_width - margin * 2, H); |
|
if (w.marker) { |
|
var marker_nvalue = (w.marker - w.options.min) / range; |
|
ctx.fillStyle = "#AA9"; |
|
ctx.fillRect( margin + marker_nvalue * (widget_width - margin * 2), y, 2, H ); |
|
} |
|
if (show_text) { |
|
ctx.textAlign = "center"; |
|
ctx.fillStyle = text_color; |
|
ctx.fillText( |
|
w.name + " " + Number(w.value).toFixed(3), |
|
widget_width * 0.5, |
|
y + H * 0.7 |
|
); |
|
} |
|
break; |
|
case "number": |
|
case "combo": |
|
ctx.textAlign = "left"; |
|
ctx.strokeStyle = outline_color; |
|
ctx.fillStyle = background_color; |
|
ctx.beginPath(); |
|
if(show_text) |
|
ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5] ); |
|
else |
|
ctx.rect(margin, y, widget_width - margin * 2, H ); |
|
ctx.fill(); |
|
if (show_text) { |
|
if(!w.disabled) |
|
ctx.stroke(); |
|
ctx.fillStyle = text_color; |
|
if(!w.disabled) |
|
{ |
|
ctx.beginPath(); |
|
ctx.moveTo(margin + 16, y + 5); |
|
ctx.lineTo(margin + 6, y + H * 0.5); |
|
ctx.lineTo(margin + 16, y + H - 5); |
|
ctx.fill(); |
|
ctx.beginPath(); |
|
ctx.moveTo(widget_width - margin - 16, y + 5); |
|
ctx.lineTo(widget_width - margin - 6, y + H * 0.5); |
|
ctx.lineTo(widget_width - margin - 16, y + H - 5); |
|
ctx.fill(); |
|
} |
|
ctx.fillStyle = secondary_text_color; |
|
ctx.fillText(w.name, margin * 2 + 5, y + H * 0.7); |
|
ctx.fillStyle = text_color; |
|
ctx.textAlign = "right"; |
|
if (w.type == "number") { |
|
ctx.fillText( |
|
Number(w.value).toFixed( |
|
w.options.precision !== undefined |
|
? w.options.precision |
|
: 3 |
|
), |
|
widget_width - margin * 2 - 20, |
|
y + H * 0.7 |
|
); |
|
} else { |
|
var v = w.value; |
|
if( w.options.values ) |
|
{ |
|
var values = w.options.values; |
|
if( values.constructor === Function ) |
|
values = values(); |
|
if(values && values.constructor !== Array) |
|
v = values[ w.value ]; |
|
} |
|
ctx.fillText( |
|
v, |
|
widget_width - margin * 2 - 20, |
|
y + H * 0.7 |
|
); |
|
} |
|
} |
|
break; |
|
case "string": |
|
case "text": |
|
ctx.textAlign = "left"; |
|
ctx.strokeStyle = outline_color; |
|
ctx.fillStyle = background_color; |
|
ctx.beginPath(); |
|
if (show_text) |
|
ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]); |
|
else |
|
ctx.rect( margin, y, widget_width - margin * 2, H ); |
|
ctx.fill(); |
|
if (show_text) { |
|
if(!w.disabled) |
|
ctx.stroke(); |
|
ctx.save(); |
|
ctx.beginPath(); |
|
ctx.rect(margin, y, widget_width - margin * 2, H); |
|
ctx.clip(); |
|
|
|
//ctx.stroke(); |
|
ctx.fillStyle = secondary_text_color; |
|
if (w.name != null) { |
|
ctx.fillText(w.name, margin * 2, y + H * 0.7); |
|
} |
|
ctx.fillStyle = text_color; |
|
ctx.textAlign = "right"; |
|
ctx.fillText(String(w.value).substr(0,30), widget_width - margin * 2, y + H * 0.7); //30 chars max |
|
ctx.restore(); |
|
} |
|
break; |
|
default: |
|
if (w.draw) { |
|
w.draw(ctx, node, widget_width, y, H); |
|
} |
|
break; |
|
} |
|
posY += (w.computeSize ? w.computeSize(widget_width)[1] : H) + 4; |
|
ctx.globalAlpha = this.editor_alpha; |
|
|
|
} |
|
ctx.restore(); |
|
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; |
|
var ref_window = this.getCanvasWindow(); |
|
|
|
for (var i = 0; i < node.widgets.length; ++i) { |
|
var w = node.widgets[i]; |
|
if(!w || w.disabled) |
|
continue; |
|
var widget_height = w.computeSize ? w.computeSize(width)[1] : LiteGraph.NODE_WIDGET_HEIGHT; |
|
var widget_width = w.width || width; |
|
//outside |
|
if ( w != active_widget && |
|
(x < 6 || x > widget_width - 12 || y < w.last_y || y > w.last_y + widget_height || w.last_y === undefined) ) |
|
continue; |
|
|
|
var old_value = w.value; |
|
|
|
//if ( w == active_widget || (x > 6 && x < widget_width - 12 && y > w.last_y && y < w.last_y + widget_height) ) { |
|
//inside widget |
|
switch (w.type) { |
|
case "button": |
|
if (event.type === LiteGraph.pointerevents_method+"down") { |
|
if (w.callback) { |
|
setTimeout(function() { |
|
w.callback(w, that, node, pos, event); |
|
}, 20); |
|
} |
|
w.clicked = true; |
|
this.dirty_canvas = true; |
|
} |
|
break; |
|
case "slider": |
|
var range = w.options.max - w.options.min; |
|
var nvalue = Math.clamp((x - 15) / (widget_width - 30), 0, 1); |
|
w.value = w.options.min + (w.options.max - w.options.min) * nvalue; |
|
if (w.callback) { |
|
setTimeout(function() { |
|
inner_value_change(w, w.value); |
|
}, 20); |
|
} |
|
this.dirty_canvas = true; |
|
break; |
|
case "number": |
|
case "combo": |
|
var old_value = w.value; |
|
if (event.type == LiteGraph.pointerevents_method+"move" && 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 == LiteGraph.pointerevents_method+"down") { |
|
var values = w.options.values; |
|
if (values && values.constructor === Function) { |
|
values = w.options.values(w, node); |
|
} |
|
var values_list = null; |
|
|
|
if( w.type != "number") |
|
values_list = values.constructor === Array ? values : Object.keys(values); |
|
|
|
var delta = x < 40 ? -1 : x > widget_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) { //clicked in arrow, used for combos |
|
var index = -1; |
|
this.last_mouseclick = 0; //avoids dobl click event |
|
if(values.constructor === Object) |
|
index = values_list.indexOf( String( w.value ) ) + delta; |
|
else |
|
index = values_list.indexOf( w.value ) + delta; |
|
if (index >= values_list.length) { |
|
index = values_list.length - 1; |
|
} |
|
if (index < 0) { |
|
index = 0; |
|
} |
|
if( values.constructor === Array ) |
|
w.value = values[index]; |
|
else |
|
w.value = index; |
|
} else { //combo clicked |
|
var text_values = values != values_list ? Object.values(values) : values; |
|
var menu = new LiteGraph.ContextMenu(text_values, { |
|
scale: Math.max(1, this.ds.scale), |
|
event: event, |
|
className: "dark", |
|
callback: inner_clicked.bind(w) |
|
}, |
|
ref_window); |
|
function inner_clicked(v, option, event) { |
|
if(values != values_list) |
|
v = text_values.indexOf(v); |
|
this.value = v; |
|
inner_value_change(this, v); |
|
that.dirty_canvas = true; |
|
return false; |
|
} |
|
} |
|
} //end mousedown |
|
else if(event.type == LiteGraph.pointerevents_method+"up" && w.type == "number") |
|
{ |
|
var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; |
|
if (event.click_time < 200 && delta == 0) { |
|
this.prompt("Value",w.value,function(v) { |
|
this.value = Number(v); |
|
inner_value_change(this, this.value); |
|
}.bind(w), |
|
event); |
|
} |
|
} |
|
|
|
if( old_value != w.value ) |
|
setTimeout( |
|
function() { |
|
inner_value_change(this, this.value); |
|
}.bind(w), |
|
20 |
|
); |
|
this.dirty_canvas = true; |
|
break; |
|
case "toggle": |
|
if (event.type == LiteGraph.pointerevents_method+"down") { |
|
w.value = !w.value; |
|
setTimeout(function() { |
|
inner_value_change(w, w.value); |
|
}, 20); |
|
} |
|
break; |
|
case "string": |
|
case "text": |
|
if (event.type == LiteGraph.pointerevents_method+"down") { |
|
this.prompt("Value",w.value,function(v) { |
|
this.value = v; |
|
inner_value_change(this, v); |
|
}.bind(w), |
|
event,w.options ? w.options.multiline : false ); |
|
} |
|
break; |
|
default: |
|
if (w.mouse) { |
|
this.dirty_canvas = w.mouse(event, [x, y], node); |
|
} |
|
break; |
|
} //end switch |
|
|
|
//value changed |
|
if( old_value != w.value ) |
|
{ |
|
if(node.onWidgetChanged) |
|
node.onWidgetChanged( w.name,w.value,old_value,w ); |
|
node.graph._version++; |
|
} |
|
|
|
return w; |
|
}//end for |
|
|
|
function inner_value_change(widget, value) { |
|
widget.value = value; |
|
if ( widget.options && widget.options.property && node.properties[widget.options.property] !== undefined ) { |
|
node.setProperty( widget.options.property, value ); |
|
} |
|
if (widget.callback) { |
|
widget.callback(widget.value, that, node, pos, event); |
|
} |
|
} |
|
|
|
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 * this.editor_alpha; |
|
|
|
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 * this.editor_alpha; |
|
ctx.beginPath(); |
|
ctx.rect(pos[0] + 0.5, pos[1] + 0.5, size[0], size[1]); |
|
ctx.fill(); |
|
ctx.globalAlpha = this.editor_alpha; |
|
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(); |
|
|
|
var font_size = |
|
group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE; |
|
ctx.font = font_size + "px Arial"; |
|
ctx.textAlign = "left"; |
|
ctx.fillText(group.title, pos[0] + 4, pos[1] + font_size); |
|
} |
|
|
|
ctx.restore(); |
|
}; |
|
|
|
LGraphCanvas.prototype.adjustNodesSize = function() { |
|
var nodes = this.graph._nodes; |
|
for (var i = 0; i < nodes.length; ++i) { |
|
nodes[i].size = nodes[i].computeSize(); |
|
} |
|
this.setDirty(true, true); |
|
}; |
|
|
|
/** |
|
* 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 |
|
}; |
|
|
|
/* this is an implementation for touch not in production and not ready |
|
*/ |
|
/*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); |
|
|
|
// this is eventually a Dom object, get the LGraphCanvas back |
|
if(typeof this.getCanvasWindow == "undefined"){ |
|
var window = this.lgraphcanvas.getCanvasWindow(); |
|
}else{ |
|
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.convertEventToCanvasOffset(mouse_event); |
|
canvas.graph.add(group); |
|
}; |
|
|
|
LGraphCanvas.onMenuAdd = function (node, options, e, prev_menu, callback) { |
|
|
|
var canvas = LGraphCanvas.active_canvas; |
|
var ref_window = canvas.getCanvasWindow(); |
|
var graph = canvas.graph; |
|
if (!graph) |
|
return; |
|
|
|
function inner_onMenuAdded(base_category ,prev_menu){ |
|
|
|
var categories = LiteGraph.getNodeTypesCategories(canvas.filter || graph.filter).filter(function(category){return category.startsWith(base_category)}); |
|
var entries = []; |
|
|
|
categories.map(function(category){ |
|
|
|
if (!category) |
|
return; |
|
|
|
var base_category_regex = new RegExp('^(' + base_category + ')'); |
|
var category_name = category.replace(base_category_regex,"").split('/')[0]; |
|
var category_path = base_category === '' ? category_name + '/' : base_category + category_name + '/'; |
|
|
|
var name = category_name; |
|
if(name.indexOf("::") != -1) //in case it has a namespace like "shader::math/rand" it hides the namespace |
|
name = name.split("::")[1]; |
|
|
|
var index = entries.findIndex(function(entry){return entry.value === category_path}); |
|
if (index === -1) { |
|
entries.push({ value: category_path, content: name, has_submenu: true, callback : function(value, event, mouseEvent, contextMenu){ |
|
inner_onMenuAdded(value.value, contextMenu) |
|
}}); |
|
} |
|
|
|
}); |
|
|
|
var nodes = LiteGraph.getNodeTypesInCategory(base_category.slice(0, -1), canvas.filter || graph.filter ); |
|
nodes.map(function(node){ |
|
|
|
if (node.skip_list) |
|
return; |
|
|
|
var entry = { value: node.type, content: node.title, has_submenu: false , callback : function(value, event, mouseEvent, contextMenu){ |
|
|
|
var first_event = contextMenu.getFirstEvent(); |
|
canvas.graph.beforeChange(); |
|
var node = LiteGraph.createNode(value.value); |
|
if (node) { |
|
node.pos = canvas.convertEventToCanvasOffset(first_event); |
|
canvas.graph.add(node); |
|
} |
|
if(callback) |
|
callback(node); |
|
canvas.graph.afterChange(); |
|
|
|
} |
|
} |
|
|
|
entries.push(entry); |
|
|
|
}); |
|
|
|
new LiteGraph.ContextMenu( entries, { event: e, parentMenu: prev_menu }, ref_window ); |
|
|
|
} |
|
|
|
inner_onMenuAdded('',prev_menu); |
|
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=0; i < options.length; i++) { |
|
var entry = options[i]; |
|
if (!entry) { |
|
entries.push(null); |
|
continue; |
|
} |
|
var label = entry[0]; |
|
if(!entry[2]) |
|
entry[2] = {}; |
|
|
|
if (entry[2].label) { |
|
label = entry[2].label; |
|
} |
|
|
|
entry[2].removable = true; |
|
var data = { content: label, value: entry }; |
|
if (entry[1] == LiteGraph.ACTION) { |
|
data.className = "event"; |
|
} |
|
entries.push(data); |
|
} |
|
} |
|
|
|
if (node.onMenuNodeInputs) { |
|
var retEntries = node.onMenuNodeInputs(entries); |
|
if(retEntries) entries = retEntries; |
|
} |
|
|
|
if (!entries.length) { |
|
console.log("no input entries"); |
|
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.graph.beforeChange(); |
|
node.addInput(v.value[0], v.value[1], v.value[2]); |
|
|
|
if (node.onNodeInputAdd) { // callback to the node when adding a slot |
|
node.onNodeInputAdd(v.value); |
|
} |
|
node.setDirtyCanvas(true, true); |
|
node.graph.afterChange(); |
|
} |
|
} |
|
|
|
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=0; i < options.length; i++) { |
|
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] = {}; |
|
if (entry[2].label) { |
|
label = entry[2].label; |
|
} |
|
entry[2].removable = true; |
|
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 (LiteGraph.do_add_triggers_slots){ //canvas.allow_addOutSlot_onExecuted |
|
if (node.findOutputSlot("onExecuted") == -1){ |
|
entries.push({content: "On Executed", value: ["onExecuted", LiteGraph.EVENT, {nameLocked: true}], className: "event"}); //, opts: {} |
|
} |
|
} |
|
// add callback for modifing the menu elements onMenuNodeOutputs |
|
if (node.onMenuNodeOutputs) { |
|
var retEntries = node.onMenuNodeOutputs(entries); |
|
if(retEntries) entries = retEntries; |
|
} |
|
|
|
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.graph.beforeChange(); |
|
node.addOutput(v.value[0], v.value[1], v.value[2]); |
|
|
|
if (node.onNodeOutputAdd) { // a callback to the node when adding a slot |
|
node.onNodeOutputAdd(v.value); |
|
} |
|
node.setDirtyCanvas(true, true); |
|
node.graph.afterChange(); |
|
} |
|
} |
|
|
|
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] : " "; |
|
if( typeof value == "object" ) |
|
value = JSON.stringify(value); |
|
var info = node.getPropertyInfo(i); |
|
if(info.type == "enum" || info.type == "combo") |
|
value = LGraphCanvas.getPropertyPrintableValue( value, info.values ); |
|
|
|
//value could contain invalid html characters, clean that |
|
value = LGraphCanvas.decodeHTML(value); |
|
entries.push({ |
|
content: |
|
"<span class='property_name'>" + |
|
(info.label ? info.label : 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.onMenuResizeNode = function(value, options, e, menu, node) { |
|
if (!node) { |
|
return; |
|
} |
|
|
|
var fApplyMultiNode = function(node){ |
|
node.size = node.computeSize(); |
|
if (node.onResize) |
|
node.onResize(node.size); |
|
} |
|
|
|
var graphcanvas = LGraphCanvas.active_canvas; |
|
if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ |
|
fApplyMultiNode(node); |
|
}else{ |
|
for (var i in graphcanvas.selected_nodes) { |
|
fApplyMultiNode(graphcanvas.selected_nodes[i]); |
|
} |
|
} |
|
|
|
node.setDirtyCanvas(true, true); |
|
}; |
|
|
|
LGraphCanvas.prototype.showLinkMenu = function(link, e) { |
|
var that = this; |
|
// console.log(link); |
|
var node_left = that.graph.getNodeById( link.origin_id ); |
|
var node_right = that.graph.getNodeById( link.target_id ); |
|
var fromType = false; |
|
if (node_left && node_left.outputs && node_left.outputs[link.origin_slot]) fromType = node_left.outputs[link.origin_slot].type; |
|
var destType = false; |
|
if (node_right && node_right.outputs && node_right.outputs[link.target_slot]) destType = node_right.inputs[link.target_slot].type; |
|
|
|
var options = ["Add Node",null,"Delete",null]; |
|
|
|
|
|
var menu = new LiteGraph.ContextMenu(options, { |
|
event: e, |
|
title: link.data != null ? link.data.constructor.name : null, |
|
callback: inner_clicked |
|
}); |
|
|
|
function inner_clicked(v,options,e) { |
|
switch (v) { |
|
case "Add Node": |
|
LGraphCanvas.onMenuAdd(null, null, e, menu, function(node){ |
|
// console.debug("node autoconnect"); |
|
if(!node.inputs || !node.inputs.length || !node.outputs || !node.outputs.length){ |
|
return; |
|
} |
|
// leave the connection type checking inside connectByType |
|
if (node_left.connectByType( link.origin_slot, node, fromType )){ |
|
node.connectByType( link.target_slot, node_right, destType ); |
|
node.pos[0] -= node.size[0] * 0.5; |
|
} |
|
}); |
|
break; |
|
|
|
case "Delete": |
|
that.graph.removeLink(link.id); |
|
break; |
|
default: |
|
/*var nodeCreated = createDefaultNodeForSlot({ nodeFrom: node_left |
|
,slotFrom: link.origin_slot |
|
,nodeTo: node |
|
,slotTo: link.target_slot |
|
,e: e |
|
,nodeType: "AUTO" |
|
}); |
|
if(nodeCreated) console.log("new node in beetween "+v+" created");*/ |
|
} |
|
} |
|
|
|
return false; |
|
}; |
|
|
|
LGraphCanvas.prototype.createDefaultNodeForSlot = function(optPass) { // addNodeMenu for connection |
|
var optPass = optPass || {}; |
|
var opts = Object.assign({ nodeFrom: null // input |
|
,slotFrom: null // input |
|
,nodeTo: null // output |
|
,slotTo: null // output |
|
,position: [] // pass the event coords |
|
,nodeType: null // choose a nodetype to add, AUTO to set at first good |
|
,posAdd:[0,0] // adjust x,y |
|
,posSizeFix:[0,0] // alpha, adjust the position x,y based on the new node size w,h |
|
} |
|
,optPass |
|
); |
|
var that = this; |
|
|
|
var isFrom = opts.nodeFrom && opts.slotFrom!==null; |
|
var isTo = !isFrom && opts.nodeTo && opts.slotTo!==null; |
|
|
|
if (!isFrom && !isTo){ |
|
console.warn("No data passed to createDefaultNodeForSlot "+opts.nodeFrom+" "+opts.slotFrom+" "+opts.nodeTo+" "+opts.slotTo); |
|
return false; |
|
} |
|
if (!opts.nodeType){ |
|
console.warn("No type to createDefaultNodeForSlot"); |
|
return false; |
|
} |
|
|
|
var nodeX = isFrom ? opts.nodeFrom : opts.nodeTo; |
|
var slotX = isFrom ? opts.slotFrom : opts.slotTo; |
|
|
|
var iSlotConn = false; |
|
switch (typeof slotX){ |
|
case "string": |
|
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false); |
|
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; |
|
break; |
|
case "object": |
|
// ok slotX |
|
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name); |
|
break; |
|
case "number": |
|
iSlotConn = slotX; |
|
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; |
|
break; |
|
case "undefined": |
|
default: |
|
// bad ? |
|
//iSlotConn = 0; |
|
console.warn("Cant get slot information "+slotX); |
|
return false; |
|
} |
|
|
|
if (slotX===false || iSlotConn===false){ |
|
console.warn("createDefaultNodeForSlot bad slotX "+slotX+" "+iSlotConn); |
|
} |
|
|
|
// check for defaults nodes for this slottype |
|
var fromSlotType = slotX.type==LiteGraph.EVENT?"_event_":slotX.type; |
|
var slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in; |
|
if(slotTypesDefault && slotTypesDefault[fromSlotType]){ |
|
if (slotX.link !== null) { |
|
// is connected |
|
}else{ |
|
// is not not connected |
|
} |
|
nodeNewType = false; |
|
if(typeof slotTypesDefault[fromSlotType] == "object" || typeof slotTypesDefault[fromSlotType] == "array"){ |
|
for(var typeX in slotTypesDefault[fromSlotType]){ |
|
if (opts.nodeType == slotTypesDefault[fromSlotType][typeX] || opts.nodeType == "AUTO"){ |
|
nodeNewType = slotTypesDefault[fromSlotType][typeX]; |
|
// console.log("opts.nodeType == slotTypesDefault[fromSlotType][typeX] :: "+opts.nodeType); |
|
break; // -------- |
|
} |
|
} |
|
}else{ |
|
if (opts.nodeType == slotTypesDefault[fromSlotType] || opts.nodeType == "AUTO") nodeNewType = slotTypesDefault[fromSlotType]; |
|
} |
|
if (nodeNewType) { |
|
var nodeNewOpts = false; |
|
if (typeof nodeNewType == "object" && nodeNewType.node){ |
|
nodeNewOpts = nodeNewType; |
|
nodeNewType = nodeNewType.node; |
|
} |
|
|
|
//that.graph.beforeChange(); |
|
|
|
var newNode = LiteGraph.createNode(nodeNewType); |
|
if(newNode){ |
|
// if is object pass options |
|
if (nodeNewOpts){ |
|
if (nodeNewOpts.properties) { |
|
for (var i in nodeNewOpts.properties) { |
|
newNode.addProperty( i, nodeNewOpts.properties[i] ); |
|
} |
|
} |
|
if (nodeNewOpts.inputs) { |
|
newNode.inputs = []; |
|
for (var i in nodeNewOpts.inputs) { |
|
newNode.addOutput( |
|
nodeNewOpts.inputs[i][0], |
|
nodeNewOpts.inputs[i][1] |
|
); |
|
} |
|
} |
|
if (nodeNewOpts.outputs) { |
|
newNode.outputs = []; |
|
for (var i in nodeNewOpts.outputs) { |
|
newNode.addOutput( |
|
nodeNewOpts.outputs[i][0], |
|
nodeNewOpts.outputs[i][1] |
|
); |
|
} |
|
} |
|
if (nodeNewOpts.title) { |
|
newNode.title = nodeNewOpts.title; |
|
} |
|
if (nodeNewOpts.json) { |
|
newNode.configure(nodeNewOpts.json); |
|
} |
|
|
|
} |
|
|
|
// add the node |
|
that.graph.add(newNode); |
|
newNode.pos = [ opts.position[0]+opts.posAdd[0]+(opts.posSizeFix[0]?opts.posSizeFix[0]*newNode.size[0]:0) |
|
,opts.position[1]+opts.posAdd[1]+(opts.posSizeFix[1]?opts.posSizeFix[1]*newNode.size[1]:0)]; //that.last_click_position; //[e.canvasX+30, e.canvasX+5];*/ |
|
|
|
//that.graph.afterChange(); |
|
|
|
// connect the two! |
|
if (isFrom){ |
|
opts.nodeFrom.connectByType( iSlotConn, newNode, fromSlotType ); |
|
}else{ |
|
opts.nodeTo.connectByTypeOutput( iSlotConn, newNode, fromSlotType ); |
|
} |
|
|
|
// if connecting in between |
|
if (isFrom && isTo){ |
|
// TODO |
|
} |
|
|
|
return true; |
|
|
|
}else{ |
|
console.log("failed creating "+nodeNewType); |
|
} |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
LGraphCanvas.prototype.showConnectionMenu = function(optPass) { // addNodeMenu for connection |
|
var optPass = optPass || {}; |
|
var opts = Object.assign({ nodeFrom: null // input |
|
,slotFrom: null // input |
|
,nodeTo: null // output |
|
,slotTo: null // output |
|
,e: null |
|
} |
|
,optPass |
|
); |
|
var that = this; |
|
|
|
var isFrom = opts.nodeFrom && opts.slotFrom; |
|
var isTo = !isFrom && opts.nodeTo && opts.slotTo; |
|
|
|
if (!isFrom && !isTo){ |
|
console.warn("No data passed to showConnectionMenu"); |
|
return false; |
|
} |
|
|
|
var nodeX = isFrom ? opts.nodeFrom : opts.nodeTo; |
|
var slotX = isFrom ? opts.slotFrom : opts.slotTo; |
|
|
|
var iSlotConn = false; |
|
switch (typeof slotX){ |
|
case "string": |
|
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false); |
|
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; |
|
break; |
|
case "object": |
|
// ok slotX |
|
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name); |
|
break; |
|
case "number": |
|
iSlotConn = slotX; |
|
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; |
|
break; |
|
default: |
|
// bad ? |
|
//iSlotConn = 0; |
|
console.warn("Cant get slot information "+slotX); |
|
return false; |
|
} |
|
|
|
var options = ["Add Node",null]; |
|
|
|
if (that.allow_searchbox){ |
|
options.push("Search"); |
|
options.push(null); |
|
} |
|
|
|
// get defaults nodes for this slottype |
|
var fromSlotType = slotX.type==LiteGraph.EVENT?"_event_":slotX.type; |
|
var slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in; |
|
if(slotTypesDefault && slotTypesDefault[fromSlotType]){ |
|
if(typeof slotTypesDefault[fromSlotType] == "object" || typeof slotTypesDefault[fromSlotType] == "array"){ |
|
for(var typeX in slotTypesDefault[fromSlotType]){ |
|
options.push(slotTypesDefault[fromSlotType][typeX]); |
|
} |
|
}else{ |
|
options.push(slotTypesDefault[fromSlotType]); |
|
} |
|
} |
|
|
|
// build menu |
|
var menu = new LiteGraph.ContextMenu(options, { |
|
event: opts.e, |
|
title: (slotX && slotX.name!="" ? (slotX.name + (fromSlotType?" | ":"")) : "")+(slotX && fromSlotType ? fromSlotType : ""), |
|
callback: inner_clicked |
|
}); |
|
|
|
// callback |
|
function inner_clicked(v,options,e) { |
|
//console.log("Process showConnectionMenu selection"); |
|
switch (v) { |
|
case "Add Node": |
|
LGraphCanvas.onMenuAdd(null, null, e, menu, function(node){ |
|
if (isFrom){ |
|
opts.nodeFrom.connectByType( iSlotConn, node, fromSlotType ); |
|
}else{ |
|
opts.nodeTo.connectByTypeOutput( iSlotConn, node, fromSlotType ); |
|
} |
|
}); |
|
break; |
|
case "Search": |
|
if(isFrom){ |
|
that.showSearchBox(e,{node_from: opts.nodeFrom, slot_from: slotX, type_filter_in: fromSlotType}); |
|
}else{ |
|
that.showSearchBox(e,{node_to: opts.nodeTo, slot_from: slotX, type_filter_out: fromSlotType}); |
|
} |
|
break; |
|
default: |
|
// check for defaults nodes for this slottype |
|
var nodeCreated = that.createDefaultNodeForSlot(Object.assign(opts,{ position: [opts.e.canvasX, opts.e.canvasY] |
|
,nodeType: v |
|
})); |
|
if (nodeCreated){ |
|
// new node created |
|
//console.log("node "+v+" created") |
|
}else{ |
|
// failed or v is not in defaults |
|
} |
|
break; |
|
} |
|
} |
|
|
|
return false; |
|
}; |
|
|
|
// TODO refactor :: this is used fot title but not for properties! |
|
LGraphCanvas.onShowPropertyEditor = function(item, options, e, menu, node) { |
|
var input_html = ""; |
|
var property = item.property || "title"; |
|
var value = node[property]; |
|
|
|
// TODO refactor :: use createDialog ? |
|
|
|
var dialog = document.createElement("div"); |
|
dialog.is_modified = false; |
|
dialog.className = "graphdialog"; |
|
dialog.innerHTML = |
|
"<span class='name'></span><input autofocus type='text' class='value'/><button>OK</button>"; |
|
dialog.close = function() { |
|
if (dialog.parentNode) { |
|
dialog.parentNode.removeChild(dialog); |
|
} |
|
}; |
|
var title = dialog.querySelector(".name"); |
|
title.innerText = property; |
|
var input = dialog.querySelector(".value"); |
|
if (input) { |
|
input.value = value; |
|
input.addEventListener("blur", function(e) { |
|
this.focus(); |
|
}); |
|
input.addEventListener("keydown", function(e) { |
|
dialog.is_modified = true; |
|
if (e.keyCode == 27) { |
|
//ESC |
|
dialog.close(); |
|
} else if (e.keyCode == 13) { |
|
inner(); // save |
|
} else if (e.keyCode != 13 && e.target.localName != "textarea") { |
|
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.clientX + offsetx + "px"; |
|
dialog.style.top = event.clientY + 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); |
|
|
|
if(input) input.focus(); |
|
|
|
var dialogCloseTimer = null; |
|
dialog.addEventListener("mouseleave", function(e) { |
|
if(LiteGraph.dialog_close_on_mouse_leave) |
|
if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) |
|
dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close(); |
|
}); |
|
dialog.addEventListener("mouseenter", function(e) { |
|
if(LiteGraph.dialog_close_on_mouse_leave) |
|
if(dialogCloseTimer) clearTimeout(dialogCloseTimer); |
|
}); |
|
|
|
function inner() { |
|
if(input) setValue(input.value); |
|
} |
|
|
|
function setValue(value) { |
|
if (item.type == "Number") { |
|
value = Number(value); |
|
} else if (item.type == "Boolean") { |
|
value = Boolean(value); |
|
} |
|
node[property] = value; |
|
if (dialog.parentNode) { |
|
dialog.parentNode.removeChild(dialog); |
|
} |
|
node.setDirtyCanvas(true, true); |
|
} |
|
}; |
|
|
|
// refactor: there are different dialogs, some uses createDialog some dont |
|
LGraphCanvas.prototype.prompt = function(title, value, callback, event, multiline) { |
|
var that = this; |
|
var input_html = ""; |
|
title = title || ""; |
|
|
|
var dialog = document.createElement("div"); |
|
dialog.is_modified = false; |
|
dialog.className = "graphdialog rounded"; |
|
if(multiline) |
|
dialog.innerHTML = "<span class='name'></span> <textarea autofocus class='value'></textarea><button class='rounded'>OK</button>"; |
|
else |
|
dialog.innerHTML = "<span class='name'></span> <input autofocus type='text' class='value'/><button class='rounded'>OK</button>"; |
|
dialog.close = function() { |
|
that.prompt_box = null; |
|
if (dialog.parentNode) { |
|
dialog.parentNode.removeChild(dialog); |
|
} |
|
}; |
|
|
|
var graphcanvas = LGraphCanvas.active_canvas; |
|
var canvas = graphcanvas.canvas; |
|
canvas.parentNode.appendChild(dialog); |
|
|
|
if (this.ds.scale > 1) { |
|
dialog.style.transform = "scale(" + this.ds.scale + ")"; |
|
} |
|
|
|
var dialogCloseTimer = null; |
|
var prevent_timeout = false; |
|
LiteGraph.pointerListenerAdd(dialog,"leave", function(e) { |
|
if (prevent_timeout) |
|
return; |
|
if(LiteGraph.dialog_close_on_mouse_leave) |
|
if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) |
|
dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close(); |
|
}); |
|
LiteGraph.pointerListenerAdd(dialog,"enter", function(e) { |
|
if(LiteGraph.dialog_close_on_mouse_leave) |
|
if(dialogCloseTimer) clearTimeout(dialogCloseTimer); |
|
}); |
|
var selInDia = dialog.querySelectorAll("select"); |
|
if (selInDia){ |
|
// if filtering, check focus changed to comboboxes and prevent closing |
|
selInDia.forEach(function(selIn) { |
|
selIn.addEventListener("click", function(e) { |
|
prevent_timeout++; |
|
}); |
|
selIn.addEventListener("blur", function(e) { |
|
prevent_timeout = 0; |
|
}); |
|
selIn.addEventListener("change", function(e) { |
|
prevent_timeout = -1; |
|
}); |
|
}); |
|
} |
|
|
|
if (that.prompt_box) { |
|
that.prompt_box.close(); |
|
} |
|
that.prompt_box = dialog; |
|
|
|
var first = null; |
|
var timeout = null; |
|
var selected = null; |
|
|
|
var name_element = dialog.querySelector(".name"); |
|
name_element.innerText = title; |
|
var value_element = dialog.querySelector(".value"); |
|
value_element.value = value; |
|
|
|
var input = value_element; |
|
input.addEventListener("keydown", function(e) { |
|
dialog.is_modified = true; |
|
if (e.keyCode == 27) { |
|
//ESC |
|
dialog.close(); |
|
} else if (e.keyCode == 13 && e.target.localName != "textarea") { |
|
if (callback) { |
|
callback(this.value); |
|
} |
|
dialog.close(); |
|
} else { |
|
return; |
|
} |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
}); |
|
|
|
var button = dialog.querySelector("button"); |
|
button.addEventListener("click", function(e) { |
|
if (callback) { |
|
callback(input.value); |
|
} |
|
that.setDirty(true); |
|
dialog.close(); |
|
}); |
|
|
|
var rect = canvas.getBoundingClientRect(); |
|
var offsetx = -20; |
|
var offsety = -20; |
|
if (rect) { |
|
offsetx -= rect.left; |
|
offsety -= rect.top; |
|
} |
|
|
|
if (event) { |
|
dialog.style.left = event.clientX + offsetx + "px"; |
|
dialog.style.top = event.clientY + offsety + "px"; |
|
} else { |
|
dialog.style.left = canvas.width * 0.5 + offsetx + "px"; |
|
dialog.style.top = canvas.height * 0.5 + offsety + "px"; |
|
} |
|
|
|
setTimeout(function() { |
|
input.focus(); |
|
}, 10); |
|
|
|
return dialog; |
|
}; |
|
|
|
LGraphCanvas.search_limit = -1; |
|
LGraphCanvas.prototype.showSearchBox = function(event, options) { |
|
// proposed defaults |
|
def_options = { slot_from: null |
|
,node_from: null |
|
,node_to: null |
|
,do_type_filter: LiteGraph.search_filter_enabled // TODO check for registered_slot_[in/out]_types not empty // this will be checked for functionality enabled : filter on slot type, in and out |
|
,type_filter_in: false // these are default: pass to set initially set values |
|
,type_filter_out: false |
|
,show_general_if_none_on_typefilter: true |
|
,show_general_after_typefiltered: true |
|
,hide_on_mouse_leave: LiteGraph.search_hide_on_mouse_leave |
|
,show_all_if_empty: true |
|
,show_all_on_open: LiteGraph.search_show_all_on_open |
|
}; |
|
options = Object.assign(def_options, options || {}); |
|
|
|
//console.log(options); |
|
|
|
var that = this; |
|
var input_html = ""; |
|
var graphcanvas = LGraphCanvas.active_canvas; |
|
var canvas = graphcanvas.canvas; |
|
var root_document = canvas.ownerDocument || document; |
|
|
|
var dialog = document.createElement("div"); |
|
dialog.className = "litegraph litesearchbox graphdialog rounded"; |
|
dialog.innerHTML = "<span class='name'>Search</span> <input autofocus type='text' class='value rounded'/>"; |
|
if (options.do_type_filter){ |
|
dialog.innerHTML += "<select class='slot_in_type_filter'><option value=''></option></select>"; |
|
dialog.innerHTML += "<select class='slot_out_type_filter'><option value=''></option></select>"; |
|
} |
|
dialog.innerHTML += "<div class='helper'></div>"; |
|
|
|
if( root_document.fullscreenElement ) |
|
root_document.fullscreenElement.appendChild(dialog); |
|
else |
|
{ |
|
root_document.body.appendChild(dialog); |
|
root_document.body.style.overflow = "hidden"; |
|
} |
|
// dialog element has been appended |
|
|
|
if (options.do_type_filter){ |
|
var selIn = dialog.querySelector(".slot_in_type_filter"); |
|
var selOut = dialog.querySelector(".slot_out_type_filter"); |
|
} |
|
|
|
dialog.close = function() { |
|
that.search_box = null; |
|
this.blur(); |
|
canvas.focus(); |
|
root_document.body.style.overflow = ""; |
|
|
|
setTimeout(function() { |
|
that.canvas.focus(); |
|
}, 20); //important, if canvas loses focus keys wont be captured |
|
if (dialog.parentNode) { |
|
dialog.parentNode.removeChild(dialog); |
|
} |
|
}; |
|
|
|
if (this.ds.scale > 1) { |
|
dialog.style.transform = "scale(" + this.ds.scale + ")"; |
|
} |
|
|
|
// hide on mouse leave |
|
if(options.hide_on_mouse_leave){ |
|
var prevent_timeout = false; |
|
var timeout_close = null; |
|
LiteGraph.pointerListenerAdd(dialog,"enter", function(e) { |
|
if (timeout_close) { |
|
clearTimeout(timeout_close); |
|
timeout_close = null; |
|
} |
|
}); |
|
LiteGraph.pointerListenerAdd(dialog,"leave", function(e) { |
|
if (prevent_timeout){ |
|
return; |
|
} |
|
timeout_close = setTimeout(function() { |
|
dialog.close(); |
|
}, 500); |
|
}); |
|
// if filtering, check focus changed to comboboxes and prevent closing |
|
if (options.do_type_filter){ |
|
selIn.addEventListener("click", function(e) { |
|
prevent_timeout++; |
|
}); |
|
selIn.addEventListener("blur", function(e) { |
|
prevent_timeout = 0; |
|
}); |
|
selIn.addEventListener("change", function(e) { |
|
prevent_timeout = -1; |
|
}); |
|
selOut.addEventListener("click", function(e) { |
|
prevent_timeout++; |
|
}); |
|
selOut.addEventListener("blur", function(e) { |
|
prevent_timeout = 0; |
|
}); |
|
selOut.addEventListener("change", function(e) { |
|
prevent_timeout = -1; |
|
}); |
|
} |
|
} |
|
|
|
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("blur", function(e) { |
|
this.focus(); |
|
}); |
|
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, 250); |
|
return; |
|
} |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
e.stopImmediatePropagation(); |
|
return true; |
|
}); |
|
} |
|
|
|
// if should filter on type, load and fill selected and choose elements if passed |
|
if (options.do_type_filter){ |
|
if (selIn){ |
|
var aSlots = LiteGraph.slot_types_in; |
|
var nSlots = aSlots.length; // this for object :: Object.keys(aSlots).length; |
|
|
|
if (options.type_filter_in == LiteGraph.EVENT || options.type_filter_in == LiteGraph.ACTION) |
|
options.type_filter_in = "_event_"; |
|
/* this will filter on * .. but better do it manually in case |
|
else if(options.type_filter_in === "" || options.type_filter_in === 0) |
|
options.type_filter_in = "*";*/ |
|
|
|
for (var iK=0; iK<nSlots; iK++){ |
|
var opt = document.createElement('option'); |
|
opt.value = aSlots[iK]; |
|
opt.innerHTML = aSlots[iK]; |
|
selIn.appendChild(opt); |
|
if(options.type_filter_in !==false && (options.type_filter_in+"").toLowerCase() == (aSlots[iK]+"").toLowerCase()){ |
|
//selIn.selectedIndex .. |
|
opt.selected = true; |
|
//console.log("comparing IN "+options.type_filter_in+" :: "+aSlots[iK]); |
|
}else{ |
|
//console.log("comparing OUT "+options.type_filter_in+" :: "+aSlots[iK]); |
|
} |
|
} |
|
selIn.addEventListener("change",function(){ |
|
refreshHelper(); |
|
}); |
|
} |
|
if (selOut){ |
|
var aSlots = LiteGraph.slot_types_out; |
|
var nSlots = aSlots.length; // this for object :: Object.keys(aSlots).length; |
|
|
|
if (options.type_filter_out == LiteGraph.EVENT || options.type_filter_out == LiteGraph.ACTION) |
|
options.type_filter_out = "_event_"; |
|
/* this will filter on * .. but better do it manually in case |
|
else if(options.type_filter_out === "" || options.type_filter_out === 0) |
|
options.type_filter_out = "*";*/ |
|
|
|
for (var iK=0; iK<nSlots; iK++){ |
|
var opt = document.createElement('option'); |
|
opt.value = aSlots[iK]; |
|
opt.innerHTML = aSlots[iK]; |
|
selOut.appendChild(opt); |
|
if(options.type_filter_out !==false && (options.type_filter_out+"").toLowerCase() == (aSlots[iK]+"").toLowerCase()){ |
|
//selOut.selectedIndex .. |
|
opt.selected = true; |
|
} |
|
} |
|
selOut.addEventListener("change",function(){ |
|
refreshHelper(); |
|
}); |
|
} |
|
} |
|
|
|
//compute best position |
|
var rect = canvas.getBoundingClientRect(); |
|
|
|
var left = ( event ? event.clientX : (rect.left + rect.width * 0.5) ) - 80; |
|
var top = ( event ? event.clientY : (rect.top + rect.height * 0.5) ) - 20; |
|
dialog.style.left = left + "px"; |
|
dialog.style.top = top + "px"; |
|
|
|
//To avoid out of screen problems |
|
if(event.layerY > (rect.height - 200)) |
|
helper.style.maxHeight = (rect.height - event.layerY - 20) + "px"; |
|
|
|
/* |
|
var offsetx = -20; |
|
var offsety = -20; |
|
if (rect) { |
|
offsetx -= rect.left; |
|
offsety -= rect.top; |
|
} |
|
|
|
if (event) { |
|
dialog.style.left = event.clientX + offsetx + "px"; |
|
dialog.style.top = event.clientY + 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(); |
|
if (options.show_all_on_open) refreshHelper(); |
|
|
|
function select(name) { |
|
if (name) { |
|
if (that.onSearchBoxSelection) { |
|
that.onSearchBoxSelection(name, event, graphcanvas); |
|
} else { |
|
var extra = LiteGraph.searchbox_extras[name.toLowerCase()]; |
|
if (extra) { |
|
name = extra.type; |
|
} |
|
|
|
graphcanvas.graph.beforeChange(); |
|
var node = LiteGraph.createNode(name); |
|
if (node) { |
|
node.pos = graphcanvas.convertEventToCanvasOffset( |
|
event |
|
); |
|
graphcanvas.graph.add(node, false); |
|
} |
|
|
|
if (extra && extra.data) { |
|
if (extra.data.properties) { |
|
for (var i in extra.data.properties) { |
|
node.addProperty( i, extra.data.properties[i] ); |
|
} |
|
} |
|
if (extra.data.inputs) { |
|
node.inputs = []; |
|
for (var i in extra.data.inputs) { |
|
node.addOutput( |
|
extra.data.inputs[i][0], |
|
extra.data.inputs[i][1] |
|
); |
|
} |
|
} |
|
if (extra.data.outputs) { |
|
node.outputs = []; |
|
for (var i in extra.data.outputs) { |
|
node.addOutput( |
|
extra.data.outputs[i][0], |
|
extra.data.outputs[i][1] |
|
); |
|
} |
|
} |
|
if (extra.data.title) { |
|
node.title = extra.data.title; |
|
} |
|
if (extra.data.json) { |
|
node.configure(extra.data.json); |
|
} |
|
|
|
} |
|
|
|
// join node after inserting |
|
if (options.node_from){ |
|
var iS = false; |
|
switch (typeof options.slot_from){ |
|
case "string": |
|
iS = options.node_from.findOutputSlot(options.slot_from); |
|
break; |
|
case "object": |
|
if (options.slot_from.name){ |
|
iS = options.node_from.findOutputSlot(options.slot_from.name); |
|
}else{ |
|
iS = -1; |
|
} |
|
if (iS==-1 && typeof options.slot_from.slot_index !== "undefined") iS = options.slot_from.slot_index; |
|
break; |
|
case "number": |
|
iS = options.slot_from; |
|
break; |
|
default: |
|
iS = 0; // try with first if no name set |
|
} |
|
if (typeof options.node_from.outputs[iS] !== undefined){ |
|
if (iS!==false && iS>-1){ |
|
options.node_from.connectByType( iS, node, options.node_from.outputs[iS].type ); |
|
} |
|
}else{ |
|
// console.warn("cant find slot " + options.slot_from); |
|
} |
|
} |
|
if (options.node_to){ |
|
var iS = false; |
|
switch (typeof options.slot_from){ |
|
case "string": |
|
iS = options.node_to.findInputSlot(options.slot_from); |
|
break; |
|
case "object": |
|
if (options.slot_from.name){ |
|
iS = options.node_to.findInputSlot(options.slot_from.name); |
|
}else{ |
|
iS = -1; |
|
} |
|
if (iS==-1 && typeof options.slot_from.slot_index !== "undefined") iS = options.slot_from.slot_index; |
|
break; |
|
case "number": |
|
iS = options.slot_from; |
|
break; |
|
default: |
|
iS = 0; // try with first if no name set |
|
} |
|
if (typeof options.node_to.inputs[iS] !== undefined){ |
|
if (iS!==false && iS>-1){ |
|
// try connection |
|
options.node_to.connectByTypeOutput(iS,node,options.node_to.inputs[iS].type); |
|
} |
|
}else{ |
|
// console.warn("cant find slot_nodeTO " + options.slot_from); |
|
} |
|
} |
|
|
|
graphcanvas.graph.afterChange(); |
|
} |
|
} |
|
|
|
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({block: "end", behavior: "smooth"}); |
|
} |
|
|
|
function refreshHelper() { |
|
timeout = null; |
|
var str = input.value; |
|
first = null; |
|
helper.innerHTML = ""; |
|
if (!str && !options.show_all_if_empty) { |
|
return; |
|
} |
|
|
|
if (that.onSearchBox) { |
|
var list = that.onSearchBox(helper, str, graphcanvas); |
|
if (list) { |
|
for (var i = 0; i < list.length; ++i) { |
|
addResult(list[i]); |
|
} |
|
} |
|
} else { |
|
var c = 0; |
|
str = str.toLowerCase(); |
|
var filter = graphcanvas.filter || graphcanvas.graph.filter; |
|
|
|
// filter by type preprocess |
|
if(options.do_type_filter && that.search_box){ |
|
var sIn = that.search_box.querySelector(".slot_in_type_filter"); |
|
var sOut = that.search_box.querySelector(".slot_out_type_filter"); |
|
}else{ |
|
var sIn = false; |
|
var sOut = false; |
|
} |
|
|
|
//extras |
|
for (var i in LiteGraph.searchbox_extras) { |
|
var extra = LiteGraph.searchbox_extras[i]; |
|
if ((!options.show_all_if_empty || str) && extra.desc.toLowerCase().indexOf(str) === -1) { |
|
continue; |
|
} |
|
var ctor = LiteGraph.registered_node_types[ extra.type ]; |
|
if( ctor && ctor.filter != filter ) |
|
continue; |
|
if( ! inner_test_filter(extra.type) ) |
|
continue; |
|
addResult( extra.desc, "searchbox_extra" ); |
|
if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { |
|
break; |
|
} |
|
} |
|
|
|
var filtered = null; |
|
if (Array.prototype.filter) { //filter supported |
|
var keys = Object.keys( LiteGraph.registered_node_types ); //types |
|
var filtered = keys.filter( inner_test_filter ); |
|
} else { |
|
filtered = []; |
|
for (var i in LiteGraph.registered_node_types) { |
|
if( inner_test_filter(i) ) |
|
filtered.push(i); |
|
} |
|
} |
|
|
|
for (var i = 0; i < filtered.length; i++) { |
|
addResult(filtered[i]); |
|
if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { |
|
break; |
|
} |
|
} |
|
|
|
// add general type if filtering |
|
if (options.show_general_after_typefiltered |
|
&& (sIn.value || sOut.value) |
|
){ |
|
filtered_extra = []; |
|
for (var i in LiteGraph.registered_node_types) { |
|
if( inner_test_filter(i, {inTypeOverride: sIn&&sIn.value?"*":false, outTypeOverride: sOut&&sOut.value?"*":false}) ) |
|
filtered_extra.push(i); |
|
} |
|
for (var i = 0; i < filtered_extra.length; i++) { |
|
addResult(filtered_extra[i], "generic_type"); |
|
if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { |
|
break; |
|
} |
|
} |
|
} |
|
|
|
// check il filtering gave no results |
|
if ((sIn.value || sOut.value) && |
|
( (helper.childNodes.length == 0 && options.show_general_if_none_on_typefilter) ) |
|
){ |
|
filtered_extra = []; |
|
for (var i in LiteGraph.registered_node_types) { |
|
if( inner_test_filter(i, {skipFilter: true}) ) |
|
filtered_extra.push(i); |
|
} |
|
for (var i = 0; i < filtered_extra.length; i++) { |
|
addResult(filtered_extra[i], "not_in_filter"); |
|
if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { |
|
break; |
|
} |
|
} |
|
} |
|
|
|
function inner_test_filter( type, optsIn ) |
|
{ |
|
var optsIn = optsIn || {}; |
|
var optsDef = { skipFilter: false |
|
,inTypeOverride: false |
|
,outTypeOverride: false |
|
}; |
|
var opts = Object.assign(optsDef,optsIn); |
|
var ctor = LiteGraph.registered_node_types[ type ]; |
|
if(filter && ctor.filter != filter ) |
|
return false; |
|
if ((!options.show_all_if_empty || str) && type.toLowerCase().indexOf(str) === -1) |
|
return false; |
|
|
|
// filter by slot IN, OUT types |
|
if(options.do_type_filter && !opts.skipFilter){ |
|
var sType = type; |
|
|
|
var sV = sIn.value; |
|
if (opts.inTypeOverride!==false) sV = opts.inTypeOverride; |
|
//if (sV.toLowerCase() == "_event_") sV = LiteGraph.EVENT; // -1 |
|
|
|
if(sIn && sV){ |
|
//console.log("will check filter against "+sV); |
|
if (LiteGraph.registered_slot_in_types[sV] && LiteGraph.registered_slot_in_types[sV].nodes){ // type is stored |
|
//console.debug("check "+sType+" in "+LiteGraph.registered_slot_in_types[sV].nodes); |
|
var doesInc = LiteGraph.registered_slot_in_types[sV].nodes.includes(sType); |
|
if (doesInc!==false){ |
|
//console.log(sType+" HAS "+sV); |
|
}else{ |
|
/*console.debug(LiteGraph.registered_slot_in_types[sV]); |
|
console.log(+" DONT includes "+type);*/ |
|
return false; |
|
} |
|
} |
|
} |
|
|
|
var sV = sOut.value; |
|
if (opts.outTypeOverride!==false) sV = opts.outTypeOverride; |
|
//if (sV.toLowerCase() == "_event_") sV = LiteGraph.EVENT; // -1 |
|
|
|
if(sOut && sV){ |
|
//console.log("search will check filter against "+sV); |
|
if (LiteGraph.registered_slot_out_types[sV] && LiteGraph.registered_slot_out_types[sV].nodes){ // type is stored |
|
//console.debug("check "+sType+" in "+LiteGraph.registered_slot_out_types[sV].nodes); |
|
var doesInc = LiteGraph.registered_slot_out_types[sV].nodes.includes(sType); |
|
if (doesInc!==false){ |
|
//console.log(sType+" HAS "+sV); |
|
}else{ |
|
/*console.debug(LiteGraph.registered_slot_out_types[sV]); |
|
console.log(+" DONT includes "+type);*/ |
|
return false; |
|
} |
|
} |
|
} |
|
} |
|
return true; |
|
} |
|
} |
|
|
|
function addResult(type, className) { |
|
var help = document.createElement("div"); |
|
if (!first) { |
|
first = type; |
|
} |
|
help.innerText = type; |
|
help.dataset["type"] = escape(type); |
|
help.className = "litegraph lite-search-item"; |
|
if (className) { |
|
help.className += " " + className; |
|
} |
|
help.addEventListener("click", function(e) { |
|
select(unescape(this.dataset["type"])); |
|
}); |
|
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 info = node.getPropertyInfo(property); |
|
var type = info.type; |
|
|
|
var input_html = ""; |
|
|
|
if (type == "string" || type == "number" || type == "array" || type == "object") { |
|
input_html = "<input autofocus type='text' class='value'/>"; |
|
} else if ( (type == "enum" || type == "combo") && info.values) { |
|
input_html = "<select autofocus type='text' class='value'>"; |
|
for (var i in info.values) { |
|
var v = i; |
|
if( info.values.constructor === Array ) |
|
v = info.values[i]; |
|
|
|
input_html += |
|
"<option value='" + |
|
v + |
|
"' " + |
|
(v == node.properties[property] ? "selected" : "") + |
|
">" + |
|
info.values[i] + |
|
"</option>"; |
|
} |
|
input_html += "</select>"; |
|
} else if (type == "boolean" || type == "toggle") { |
|
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'>" + |
|
(info.label ? info.label : property) + |
|
"</span>" + |
|
input_html + |
|
"<button>OK</button>", |
|
options |
|
); |
|
|
|
var input = false; |
|
if ((type == "enum" || type == "combo") && info.values) { |
|
input = dialog.querySelector("select"); |
|
input.addEventListener("change", function(e) { |
|
dialog.modified(); |
|
setValue(e.target.value); |
|
//var index = e.target.value; |
|
//setValue( e.options[e.selectedIndex].value ); |
|
}); |
|
} else if (type == "boolean" || type == "toggle") { |
|
input = dialog.querySelector("input"); |
|
if (input) { |
|
input.addEventListener("click", function(e) { |
|
dialog.modified(); |
|
setValue(!!input.checked); |
|
}); |
|
} |
|
} else { |
|
input = dialog.querySelector("input"); |
|
if (input) { |
|
input.addEventListener("blur", function(e) { |
|
this.focus(); |
|
}); |
|
|
|
var v = node.properties[property] !== undefined ? node.properties[property] : ""; |
|
if (type !== 'string') { |
|
v = JSON.stringify(v); |
|
} |
|
|
|
input.value = v; |
|
input.addEventListener("keydown", function(e) { |
|
if (e.keyCode == 27) { |
|
//ESC |
|
dialog.close(); |
|
} else if (e.keyCode == 13) { |
|
// ENTER |
|
inner(); // save |
|
} else if (e.keyCode != 13) { |
|
dialog.modified(); |
|
return; |
|
} |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
}); |
|
} |
|
} |
|
if (input) input.focus(); |
|
|
|
var button = dialog.querySelector("button"); |
|
button.addEventListener("click", inner); |
|
|
|
function inner() { |
|
setValue(input.value); |
|
} |
|
|
|
function setValue(value) { |
|
|
|
if(info && info.values && info.values.constructor === Object && info.values[value] != undefined ) |
|
value = info.values[value]; |
|
|
|
if (typeof node.properties[property] == "number") { |
|
value = Number(value); |
|
} |
|
if (type == "array" || type == "object") { |
|
value = JSON.parse(value); |
|
} |
|
node.properties[property] = value; |
|
if (node.graph) { |
|
node.graph._version++; |
|
} |
|
if (node.onPropertyChanged) { |
|
node.onPropertyChanged(property, value); |
|
} |
|
if(options.onclose) |
|
options.onclose(); |
|
dialog.close(); |
|
node.setDirtyCanvas(true, true); |
|
} |
|
|
|
return dialog; |
|
}; |
|
|
|
// TODO refactor, theer are different dialog, some uses createDialog, some dont |
|
LGraphCanvas.prototype.createDialog = function(html, options) { |
|
def_options = { checkForInput: false, closeOnLeave: true, closeOnLeave_checkModified: true }; |
|
options = Object.assign(def_options, options || {}); |
|
|
|
var dialog = document.createElement("div"); |
|
dialog.className = "graphdialog"; |
|
dialog.innerHTML = html; |
|
dialog.is_modified = false; |
|
|
|
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.clientX; |
|
offsety += options.event.clientY; |
|
} //centered |
|
else { |
|
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); |
|
|
|
// acheck for input and use default behaviour: save on enter, close on esc |
|
if (options.checkForInput){ |
|
var aI = []; |
|
var focused = false; |
|
if (aI = dialog.querySelectorAll("input")){ |
|
aI.forEach(function(iX) { |
|
iX.addEventListener("keydown",function(e){ |
|
dialog.modified(); |
|
if (e.keyCode == 27) { |
|
dialog.close(); |
|
} else if (e.keyCode != 13) { |
|
return; |
|
} |
|
// set value ? |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
}); |
|
if (!focused) iX.focus(); |
|
}); |
|
} |
|
} |
|
|
|
dialog.modified = function(){ |
|
dialog.is_modified = true; |
|
} |
|
dialog.close = function() { |
|
if (dialog.parentNode) { |
|
dialog.parentNode.removeChild(dialog); |
|
} |
|
}; |
|
|
|
var dialogCloseTimer = null; |
|
var prevent_timeout = false; |
|
dialog.addEventListener("mouseleave", function(e) { |
|
if (prevent_timeout) |
|
return; |
|
if(options.closeOnLeave || LiteGraph.dialog_close_on_mouse_leave) |
|
if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) |
|
dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close(); |
|
}); |
|
dialog.addEventListener("mouseenter", function(e) { |
|
if(options.closeOnLeave || LiteGraph.dialog_close_on_mouse_leave) |
|
if(dialogCloseTimer) clearTimeout(dialogCloseTimer); |
|
}); |
|
var selInDia = dialog.querySelectorAll("select"); |
|
if (selInDia){ |
|
// if filtering, check focus changed to comboboxes and prevent closing |
|
selInDia.forEach(function(selIn) { |
|
selIn.addEventListener("click", function(e) { |
|
prevent_timeout++; |
|
}); |
|
selIn.addEventListener("blur", function(e) { |
|
prevent_timeout = 0; |
|
}); |
|
selIn.addEventListener("change", function(e) { |
|
prevent_timeout = -1; |
|
}); |
|
}); |
|
} |
|
|
|
return dialog; |
|
}; |
|
|
|
LGraphCanvas.prototype.createPanel = function(title, options) { |
|
options = options || {}; |
|
|
|
var ref_window = options.window || window; |
|
var root = document.createElement("div"); |
|
root.className = "litegraph dialog"; |
|
root.innerHTML = "<div class='dialog-header'><span class='dialog-title'></span></div><div class='dialog-content'></div><div style='display:none;' class='dialog-alt-content'></div><div class='dialog-footer'></div>"; |
|
root.header = root.querySelector(".dialog-header"); |
|
|
|
if(options.width) |
|
root.style.width = options.width + (options.width.constructor === Number ? "px" : ""); |
|
if(options.height) |
|
root.style.height = options.height + (options.height.constructor === Number ? "px" : ""); |
|
if(options.closable) |
|
{ |
|
var close = document.createElement("span"); |
|
close.innerHTML = "✕"; |
|
close.classList.add("close"); |
|
close.addEventListener("click",function(){ |
|
root.close(); |
|
}); |
|
root.header.appendChild(close); |
|
} |
|
root.title_element = root.querySelector(".dialog-title"); |
|
root.title_element.innerText = title; |
|
root.content = root.querySelector(".dialog-content"); |
|
root.alt_content = root.querySelector(".dialog-alt-content"); |
|
root.footer = root.querySelector(".dialog-footer"); |
|
|
|
root.close = function() |
|
{ |
|
if (root.onClose && typeof root.onClose == "function"){ |
|
root.onClose(); |
|
} |
|
root.parentNode.removeChild(root); |
|
/* XXX CHECK THIS */ |
|
if(this.parentNode){ |
|
this.parentNode.removeChild(this); |
|
} |
|
/* XXX this was not working, was fixed with an IF, check this */ |
|
} |
|
|
|
// function to swap panel content |
|
root.toggleAltContent = function(force){ |
|
if (typeof force != "undefined"){ |
|
var vTo = force ? "block" : "none"; |
|
var vAlt = force ? "none" : "block"; |
|
}else{ |
|
var vTo = root.alt_content.style.display != "block" ? "block" : "none"; |
|
var vAlt = root.alt_content.style.display != "block" ? "none" : "block"; |
|
} |
|
root.alt_content.style.display = vTo; |
|
root.content.style.display = vAlt; |
|
} |
|
|
|
root.toggleFooterVisibility = function(force){ |
|
if (typeof force != "undefined"){ |
|
var vTo = force ? "block" : "none"; |
|
}else{ |
|
var vTo = root.footer.style.display != "block" ? "block" : "none"; |
|
} |
|
root.footer.style.display = vTo; |
|
} |
|
|
|
root.clear = function() |
|
{ |
|
this.content.innerHTML = ""; |
|
} |
|
|
|
root.addHTML = function(code, classname, on_footer) |
|
{ |
|
var elem = document.createElement("div"); |
|
if(classname) |
|
elem.className = classname; |
|
elem.innerHTML = code; |
|
if(on_footer) |
|
root.footer.appendChild(elem); |
|
else |
|
root.content.appendChild(elem); |
|
return elem; |
|
} |
|
|
|
root.addButton = function( name, callback, options ) |
|
{ |
|
var elem = document.createElement("button"); |
|
elem.innerText = name; |
|
elem.options = options; |
|
elem.classList.add("btn"); |
|
elem.addEventListener("click",callback); |
|
root.footer.appendChild(elem); |
|
return elem; |
|
} |
|
|
|
root.addSeparator = function() |
|
{ |
|
var elem = document.createElement("div"); |
|
elem.className = "separator"; |
|
root.content.appendChild(elem); |
|
} |
|
|
|
root.addWidget = function( type, name, value, options, callback ) |
|
{ |
|
options = options || {}; |
|
var str_value = String(value); |
|
type = type.toLowerCase(); |
|
if(type == "number") |
|
str_value = value.toFixed(3); |
|
|
|
var elem = document.createElement("div"); |
|
elem.className = "property"; |
|
elem.innerHTML = "<span class='property_name'></span><span class='property_value'></span>"; |
|
elem.querySelector(".property_name").innerText = options.label || name; |
|
var value_element = elem.querySelector(".property_value"); |
|
value_element.innerText = str_value; |
|
elem.dataset["property"] = name; |
|
elem.dataset["type"] = options.type || type; |
|
elem.options = options; |
|
elem.value = value; |
|
|
|
if( type == "code" ) |
|
elem.addEventListener("click", function(e){ root.inner_showCodePad( this.dataset["property"] ); }); |
|
else if (type == "boolean") |
|
{ |
|
elem.classList.add("boolean"); |
|
if(value) |
|
elem.classList.add("bool-on"); |
|
elem.addEventListener("click", function(){ |
|
//var v = node.properties[this.dataset["property"]]; |
|
//node.setProperty(this.dataset["property"],!v); this.innerText = v ? "true" : "false"; |
|
var propname = this.dataset["property"]; |
|
this.value = !this.value; |
|
this.classList.toggle("bool-on"); |
|
this.querySelector(".property_value").innerText = this.value ? "true" : "false"; |
|
innerChange(propname, this.value ); |
|
}); |
|
} |
|
else if (type == "string" || type == "number") |
|
{ |
|
value_element.setAttribute("contenteditable",true); |
|
value_element.addEventListener("keydown", function(e){ |
|
if(e.code == "Enter" && (type != "string" || !e.shiftKey)) // allow for multiline |
|
{ |
|
e.preventDefault(); |
|
this.blur(); |
|
} |
|
}); |
|
value_element.addEventListener("blur", function(){ |
|
var v = this.innerText; |
|
var propname = this.parentNode.dataset["property"]; |
|
var proptype = this.parentNode.dataset["type"]; |
|
if( proptype == "number") |
|
v = Number(v); |
|
innerChange(propname, v); |
|
}); |
|
} |
|
else if (type == "enum" || type == "combo") { |
|
var str_value = LGraphCanvas.getPropertyPrintableValue( value, options.values ); |
|
value_element.innerText = str_value; |
|
|
|
value_element.addEventListener("click", function(event){ |
|
var values = options.values || []; |
|
var propname = this.parentNode.dataset["property"]; |
|
var elem_that = this; |
|
var menu = new LiteGraph.ContextMenu(values,{ |
|
event: event, |
|
className: "dark", |
|
callback: inner_clicked |
|
}, |
|
ref_window); |
|
function inner_clicked(v, option, event) { |
|
//node.setProperty(propname,v); |
|
//graphcanvas.dirty_canvas = true; |
|
elem_that.innerText = v; |
|
innerChange(propname,v); |
|
return false; |
|
} |
|
}); |
|
} |
|
|
|
root.content.appendChild(elem); |
|
|
|
function innerChange(name, value) |
|
{ |
|
//console.log("change",name,value); |
|
//that.dirty_canvas = true; |
|
if(options.callback) |
|
options.callback(name,value,options); |
|
if(callback) |
|
callback(name,value,options); |
|
} |
|
|
|
return elem; |
|
} |
|
|
|
if (root.onOpen && typeof root.onOpen == "function") root.onOpen(); |
|
|
|
return root; |
|
}; |
|
|
|
LGraphCanvas.getPropertyPrintableValue = function(value, values) |
|
{ |
|
if(!values) |
|
return String(value); |
|
|
|
if(values.constructor === Array) |
|
{ |
|
return String(value); |
|
} |
|
|
|
if(values.constructor === Object) |
|
{ |
|
var desc_value = ""; |
|
for(var k in values) |
|
{ |
|
if(values[k] != value) |
|
continue; |
|
desc_value = k; |
|
break; |
|
} |
|
return String(value) + " ("+desc_value+")"; |
|
} |
|
} |
|
|
|
LGraphCanvas.prototype.closePanels = function(){ |
|
var panel = document.querySelector("#node-panel"); |
|
if(panel) |
|
panel.close(); |
|
var panel = document.querySelector("#option-panel"); |
|
if(panel) |
|
panel.close(); |
|
} |
|
|
|
LGraphCanvas.prototype.showShowGraphOptionsPanel = function(refOpts, obEv, refMenu, refMenu2){ |
|
if(this.constructor && this.constructor.name == "HTMLDivElement"){ |
|
// assume coming from the menu event click |
|
if (!obEv || !obEv.event || !obEv.event.target || !obEv.event.target.lgraphcanvas){ |
|
console.warn("Canvas not found"); // need a ref to canvas obj |
|
/*console.debug(event); |
|
console.debug(event.target);*/ |
|
return; |
|
} |
|
var graphcanvas = obEv.event.target.lgraphcanvas; |
|
}else{ |
|
// assume called internally |
|
var graphcanvas = this; |
|
} |
|
graphcanvas.closePanels(); |
|
var ref_window = graphcanvas.getCanvasWindow(); |
|
panel = graphcanvas.createPanel("Options",{ |
|
closable: true |
|
,window: ref_window |
|
,onOpen: function(){ |
|
graphcanvas.OPTIONPANEL_IS_OPEN = true; |
|
} |
|
,onClose: function(){ |
|
graphcanvas.OPTIONPANEL_IS_OPEN = false; |
|
graphcanvas.options_panel = null; |
|
} |
|
}); |
|
graphcanvas.options_panel = panel; |
|
panel.id = "option-panel"; |
|
panel.classList.add("settings"); |
|
|
|
function inner_refresh(){ |
|
|
|
panel.content.innerHTML = ""; //clear |
|
|
|
var fUpdate = function(name, value, options){ |
|
switch(name){ |
|
/*case "Render mode": |
|
// Case "".. |
|
if (options.values && options.key){ |
|
var kV = Object.values(options.values).indexOf(value); |
|
if (kV>=0 && options.values[kV]){ |
|
console.debug("update graph options: "+options.key+": "+kV); |
|
graphcanvas[options.key] = kV; |
|
//console.debug(graphcanvas); |
|
break; |
|
} |
|
} |
|
console.warn("unexpected options"); |
|
console.debug(options); |
|
break;*/ |
|
default: |
|
//console.debug("want to update graph options: "+name+": "+value); |
|
if (options && options.key){ |
|
name = options.key; |
|
} |
|
if (options.values){ |
|
value = Object.values(options.values).indexOf(value); |
|
} |
|
//console.debug("update graph option: "+name+": "+value); |
|
graphcanvas[name] = value; |
|
break; |
|
} |
|
}; |
|
|
|
// panel.addWidget( "string", "Graph name", "", {}, fUpdate); // implement |
|
|
|
var aProps = LiteGraph.availableCanvasOptions; |
|
aProps.sort(); |
|
for(pI in aProps){ |
|
var pX = aProps[pI]; |
|
panel.addWidget( "boolean", pX, graphcanvas[pX], {key: pX, on: "True", off: "False"}, fUpdate); |
|
} |
|
|
|
var aLinks = [ graphcanvas.links_render_mode ]; |
|
panel.addWidget( "combo", "Render mode", LiteGraph.LINK_RENDER_MODES[graphcanvas.links_render_mode], {key: "links_render_mode", values: LiteGraph.LINK_RENDER_MODES}, fUpdate); |
|
|
|
panel.addSeparator(); |
|
|
|
panel.footer.innerHTML = ""; // clear |
|
|
|
} |
|
inner_refresh(); |
|
|
|
graphcanvas.canvas.parentNode.appendChild( panel ); |
|
} |
|
|
|
LGraphCanvas.prototype.showShowNodePanel = function( node ) |
|
{ |
|
this.SELECTED_NODE = node; |
|
this.closePanels(); |
|
var ref_window = this.getCanvasWindow(); |
|
var that = this; |
|
var graphcanvas = this; |
|
panel = this.createPanel(node.title || "",{ |
|
closable: true |
|
,window: ref_window |
|
,onOpen: function(){ |
|
graphcanvas.NODEPANEL_IS_OPEN = true; |
|
} |
|
,onClose: function(){ |
|
graphcanvas.NODEPANEL_IS_OPEN = false; |
|
graphcanvas.node_panel = null; |
|
} |
|
}); |
|
graphcanvas.node_panel = panel; |
|
panel.id = "node-panel"; |
|
panel.node = node; |
|
panel.classList.add("settings"); |
|
|
|
function inner_refresh() |
|
{ |
|
panel.content.innerHTML = ""; //clear |
|
panel.addHTML("<span class='node_type'>"+node.type+"</span><span class='node_desc'>"+(node.constructor.desc || "")+"</span><span class='separator'></span>"); |
|
|
|
panel.addHTML("<h3>Properties</h3>"); |
|
|
|
var fUpdate = function(name,value){ |
|
graphcanvas.graph.beforeChange(node); |
|
switch(name){ |
|
case "Title": |
|
node.title = value; |
|
break; |
|
case "Mode": |
|
var kV = Object.values(LiteGraph.NODE_MODES).indexOf(value); |
|
if (kV>=0 && LiteGraph.NODE_MODES[kV]){ |
|
node.changeMode(kV); |
|
}else{ |
|
console.warn("unexpected mode: "+value); |
|
} |
|
break; |
|
case "Color": |
|
if (LGraphCanvas.node_colors[value]){ |
|
node.color = LGraphCanvas.node_colors[value].color; |
|
node.bgcolor = LGraphCanvas.node_colors[value].bgcolor; |
|
}else{ |
|
console.warn("unexpected color: "+value); |
|
} |
|
break; |
|
default: |
|
node.setProperty(name,value); |
|
break; |
|
} |
|
graphcanvas.graph.afterChange(); |
|
graphcanvas.dirty_canvas = true; |
|
}; |
|
|
|
panel.addWidget( "string", "Title", node.title, {}, fUpdate); |
|
|
|
panel.addWidget( "combo", "Mode", LiteGraph.NODE_MODES[node.mode], {values: LiteGraph.NODE_MODES}, fUpdate); |
|
|
|
var nodeCol = ""; |
|
if (node.color !== undefined){ |
|
nodeCol = Object.keys(LGraphCanvas.node_colors).filter(function(nK){ return LGraphCanvas.node_colors[nK].color == node.color; }); |
|
} |
|
|
|
panel.addWidget( "combo", "Color", nodeCol, {values: Object.keys(LGraphCanvas.node_colors)}, fUpdate); |
|
|
|
for(var pName in node.properties) |
|
{ |
|
var value = node.properties[pName]; |
|
var info = node.getPropertyInfo(pName); |
|
var type = info.type || "string"; |
|
|
|
//in case the user wants control over the side panel widget |
|
if( node.onAddPropertyToPanel && node.onAddPropertyToPanel(pName,panel) ) |
|
continue; |
|
|
|
panel.addWidget( info.widget || info.type, pName, value, info, fUpdate); |
|
} |
|
|
|
panel.addSeparator(); |
|
|
|
if(node.onShowCustomPanelInfo) |
|
node.onShowCustomPanelInfo(panel); |
|
|
|
panel.footer.innerHTML = ""; // clear |
|
panel.addButton("Delete",function(){ |
|
if(node.block_delete) |
|
return; |
|
node.graph.remove(node); |
|
panel.close(); |
|
}).classList.add("delete"); |
|
} |
|
|
|
panel.inner_showCodePad = function( propname ) |
|
{ |
|
panel.classList.remove("settings"); |
|
panel.classList.add("centered"); |
|
|
|
|
|
/*if(window.CodeFlask) //disabled for now |
|
{ |
|
panel.content.innerHTML = "<div class='code'></div>"; |
|
var flask = new CodeFlask( "div.code", { language: 'js' }); |
|
flask.updateCode(node.properties[propname]); |
|
flask.onUpdate( function(code) { |
|
node.setProperty(propname, code); |
|
}); |
|
} |
|
else |
|
{*/ |
|
panel.alt_content.innerHTML = "<textarea class='code'></textarea>"; |
|
var textarea = panel.alt_content.querySelector("textarea"); |
|
var fDoneWith = function(){ |
|
panel.toggleAltContent(false); //if(node_prop_div) node_prop_div.style.display = "block"; // panel.close(); |
|
panel.toggleFooterVisibility(true); |
|
textarea.parentNode.removeChild(textarea); |
|
panel.classList.add("settings"); |
|
panel.classList.remove("centered"); |
|
inner_refresh(); |
|
} |
|
textarea.value = node.properties[propname]; |
|
textarea.addEventListener("keydown", function(e){ |
|
if(e.code == "Enter" && e.ctrlKey ) |
|
{ |
|
node.setProperty(propname, textarea.value); |
|
fDoneWith(); |
|
} |
|
}); |
|
panel.toggleAltContent(true); |
|
panel.toggleFooterVisibility(false); |
|
textarea.style.height = "calc(100% - 40px)"; |
|
/*}*/ |
|
var assign = panel.addButton( "Assign", function(){ |
|
node.setProperty(propname, textarea.value); |
|
fDoneWith(); |
|
}); |
|
panel.alt_content.appendChild(assign); //panel.content.appendChild(assign); |
|
var button = panel.addButton( "Close", fDoneWith); |
|
button.style.float = "right"; |
|
panel.alt_content.appendChild(button); // panel.content.appendChild(button); |
|
} |
|
|
|
inner_refresh(); |
|
|
|
this.canvas.parentNode.appendChild( panel ); |
|
} |
|
|
|
LGraphCanvas.prototype.showSubgraphPropertiesDialog = function(node) |
|
{ |
|
console.log("showing subgraph properties dialog"); |
|
|
|
var old_panel = this.canvas.parentNode.querySelector(".subgraph_dialog"); |
|
if(old_panel) |
|
old_panel.close(); |
|
|
|
var panel = this.createPanel("Subgraph Inputs",{closable:true, width: 500}); |
|
panel.node = node; |
|
panel.classList.add("subgraph_dialog"); |
|
|
|
function inner_refresh() |
|
{ |
|
panel.clear(); |
|
|
|
//show currents |
|
if(node.inputs) |
|
for(var i = 0; i < node.inputs.length; ++i) |
|
{ |
|
var input = node.inputs[i]; |
|
if(input.not_subgraph_input) |
|
continue; |
|
var html = "<button>✕</button> <span class='bullet_icon'></span><span class='name'></span><span class='type'></span>"; |
|
var elem = panel.addHTML(html,"subgraph_property"); |
|
elem.dataset["name"] = input.name; |
|
elem.dataset["slot"] = i; |
|
elem.querySelector(".name").innerText = input.name; |
|
elem.querySelector(".type").innerText = input.type; |
|
elem.querySelector("button").addEventListener("click",function(e){ |
|
node.removeInput( Number( this.parentNode.dataset["slot"] ) ); |
|
inner_refresh(); |
|
}); |
|
} |
|
} |
|
|
|
//add extra |
|
var html = " + <span class='label'>Name</span><input class='name'/><span class='label'>Type</span><input class='type'></input><button>+</button>"; |
|
var elem = panel.addHTML(html,"subgraph_property extra", true); |
|
elem.querySelector("button").addEventListener("click", function(e){ |
|
var elem = this.parentNode; |
|
var name = elem.querySelector(".name").value; |
|
var type = elem.querySelector(".type").value; |
|
if(!name || node.findInputSlot(name) != -1) |
|
return; |
|
node.addInput(name,type); |
|
elem.querySelector(".name").value = ""; |
|
elem.querySelector(".type").value = ""; |
|
inner_refresh(); |
|
}); |
|
|
|
inner_refresh(); |
|
this.canvas.parentNode.appendChild(panel); |
|
return panel; |
|
} |
|
LGraphCanvas.prototype.showSubgraphPropertiesDialogRight = function (node) { |
|
|
|
// console.log("showing subgraph properties dialog"); |
|
var that = this; |
|
// old_panel if old_panel is exist close it |
|
var old_panel = this.canvas.parentNode.querySelector(".subgraph_dialog"); |
|
if (old_panel) |
|
old_panel.close(); |
|
// new panel |
|
var panel = this.createPanel("Subgraph Outputs", { closable: true, width: 500 }); |
|
panel.node = node; |
|
panel.classList.add("subgraph_dialog"); |
|
|
|
function inner_refresh() { |
|
panel.clear(); |
|
//show currents |
|
if (node.outputs) |
|
for (var i = 0; i < node.outputs.length; ++i) { |
|
var input = node.outputs[i]; |
|
if (input.not_subgraph_output) |
|
continue; |
|
var html = "<button>✕</button> <span class='bullet_icon'></span><span class='name'></span><span class='type'></span>"; |
|
var elem = panel.addHTML(html, "subgraph_property"); |
|
elem.dataset["name"] = input.name; |
|
elem.dataset["slot"] = i; |
|
elem.querySelector(".name").innerText = input.name; |
|
elem.querySelector(".type").innerText = input.type; |
|
elem.querySelector("button").addEventListener("click", function (e) { |
|
node.removeOutput(Number(this.parentNode.dataset["slot"])); |
|
inner_refresh(); |
|
}); |
|
} |
|
} |
|
|
|
//add extra |
|
var html = " + <span class='label'>Name</span><input class='name'/><span class='label'>Type</span><input class='type'></input><button>+</button>"; |
|
var elem = panel.addHTML(html, "subgraph_property extra", true); |
|
elem.querySelector(".name").addEventListener("keydown", function (e) { |
|
if (e.keyCode == 13) { |
|
addOutput.apply(this) |
|
} |
|
}) |
|
elem.querySelector("button").addEventListener("click", function (e) { |
|
addOutput.apply(this) |
|
}); |
|
function addOutput() { |
|
var elem = this.parentNode; |
|
var name = elem.querySelector(".name").value; |
|
var type = elem.querySelector(".type").value; |
|
if (!name || node.findOutputSlot(name) != -1) |
|
return; |
|
node.addOutput(name, type); |
|
elem.querySelector(".name").value = ""; |
|
elem.querySelector(".type").value = ""; |
|
inner_refresh(); |
|
} |
|
|
|
inner_refresh(); |
|
this.canvas.parentNode.appendChild(panel); |
|
return panel; |
|
} |
|
LGraphCanvas.prototype.checkPanels = function() |
|
{ |
|
if(!this.canvas) |
|
return; |
|
var panels = this.canvas.parentNode.querySelectorAll(".litegraph.dialog"); |
|
for(var i = 0; i < panels.length; ++i) |
|
{ |
|
var panel = panels[i]; |
|
if( !panel.node ) |
|
continue; |
|
if( !panel.node.graph || panel.graph != this.graph ) |
|
panel.close(); |
|
} |
|
} |
|
|
|
LGraphCanvas.onMenuNodeCollapse = function(value, options, e, menu, node) { |
|
node.graph.beforeChange(/*?*/); |
|
|
|
var fApplyMultiNode = function(node){ |
|
node.collapse(); |
|
} |
|
|
|
var graphcanvas = LGraphCanvas.active_canvas; |
|
if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ |
|
fApplyMultiNode(node); |
|
}else{ |
|
for (var i in graphcanvas.selected_nodes) { |
|
fApplyMultiNode(graphcanvas.selected_nodes[i]); |
|
} |
|
} |
|
|
|
node.graph.afterChange(/*?*/); |
|
}; |
|
|
|
LGraphCanvas.onMenuNodePin = function(value, options, e, menu, node) { |
|
node.pin(); |
|
}; |
|
|
|
LGraphCanvas.onMenuNodeMode = function(value, options, e, menu, node) { |
|
new LiteGraph.ContextMenu( |
|
LiteGraph.NODE_MODES, |
|
{ event: e, callback: inner_clicked, parentMenu: menu, node: node } |
|
); |
|
|
|
function inner_clicked(v) { |
|
if (!node) { |
|
return; |
|
} |
|
var kV = Object.values(LiteGraph.NODE_MODES).indexOf(v); |
|
var fApplyMultiNode = function(node){ |
|
if (kV>=0 && LiteGraph.NODE_MODES[kV]) |
|
node.changeMode(kV); |
|
else{ |
|
console.warn("unexpected mode: "+v); |
|
node.changeMode(LiteGraph.ALWAYS); |
|
} |
|
} |
|
|
|
var graphcanvas = LGraphCanvas.active_canvas; |
|
if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ |
|
fApplyMultiNode(node); |
|
}else{ |
|
for (var i in graphcanvas.selected_nodes) { |
|
fApplyMultiNode(graphcanvas.selected_nodes[i]); |
|
} |
|
} |
|
} |
|
|
|
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; |
|
|
|
var fApplyColor = function(node){ |
|
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; |
|
} |
|
} |
|
|
|
var graphcanvas = LGraphCanvas.active_canvas; |
|
if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ |
|
fApplyColor(node); |
|
}else{ |
|
for (var i in graphcanvas.selected_nodes) { |
|
fApplyColor(graphcanvas.selected_nodes[i]); |
|
} |
|
} |
|
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.graph.beforeChange(/*?*/); //node |
|
|
|
var fApplyMultiNode = function(node){ |
|
node.shape = v; |
|
} |
|
|
|
var graphcanvas = LGraphCanvas.active_canvas; |
|
if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ |
|
fApplyMultiNode(node); |
|
}else{ |
|
for (var i in graphcanvas.selected_nodes) { |
|
fApplyMultiNode(graphcanvas.selected_nodes[i]); |
|
} |
|
} |
|
|
|
node.graph.afterChange(/*?*/); //node |
|
node.setDirtyCanvas(true); |
|
} |
|
|
|
return false; |
|
}; |
|
|
|
LGraphCanvas.onMenuNodeRemove = function(value, options, e, menu, node) { |
|
if (!node) { |
|
throw "no node passed"; |
|
} |
|
|
|
var graph = node.graph; |
|
graph.beforeChange(); |
|
|
|
|
|
var fApplyMultiNode = function(node){ |
|
if (node.removable === false) { |
|
return; |
|
} |
|
graph.remove(node); |
|
} |
|
|
|
var graphcanvas = LGraphCanvas.active_canvas; |
|
if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ |
|
fApplyMultiNode(node); |
|
}else{ |
|
for (var i in graphcanvas.selected_nodes) { |
|
fApplyMultiNode(graphcanvas.selected_nodes[i]); |
|
} |
|
} |
|
|
|
graph.afterChange(); |
|
node.setDirtyCanvas(true, true); |
|
}; |
|
|
|
LGraphCanvas.onMenuNodeToSubgraph = function(value, options, e, menu, node) { |
|
var graph = node.graph; |
|
var graphcanvas = LGraphCanvas.active_canvas; |
|
if(!graphcanvas) //?? |
|
return; |
|
|
|
var nodes_list = Object.values( graphcanvas.selected_nodes || {} ); |
|
if( !nodes_list.length ) |
|
nodes_list = [ node ]; |
|
|
|
var subgraph_node = LiteGraph.createNode("graph/subgraph"); |
|
subgraph_node.pos = node.pos.concat(); |
|
graph.add(subgraph_node); |
|
|
|
subgraph_node.buildFromNodes( nodes_list ); |
|
|
|
graphcanvas.deselectAllNodes(); |
|
node.setDirtyCanvas(true, true); |
|
}; |
|
|
|
LGraphCanvas.onMenuNodeClone = function(value, options, e, menu, node) { |
|
|
|
node.graph.beforeChange(); |
|
|
|
var newSelected = {}; |
|
|
|
var fApplyMultiNode = function(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); |
|
newSelected[newnode.id] = newnode; |
|
} |
|
|
|
var graphcanvas = LGraphCanvas.active_canvas; |
|
if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ |
|
fApplyMultiNode(node); |
|
}else{ |
|
for (var i in graphcanvas.selected_nodes) { |
|
fApplyMultiNode(graphcanvas.selected_nodes[i]); |
|
} |
|
} |
|
|
|
if(Object.keys(newSelected).length){ |
|
graphcanvas.selectNodes(newSelected); |
|
} |
|
|
|
node.graph.afterChange(); |
|
|
|
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; |
|
var that = this; |
|
if (this.getMenuOptions) { |
|
options = this.getMenuOptions(); |
|
} else { |
|
options = [ |
|
{ |
|
content: "Add Node", |
|
has_submenu: true, |
|
callback: LGraphCanvas.onMenuAdd |
|
}, |
|
{ content: "Add Group", callback: LGraphCanvas.onGroupAdd }, |
|
//{ content: "Arrange", callback: that.graph.arrange }, |
|
//{content:"Collapse All", callback: LGraphCanvas.onMenuCollapseAll } |
|
]; |
|
/*if (LiteGraph.showCanvasOptions){ |
|
options.push({ content: "Options", callback: that.showShowGraphOptionsPanel }); |
|
}*/ |
|
|
|
if (this._graph_stack && this._graph_stack.length > 0) { |
|
options.push(null, { |
|
content: "Close subgraph", |
|
callback: this.closeSubgraph.bind(this) |
|
}); |
|
} |
|
} |
|
|
|
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.onShowPropertyEditor |
|
}, |
|
{ |
|
content: "Mode", |
|
has_submenu: true, |
|
callback: LGraphCanvas.onMenuNodeMode |
|
}]; |
|
if(node.resizable !== false){ |
|
options.push({ |
|
content: "Resize", callback: LGraphCanvas.onMenuResizeNode |
|
}); |
|
} |
|
options.push( |
|
{ |
|
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.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.getExtraMenuOptions) { |
|
var extra = node.getExtraMenuOptions(this, options); |
|
if (extra) { |
|
extra.push(null); |
|
options = extra.concat(options); |
|
} |
|
} |
|
|
|
if (node.clonable !== false) { |
|
options.push({ |
|
content: "Clone", |
|
callback: LGraphCanvas.onMenuNodeClone |
|
}); |
|
} |
|
|
|
if(0) //TODO |
|
options.push({ |
|
content: "To Subgraph", |
|
callback: LGraphCanvas.onMenuNodeToSubgraph |
|
}); |
|
|
|
options.push(null, { |
|
content: "Remove", |
|
disabled: !(node.removable !== false && !node.block_delete ), |
|
callback: LGraphCanvas.onMenuNodeRemove |
|
}); |
|
|
|
if (node.graph && node.graph.onGetNodeMenuOptions) { |
|
node.graph.onGetNodeMenuOptions(options, node); |
|
} |
|
|
|
return options; |
|
}; |
|
|
|
LGraphCanvas.prototype.getGroupMenuOptions = function(node) { |
|
var o = [ |
|
{ content: "Title", callback: LGraphCanvas.onShowPropertyEditor }, |
|
{ |
|
content: "Color", |
|
has_submenu: true, |
|
callback: LGraphCanvas.onMenuNodeColors |
|
}, |
|
{ |
|
content: "Font size", |
|
property: "font_size", |
|
type: "Number", |
|
callback: LGraphCanvas.onShowPropertyEditor |
|
}, |
|
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, |
|
extra: node |
|
}; |
|
|
|
if(node) |
|
options.title = node.type; |
|
|
|
//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 = []; |
|
if (node.getSlotMenuOptions) { |
|
menu_info = node.getSlotMenuOptions(slot); |
|
} else { |
|
if ( |
|
slot && |
|
slot.output && |
|
slot.output.links && |
|
slot.output.links.length |
|
) { |
|
menu_info.push({ content: "Disconnect Links", slot: slot }); |
|
} |
|
var _slot = slot.input || slot.output; |
|
if (_slot.removable){ |
|
menu_info.push( |
|
_slot.locked |
|
? "Cannot remove" |
|
: { content: "Remove Slot", slot: slot } |
|
); |
|
} |
|
if (!_slot.nameLocked){ |
|
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 { |
|
menu_info = this.getCanvasMenuOptions(); |
|
var group = this.graph.getGroupOnPos( |
|
event.canvasX, |
|
event.canvasY |
|
); |
|
if (group) { |
|
//on group |
|
menu_info.push(null, { |
|
content: "Edit Group", |
|
has_submenu: true, |
|
submenu: { |
|
title: "Group", |
|
extra: group, |
|
options: this.getGroupMenuOptions(group) |
|
} |
|
}); |
|
} |
|
} |
|
} |
|
|
|
//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; |
|
node.graph.beforeChange(); |
|
if (info.input) { |
|
node.removeInput(info.slot); |
|
} else if (info.output) { |
|
node.removeOutput(info.slot); |
|
} |
|
node.graph.afterChange(); |
|
return; |
|
} else if (v.content == "Disconnect Links") { |
|
var info = v.slot; |
|
node.graph.beforeChange(); |
|
if (info.output) { |
|
node.disconnectOutput(info.slot); |
|
} else if (info.input) { |
|
node.disconnectInput(info.slot); |
|
} |
|
node.graph.afterChange(); |
|
return; |
|
} else if (v.content == "Rename Slot") { |
|
var info = v.slot; |
|
var slot_info = info.input |
|
? node.getInputInfo(info.slot) |
|
: node.getOutputInfo(info.slot); |
|
var dialog = that.createDialog( |
|
"<span class='name'>Name</span><input autofocus type='text'/><button>OK</button>", |
|
options |
|
); |
|
var input = dialog.querySelector("input"); |
|
if (input && slot_info) { |
|
input.value = slot_info.label || ""; |
|
} |
|
var inner = function(){ |
|
node.graph.beforeChange(); |
|
if (input.value) { |
|
if (slot_info) { |
|
slot_info.label = input.value; |
|
} |
|
that.setDirty(true); |
|
} |
|
dialog.close(); |
|
node.graph.afterChange(); |
|
} |
|
dialog.querySelector("button").addEventListener("click", inner); |
|
input.addEventListener("keydown", function(e) { |
|
dialog.is_modified = true; |
|
if (e.keyCode == 27) { |
|
//ESC |
|
dialog.close(); |
|
} else if (e.keyCode == 13) { |
|
inner(); // save |
|
} else if (e.keyCode != 13 && e.target.localName != "textarea") { |
|
return; |
|
} |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
}); |
|
input.focus(); |
|
} |
|
|
|
//if(v.callback) |
|
// return v.callback.call(that, node, options, e, menu, that, event ); |
|
} |
|
}; |
|
|
|
//API ************************************************* |
|
//like rect but rounded corners |
|
if (typeof(window) != "undefined" && window.CanvasRenderingContext2D && !window.CanvasRenderingContext2D.prototype.roundRect) { |
|
window.CanvasRenderingContext2D.prototype.roundRect = function( |
|
x, |
|
y, |
|
w, |
|
h, |
|
radius, |
|
radius_low |
|
) { |
|
var top_left_radius = 0; |
|
var top_right_radius = 0; |
|
var bottom_left_radius = 0; |
|
var bottom_right_radius = 0; |
|
|
|
if ( radius === 0 ) |
|
{ |
|
this.rect(x,y,w,h); |
|
return; |
|
} |
|
|
|
if(radius_low === undefined) |
|
radius_low = radius; |
|
|
|
//make it compatible with official one |
|
if(radius != null && radius.constructor === Array) |
|
{ |
|
if(radius.length == 1) |
|
top_left_radius = top_right_radius = bottom_left_radius = bottom_right_radius = radius[0]; |
|
else if(radius.length == 2) |
|
{ |
|
top_left_radius = bottom_right_radius = radius[0]; |
|
top_right_radius = bottom_left_radius = radius[1]; |
|
} |
|
else if(radius.length == 4) |
|
{ |
|
top_left_radius = radius[0]; |
|
top_right_radius = radius[1]; |
|
bottom_left_radius = radius[2]; |
|
bottom_right_radius = radius[3]; |
|
} |
|
else |
|
return; |
|
} |
|
else //old using numbers |
|
{ |
|
top_left_radius = radius || 0; |
|
top_right_radius = radius || 0; |
|
bottom_left_radius = radius_low || 0; |
|
bottom_right_radius = radius_low || 0; |
|
} |
|
|
|
//top right |
|
this.moveTo(x + top_left_radius, y); |
|
this.lineTo(x + w - top_right_radius, y); |
|
this.quadraticCurveTo(x + w, y, x + w, y + top_right_radius); |
|
|
|
//bottom right |
|
this.lineTo(x + w, y + h - bottom_right_radius); |
|
this.quadraticCurveTo( |
|
x + w, |
|
y + h, |
|
x + w - bottom_right_radius, |
|
y + h |
|
); |
|
|
|
//bottom left |
|
this.lineTo(x + bottom_right_radius, y + h); |
|
this.quadraticCurveTo(x, y + h, x, y + h - bottom_left_radius); |
|
|
|
//top left |
|
this.lineTo(x, y + bottom_left_radius); |
|
this.quadraticCurveTo(x, y, x + top_left_radius, y); |
|
}; |
|
}//if |
|
|
|
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 bounding 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; |
|
|
|
//bounding 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; |
|
} |
|
} |
|
|
|
var eventClass = null; |
|
if(options.event) //use strings because comparing classes between windows doesnt work |
|
eventClass = options.event.constructor.name; |
|
if ( eventClass !== "MouseEvent" && |
|
eventClass !== "CustomEvent" && |
|
eventClass !== "PointerEvent" |
|
) { |
|
console.error( |
|
"Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it. ("+eventClass+")" |
|
); |
|
options.event = null; |
|
} |
|
|
|
var root = document.createElement("div"); |
|
root.className = "litegraph litecontextmenu litemenubar-panel"; |
|
if (options.className) { |
|
root.className += " " + options.className; |
|
} |
|
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 caught by this element |
|
|
|
//this prevents the default context browser menu to open in case this menu was created when pressing right button |
|
LiteGraph.pointerListenerAdd(root,"up", |
|
function(e) { |
|
//console.log("pointerevents: ContextMenu up root prevent"); |
|
e.preventDefault(); |
|
return true; |
|
}, |
|
true |
|
); |
|
root.addEventListener( |
|
"contextmenu", |
|
function(e) { |
|
if (e.button != 2) { |
|
//right button |
|
return false; |
|
} |
|
e.preventDefault(); |
|
return false; |
|
}, |
|
true |
|
); |
|
|
|
LiteGraph.pointerListenerAdd(root,"down", |
|
function(e) { |
|
//console.log("pointerevents: ContextMenu down"); |
|
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 * options.scroll_speed).toFixed() + "px"; |
|
e.preventDefault(); |
|
return true; |
|
} |
|
|
|
if (!options.scroll_speed) { |
|
options.scroll_speed = 0.1; |
|
} |
|
|
|
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=0; i < values.length; i++) { |
|
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? touch enabled devices won't work TODO use a global device detector and condition on that |
|
/*LiteGraph.pointerListenerAdd(root,"leave", function(e) { |
|
console.log("pointerevents: ContextMenu leave"); |
|
if (that.lock) { |
|
return; |
|
} |
|
if (root.closing_timer) { |
|
clearTimeout(root.closing_timer); |
|
} |
|
root.closing_timer = setTimeout(that.close.bind(that, e), 500); |
|
//that.close(e); |
|
});*/ |
|
|
|
LiteGraph.pointerListenerAdd(root,"enter", function(e) { |
|
//console.log("pointerevents: ContextMenu enter"); |
|
if (root.closing_timer) { |
|
clearTimeout(root.closing_timer); |
|
} |
|
}); |
|
|
|
//insert before checking position |
|
var root_document = document; |
|
if (options.event) { |
|
root_document = options.event.target.ownerDocument; |
|
} |
|
|
|
if (!root_document) { |
|
root_document = document; |
|
} |
|
|
|
if( root_document.fullscreenElement ) |
|
root_document.fullscreenElement.appendChild(root); |
|
else |
|
root_document.body.appendChild(root); |
|
|
|
//compute best position |
|
var left = options.left || 0; |
|
var top = options.top || 0; |
|
if (options.event) { |
|
left = options.event.clientX - 10; |
|
top = options.event.clientY - 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(body_rect.height == 0) |
|
console.error("document.body height is 0. That is dangerous, set html,body { height: 100%; }"); |
|
|
|
if (body_rect.width && left > body_rect.width - root_rect.width - 10) { |
|
left = body_rect.width - root_rect.width - 10; |
|
} |
|
if (body_rect.height && 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"; |
|
|
|
if (options.scale) { |
|
root.style.transform = "scale(" + options.scale + ")"; |
|
} |
|
} |
|
|
|
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) { |
|
LiteGraph.pointerListenerAdd(element,"enter",inner_over); |
|
} |
|
|
|
function inner_over(e) { |
|
var value = this.value; |
|
if (!value || !value.has_submenu) { |
|
return; |
|
} |
|
//if it is a submenu, autoopen like the item was clicked |
|
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.extra |
|
); |
|
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, |
|
extra: value.submenu.extra, |
|
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, LiteGraph.pointerevents_method+"leave", e); |
|
} |
|
} |
|
if (this.current_submenu) { |
|
this.current_submenu.close(e, true); |
|
} |
|
|
|
if (this.root.closing_timer) { |
|
clearTimeout(this.root.closing_timer); |
|
} |
|
|
|
// TODO implement : LiteGraph.contextMenuClosed(); :: keep track of opened / closed / current ContextMenu |
|
// on key press, allow filtering/selecting the context menu elements |
|
}; |
|
|
|
//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.clientX; |
|
var top = event.clientY; |
|
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=0; i < result.length; i++) { |
|
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 enumerable |
|
if (!origin.prototype.hasOwnProperty(i)) { |
|
continue; |
|
} |
|
|
|
if (target.prototype.hasOwnProperty(i)) { |
|
//avoid overwriting 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 by some widgets to render a curve editor |
|
function CurveEditor( points ) |
|
{ |
|
this.points = points; |
|
this.selected = -1; |
|
this.nearest = -1; |
|
this.size = null; //stores last size used |
|
this.must_update = true; |
|
this.margin = 5; |
|
} |
|
|
|
CurveEditor.sampleCurve = function(f,points) |
|
{ |
|
if(!points) |
|
return; |
|
for(var i = 0; i < points.length - 1; ++i) |
|
{ |
|
var p = points[i]; |
|
var pn = points[i+1]; |
|
if(pn[0] < f) |
|
continue; |
|
var r = (pn[0] - p[0]); |
|
if( Math.abs(r) < 0.00001 ) |
|
return p[1]; |
|
var local_f = (f - p[0]) / r; |
|
return p[1] * (1.0 - local_f) + pn[1] * local_f; |
|
} |
|
return 0; |
|
} |
|
|
|
CurveEditor.prototype.draw = function( ctx, size, graphcanvas, background_color, line_color, inactive ) |
|
{ |
|
var points = this.points; |
|
if(!points) |
|
return; |
|
this.size = size; |
|
var w = size[0] - this.margin * 2; |
|
var h = size[1] - this.margin * 2; |
|
|
|
line_color = line_color || "#666"; |
|
|
|
ctx.save(); |
|
ctx.translate(this.margin,this.margin); |
|
|
|
if(background_color) |
|
{ |
|
ctx.fillStyle = "#111"; |
|
ctx.fillRect(0,0,w,h); |
|
ctx.fillStyle = "#222"; |
|
ctx.fillRect(w*0.5,0,1,h); |
|
ctx.strokeStyle = "#333"; |
|
ctx.strokeRect(0,0,w,h); |
|
} |
|
ctx.strokeStyle = line_color; |
|
if(inactive) |
|
ctx.globalAlpha = 0.5; |
|
ctx.beginPath(); |
|
for(var i = 0; i < points.length; ++i) |
|
{ |
|
var p = points[i]; |
|
ctx.lineTo( p[0] * w, (1.0 - p[1]) * h ); |
|
} |
|
ctx.stroke(); |
|
ctx.globalAlpha = 1; |
|
if(!inactive) |
|
for(var i = 0; i < points.length; ++i) |
|
{ |
|
var p = points[i]; |
|
ctx.fillStyle = this.selected == i ? "#FFF" : (this.nearest == i ? "#DDD" : "#AAA"); |
|
ctx.beginPath(); |
|
ctx.arc( p[0] * w, (1.0 - p[1]) * h, 2, 0, Math.PI * 2 ); |
|
ctx.fill(); |
|
} |
|
ctx.restore(); |
|
} |
|
|
|
//localpos is mouse in curve editor space |
|
CurveEditor.prototype.onMouseDown = function( localpos, graphcanvas ) |
|
{ |
|
var points = this.points; |
|
if(!points) |
|
return; |
|
if( localpos[1] < 0 ) |
|
return; |
|
|
|
//this.captureInput(true); |
|
var w = this.size[0] - this.margin * 2; |
|
var h = this.size[1] - this.margin * 2; |
|
var x = localpos[0] - this.margin; |
|
var y = localpos[1] - this.margin; |
|
var pos = [x,y]; |
|
var max_dist = 30 / graphcanvas.ds.scale; |
|
//search closer one |
|
this.selected = this.getCloserPoint(pos, max_dist); |
|
//create one |
|
if(this.selected == -1) |
|
{ |
|
var point = [x / w, 1 - y / h]; |
|
points.push(point); |
|
points.sort(function(a,b){ return a[0] - b[0]; }); |
|
this.selected = points.indexOf(point); |
|
this.must_update = true; |
|
} |
|
if(this.selected != -1) |
|
return true; |
|
} |
|
|
|
CurveEditor.prototype.onMouseMove = function( localpos, graphcanvas ) |
|
{ |
|
var points = this.points; |
|
if(!points) |
|
return; |
|
var s = this.selected; |
|
if(s < 0) |
|
return; |
|
var x = (localpos[0] - this.margin) / (this.size[0] - this.margin * 2 ); |
|
var y = (localpos[1] - this.margin) / (this.size[1] - this.margin * 2 ); |
|
var curvepos = [(localpos[0] - this.margin),(localpos[1] - this.margin)]; |
|
var max_dist = 30 / graphcanvas.ds.scale; |
|
this._nearest = this.getCloserPoint(curvepos, max_dist); |
|
var point = points[s]; |
|
if(point) |
|
{ |
|
var is_edge_point = s == 0 || s == points.length - 1; |
|
if( !is_edge_point && (localpos[0] < -10 || localpos[0] > this.size[0] + 10 || localpos[1] < -10 || localpos[1] > this.size[1] + 10) ) |
|
{ |
|
points.splice(s,1); |
|
this.selected = -1; |
|
return; |
|
} |
|
if( !is_edge_point ) //not edges |
|
point[0] = Math.clamp(x,0,1); |
|
else |
|
point[0] = s == 0 ? 0 : 1; |
|
point[1] = 1.0 - Math.clamp(y,0,1); |
|
points.sort(function(a,b){ return a[0] - b[0]; }); |
|
this.selected = points.indexOf(point); |
|
this.must_update = true; |
|
} |
|
} |
|
|
|
CurveEditor.prototype.onMouseUp = function( localpos, graphcanvas ) |
|
{ |
|
this.selected = -1; |
|
return false; |
|
} |
|
|
|
CurveEditor.prototype.getCloserPoint = function(pos, max_dist) |
|
{ |
|
var points = this.points; |
|
if(!points) |
|
return -1; |
|
max_dist = max_dist || 30; |
|
var w = (this.size[0] - this.margin * 2); |
|
var h = (this.size[1] - this.margin * 2); |
|
var num = points.length; |
|
var p2 = [0,0]; |
|
var min_dist = 1000000; |
|
var closest = -1; |
|
var last_valid = -1; |
|
for(var i = 0; i < num; ++i) |
|
{ |
|
var p = points[i]; |
|
p2[0] = p[0] * w; |
|
p2[1] = (1.0 - p[1]) * h; |
|
if(p2[0] < pos[0]) |
|
last_valid = i; |
|
var dist = vec2.distance(pos,p2); |
|
if(dist > min_dist || dist > max_dist) |
|
continue; |
|
closest = i; |
|
min_dist = dist; |
|
} |
|
return closest; |
|
} |
|
|
|
LiteGraph.CurveEditor = CurveEditor; |
|
|
|
//used to create nodes from wrapping functions |
|
LiteGraph.getParameterNames = function(func) { |
|
return (func + "") |
|
.replace(/[/][/].*$/gm, "") // 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 [""] |
|
}; |
|
|
|
/* helper for interaction: pointer, touch, mouse Listeners |
|
used by LGraphCanvas DragAndScale ContextMenu*/ |
|
LiteGraph.pointerListenerAdd = function(oDOM, sEvIn, fCall, capture=false) { |
|
if (!oDOM || !oDOM.addEventListener || !sEvIn || typeof fCall!=="function"){ |
|
//console.log("cant pointerListenerAdd "+oDOM+", "+sEvent+", "+fCall); |
|
return; // -- break -- |
|
} |
|
|
|
var sMethod = LiteGraph.pointerevents_method; |
|
var sEvent = sEvIn; |
|
|
|
// UNDER CONSTRUCTION |
|
// convert pointerevents to touch event when not available |
|
if (sMethod=="pointer" && !window.PointerEvent){ |
|
console.warn("sMethod=='pointer' && !window.PointerEvent"); |
|
console.log("Converting pointer["+sEvent+"] : down move up cancel enter TO touchstart touchmove touchend, etc .."); |
|
switch(sEvent){ |
|
case "down":{ |
|
sMethod = "touch"; |
|
sEvent = "start"; |
|
break; |
|
} |
|
case "move":{ |
|
sMethod = "touch"; |
|
//sEvent = "move"; |
|
break; |
|
} |
|
case "up":{ |
|
sMethod = "touch"; |
|
sEvent = "end"; |
|
break; |
|
} |
|
case "cancel":{ |
|
sMethod = "touch"; |
|
//sEvent = "cancel"; |
|
break; |
|
} |
|
case "enter":{ |
|
console.log("debug: Should I send a move event?"); // ??? |
|
break; |
|
} |
|
// case "over": case "out": not used at now |
|
default:{ |
|
console.warn("PointerEvent not available in this browser ? The event "+sEvent+" would not be called"); |
|
} |
|
} |
|
} |
|
|
|
switch(sEvent){ |
|
//both pointer and move events |
|
case "down": case "up": case "move": case "over": case "out": case "enter": |
|
{ |
|
oDOM.addEventListener(sMethod+sEvent, fCall, capture); |
|
} |
|
// only pointerevents |
|
case "leave": case "cancel": case "gotpointercapture": case "lostpointercapture": |
|
{ |
|
if (sMethod!="mouse"){ |
|
return oDOM.addEventListener(sMethod+sEvent, fCall, capture); |
|
} |
|
} |
|
// not "pointer" || "mouse" |
|
default: |
|
return oDOM.addEventListener(sEvent, fCall, capture); |
|
} |
|
} |
|
LiteGraph.pointerListenerRemove = function(oDOM, sEvent, fCall, capture=false) { |
|
if (!oDOM || !oDOM.removeEventListener || !sEvent || typeof fCall!=="function"){ |
|
//console.log("cant pointerListenerRemove "+oDOM+", "+sEvent+", "+fCall); |
|
return; // -- break -- |
|
} |
|
switch(sEvent){ |
|
//both pointer and move events |
|
case "down": case "up": case "move": case "over": case "out": case "enter": |
|
{ |
|
if (LiteGraph.pointerevents_method=="pointer" || LiteGraph.pointerevents_method=="mouse"){ |
|
oDOM.removeEventListener(LiteGraph.pointerevents_method+sEvent, fCall, capture); |
|
} |
|
} |
|
// only pointerevents |
|
case "leave": case "cancel": case "gotpointercapture": case "lostpointercapture": |
|
{ |
|
if (LiteGraph.pointerevents_method=="pointer"){ |
|
return oDOM.removeEventListener(LiteGraph.pointerevents_method+sEvent, fCall, capture); |
|
} |
|
} |
|
// not "pointer" || "mouse" |
|
default: |
|
return oDOM.removeEventListener(sEvent, fCall, capture); |
|
} |
|
} |
|
|
|
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; |
|
} |
|
|
|
|
|
|