The most powerful and modular stable diffusion GUI, api and backend with a graph/nodes interface.
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.
 
 
 
 

14424 lines
480 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: 10000, //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,
VERTICAL_LAYOUT: "vertical", // arrange nodes vertically
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: false, // [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) default 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) default 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 sequentially, 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)
ctrl_shift_v_paste_connect_unselected_outputs: true, //[true!] allows ctrl + shift + v to paste nodes with the outputs of the unselected nodes connected with the inputs of the newly pasted nodes
// if true, all newly created nodes/links will use string UUIDs for their id fields instead of integers.
// use this if you must have node IDs that are unique across all graphs and subgraphs.
use_uuids: false,
/**
* 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);
}
const classname = base_class.name;
const pos = type.lastIndexOf("/");
base_class.category = type.substring(0, pos);
if (!base_class.title) {
base_class.title = classname;
}
//extend class
for (var i in LGraphNode.prototype) {
if (!base_class.prototype[i]) {
base_class.prototype[i] = LGraphNode.prototype[i];
}
}
const prev = this.registered_node_types[type];
if(prev) {
console.log("replacing node type: " + type);
}
if( !Object.prototype.hasOwnProperty.call( 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() {
return this._shape;
},
enumerable: true,
configurable: true
});
//used to know which nodes to create when dragging files to the canvas
if (base_class.supported_extensions) {
for (let i in base_class.supported_extensions) {
const 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"
);
}
// TODO one would want to know input and ouput :: this would allow through registerNodeAndSlotType to get all the slots types
if (this.auto_load_slot_types) {
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) {
const 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;
const base_class =
type.constructor === String &&
this.registered_node_types[type] !== "anonymous"
? this.registered_node_types[type]
: type;
const class_type = base_class.constructor.type;
let allTypes = [];
if (typeof slot_type === "string") {
allTypes = slot_type.split(",");
} else if (slot_type == this.EVENT || slot_type == this.ACTION) {
allTypes = ["_event_"];
} else {
allTypes = ["*"];
}
for (let i = 0; i < allTypes.length; ++i) {
let slotType = allTypes[i];
if (slotType === "") {
slotType = "*";
}
const registerTo = out
? "registered_slot_out_types"
: "registered_slot_in_types";
if (this[registerTo][slotType] === undefined) {
this[registerTo][slotType] = { nodes: [] };
}
if (!this[registerTo][slotType].nodes.includes(class_type)) {
this[registerTo][slotType].nodes.push(class_type);
}
// check if is a new type
if (!out) {
if (!this.slot_types_in.includes(slotType.toLowerCase())) {
this.slot_types_in.push(slotType.toLowerCase());
this.slot_types_in.sort();
}
} else {
if (!this.slot_types_out.includes(slotType.toLowerCase())) {
this.slot_types_out.push(slotType.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;
},
/*
* https://gist.github.com/jed/982883?permalink_comment_id=852670#gistcomment-852670
*/
uuidv4: function() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,a=>(a^Math.random()*16>>a/4).toString(16));
},
/**
* 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, layout) {
margin = margin || 100;
const nodes = this.computeExecutionOrder(false, true);
const columns = [];
for (let i = 0; i < nodes.length; ++i) {
const node = nodes[i];
const col = node._level || 1;
if (!columns[col]) {
columns[col] = [];
}
columns[col].push(node);
}
let x = margin;
for (let i = 0; i < columns.length; ++i) {
const column = columns[i];
if (!column) {
continue;
}
let max_size = 100;
let y = margin + LiteGraph.NODE_TITLE_HEIGHT;
for (let j = 0; j < column.length; ++j) {
const node = column[j];
node.pos[0] = (layout == LiteGraph.VERTICAL_LAYOUT) ? y : x;
node.pos[1] = (layout == LiteGraph.VERTICAL_LAYOUT) ? x : y;
const max_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 1 : 0;
if (node.size[max_size_index] > max_size) {
max_size = node.size[max_size_index];
}
const node_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 0 : 1;
y += node.size[node_size_index] + 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"
);
if (LiteGraph.use_uuids) {
node.id = LiteGraph.uuidv4();
}
else {
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 (LiteGraph.use_uuids) {
if (node.id == null || node.id == -1)
node.id = LiteGraph.uuidv4();
}
else {
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];
var skip_title = n.constructor.title_mode == LiteGraph.NO_TITLE;
if (n.isPointInside(x, y, margin, skip_title)) {
// 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
});
if (LiteGraph.use_uuids) {
this.id = LiteGraph.uuidv4();
}
else {
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.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;
if (this.onConnectionsChange)
this.onConnectionsChange( LiteGraph.INPUT, i, true, link_info, input ); //link_info has been created now, so its updated
if( this.onInputAdded )
this.onInputAdded(input);
}
}
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;
if (this.onConnectionsChange)
this.onConnectionsChange( LiteGraph.OUTPUT, i, true, link_info, output ); //link_info has been created now, so its updated
}
if( this.onOutputAdded )
this.onOutputAdded(output);
}
}
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 ] != undefined))
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"];
if (LiteGraph.use_uuids) {
data["id"] = LiteGraph.uuidv4()
}
//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;
}
if(slot == null)
{
console.error("slot must be a number");
return;
}
if(slot.constructor !== Number)
console.warn("slot must be a number, use node.trigger('name') if you want to use a string");
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 output = { name: name, type: type, links: null };
if (extra_info) {
for (var i in extra_info) {
output[i] = extra_info[i];
}
}
if (!this.outputs) {
this.outputs = [];
}
this.outputs.push(output);
if (this.onOutputAdded) {
this.onOutputAdded(output);
}
if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this,type,true);
this.setSize( this.computeSize() );
this.setDirtyCanvas(true, true);
return output;
};
/**
* 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 input = { name: name, type: type, link: null };
if (extra_info) {
for (var i in extra_info) {
input[i] = extra_info[i];
}
}
if (!this.inputs) {
this.inputs = [];
}
this.inputs.push(input);
this.setSize( this.computeSize() );
if (this.onInputAdded) {
this.onInputAdded(input);
}
LiteGraph.registerNodeAndSlotType(this,type);
this.setDirtyCanvas(true, true);
return input;
};
/**
* 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 {vec2} minHeight
* @return {vec2} 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;
};
LGraphNode.prototype.inResizeCorner = function(canvasX, canvasY) {
var rows = this.outputs ? this.outputs.length : 1;
var outputs_offset = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT;
return isInsideRectangle(canvasX,
canvasY,
this.pos[0] + this.size[0] - 15,
this.pos[1] + Math.max(this.size[1] - 15, outputs_offset),
20,
20
);
}
/**
* 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
* @method getBounding
* @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage
* @param compute_outer {boolean?} [optional] set to true to include the shadow and connection points in the bounding calculation
* @return {Float32Array[4]} the bounding box in format of [topleft_cornerx, topleft_cornery, width, height]
*/
LGraphNode.prototype.getBounding = function(out, compute_outer) {
out = out || new Float32Array(4);
const nodePos = this.pos;
const isCollapsed = this.flags.collapsed;
const nodeSize = this.size;
let left_offset = 0;
// 1 offset due to how nodes are rendered
let right_offset = 1 ;
let top_offset = 0;
let bottom_offset = 0;
if (compute_outer) {
// 4 offset for collapsed node connection points
left_offset = 4;
// 6 offset for right shadow and collapsed node connection points
right_offset = 6 + left_offset;
// 4 offset for collapsed nodes top connection points
top_offset = 4;
// 5 offset for bottom shadow and collapsed node connection points
bottom_offset = 5 + top_offset;
}
out[0] = nodePos[0] - left_offset;
out[1] = nodePos[1] - LiteGraph.NODE_TITLE_HEIGHT - top_offset;
out[2] = isCollapsed ?
(this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + right_offset :
nodeSize[0] + right_offset;
out[3] = isCollapsed ?
LiteGraph.NODE_TITLE_HEIGHT + bottom_offset :
nodeSize[1] + LiteGraph.NODE_TITLE_HEIGHT + bottom_offset;
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(var sI=0;sI<aSource.length;sI++){
for(var 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(var sI=0;sI<aSource.length;sI++){
for(var 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);
}
var 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);
}
var 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;
}
}
var nextId
if (LiteGraph.use_uuids)
nextId = LiteGraph.uuidv4();
else
nextId = ++this.graph.last_link_id;
//create link class
link_info = new LLink(
nextId,
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;
if (o.font_size) {
this.font_size = o.font_size;
}
};
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_size: this.font_size
};
};
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.clear_background_color = "#222";
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.multi_select = false; //allow selecting multi nodes without pressing extra keys
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 visually 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);
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 || node.flags.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.allow_interaction && !this.connecting_node && !node.flags.collapsed && !this.live_mode ) {
//Search for corner for resize
if ( !skip_action &&
node.resizable !== false && node.inResizeCorner(e.canvasX, e.canvasY)
) {
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;
if(node && node.flags && node.flags.pinned) {
block_drag_node = true;
}
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 (this.allow_interaction && 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;
}
this.processNodeSelected(node, e);
} else { // double-click
/**
* Don't call the function if the block is already selected.
* Otherwise, it could cause the block to be unselected while its panel is open.
*/
if (!node.is_selected) 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*/
});
skip_action = true;
}
}
}
}
if (!skip_action && this.allow_dragcanvas) {
//console.log("pointerevents: dragging_canvas start from middle button");
this.dragging_canvas = true;
}
} 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;
}
//get node over
var node = this.graph.getNodeOnPos(e.canvasX,e.canvasY,this.visible_nodes);
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 || (node && node.flags.allow_interaction)) && !this.read_only) {
if (this.connecting_node) {
this.dirty_canvas = true;
}
//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 (node.inResizeCorner(e.canvasX, e.canvasY)) {
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;
if (!n.is_selected) this.processNodeSelected(n, e); /*
* Don't call the function if the block is already selected.
* Otherwise, it could cause the block to be unselected while dragging.
*/
}
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.keyCode === 67) && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
//copy
if (this.selected_nodes) {
this.copyToClipboard();
block_default = true;
}
}
if ((e.keyCode === 86) && (e.metaKey || e.ctrlKey)) {
//paste
this.pasteFromClipboard(e.shiftKey);
}
//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(nodes) {
var clipboard_info = {
nodes: [],
links: []
};
var index = 0;
var selected_nodes_array = [];
if (!nodes) nodes = this.selected_nodes;
for (var i in nodes) {
var node = nodes[i];
if (node.clonable === false)
continue;
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) {
continue;
}
clipboard_info.links.push([
target_node._relative_id,
link_info.origin_slot, //j,
node._relative_id,
link_info.target_slot,
target_node.id
]);
}
}
}
localStorage.setItem(
"litegrapheditor_clipboard",
JSON.stringify(clipboard_info)
);
};
LGraphCanvas.prototype.pasteFromClipboard = function(isConnectUnselected = false) {
// if ctrl + shift + v is off, return when isConnectUnselected is true (shift is pressed) to maintain old behavior
if (!LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) {
return;
}
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;
var origin_node_relative_id = link_info[0];
if (origin_node_relative_id != null) {
origin_node = nodes[origin_node_relative_id];
} else if (LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) {
var origin_node_id = link_info[4];
if (origin_node_id) {
origin_node = this.graph.getNodeById(origin_node_id);
}
}
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);
}
if (this.onNodeDblClicked) {
this.onNodeDblClicked(n);
}
this.setDirty(true);
};
LGraphCanvas.prototype.processNodeSelected = function(node, e) {
this.selectNode(node, e && (e.shiftKey || e.ctrlKey || this.multi_select));
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) {
this.deselectNode(node);
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, true))) {
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 pos = this.ds.convertOffsetToCanvas(this.graph_mouse);
var hover = LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h );
pos = this.last_click_position ? [this.last_click_position[0], this.last_click_position[1]] : null;
if(pos) {
var rect = this.canvas.getBoundingClientRect();
pos[0] -= rect.left;
pos[1] -= rect.top;
}
var clicked = pos && LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,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.offsetHeight - 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.ds.scale < 1.5 && !bg_already_painted && this.clear_background_color )
{
ctx.fillStyle = this.clear_background_color;
ctx.fillRect(
this.visible_area[0],
this.visible_area[1],
this.visible_area[2],
this.visible_area[3]
);
}
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":
ctx.fillStyle = background_color;
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.label || 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;
const label = w.label || w.name;
if (label != null) {
ctx.fillText(label, 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;
if(nvalue < 0.0) nvalue = 0.0;
if(nvalue > 1.0) nvalue = 1.0;
ctx.fillStyle = w.options.hasOwnProperty("slider_color") ? w.options.slider_color : (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;
if(marker_nvalue < 0.0) marker_nvalue = 0.0;
if(marker_nvalue > 1.0) marker_nvalue = 1.0;
ctx.fillStyle = w.options.hasOwnProperty("marker_color") ? w.options.marker_color : "#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.label || w.name + " " + Number(w.value).toFixed(
w.options.precision != null
? w.options.precision
: 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.label || 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;
const label = w.label || w.name;
if (label != null) {
ctx.fillText(label, 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 || (!this.allow_interaction && !node.flags.allow_interaction)) {
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 old_value = w.value;
var nvalue = clamp((x - 15) / (widget_width - 30), 0, 1);
if(w.options.read_only) break;
w.value = w.options.min + (w.options.max - w.options.min) * nvalue;
if (old_value != w.value) {
setTimeout(function() {
inner_value_change(w, w.value);
}, 20);
}
this.dirty_canvas = true;
break;
case "number":
case "combo":
var old_value = w.value;
var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0;
var allow_scroll = true;
if (delta) {
if (x > -3 && x < widget_width + 3) {
allow_scroll = false;
}
}
if (allow_scroll && event.type == LiteGraph.pointerevents_method+"move" && w.type == "number") {
if(event.deltaX)
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) {
// check if v is a valid equation or a number
if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) {
try {//solve the equation if possible
v = eval(v);
} catch (e) { }
}
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) {
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) {
if(widget.type == "number"){
value = Number(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);
};
/**
* Determines the furthest nodes in each direction
* @param nodes {LGraphNode[]} the nodes to from which boundary nodes will be extracted
* @return {{left: LGraphNode, top: LGraphNode, right: LGraphNode, bottom: LGraphNode}}
*/
LGraphCanvas.getBoundaryNodes = function(nodes) {
let top = null;
let right = null;
let bottom = null;
let left = null;
for (const nID in nodes) {
const node = nodes[nID];
const [x, y] = node.pos;
const [width, height] = node.size;
if (top === null || y < top.pos[1]) {
top = node;
}
if (right === null || x + width > right.pos[0] + right.size[0]) {
right = node;
}
if (bottom === null || y + height > bottom.pos[1] + bottom.size[1]) {
bottom = node;
}
if (left === null || x < left.pos[0]) {
left = node;
}
}
return {
"top": top,
"right": right,
"bottom": bottom,
"left": left
};
}
/**
* Determines the furthest nodes in each direction for the currently selected nodes
* @return {{left: LGraphNode, top: LGraphNode, right: LGraphNode, bottom: LGraphNode}}
*/
LGraphCanvas.prototype.boundaryNodesForSelection = function() {
return LGraphCanvas.getBoundaryNodes(Object.values(this.selected_nodes));
}
/**
*
* @param {LGraphNode[]} nodes a list of nodes
* @param {"top"|"bottom"|"left"|"right"} direction Direction to align the nodes
* @param {LGraphNode?} align_to Node to align to (if null, align to the furthest node in the given direction)
*/
LGraphCanvas.alignNodes = function (nodes, direction, align_to) {
if (!nodes) {
return;
}
const canvas = LGraphCanvas.active_canvas;
let boundaryNodes = []
if (align_to === undefined) {
boundaryNodes = LGraphCanvas.getBoundaryNodes(nodes)
} else {
boundaryNodes = {
"top": align_to,
"right": align_to,
"bottom": align_to,
"left": align_to
}
}
for (const [_, node] of Object.entries(canvas.selected_nodes)) {
switch (direction) {
case "right":
node.pos[0] = boundaryNodes["right"].pos[0] + boundaryNodes["right"].size[0] - node.size[0];
break;
case "left":
node.pos[0] = boundaryNodes["left"].pos[0];
break;
case "top":
node.pos[1] = boundaryNodes["top"].pos[1];
break;
case "bottom":
node.pos[1] = boundaryNodes["bottom"].pos[1] + boundaryNodes["bottom"].size[1] - node.size[1];
break;
}
}
canvas.dirty_canvas = true;
canvas.dirty_bgcanvas = true;
};
LGraphCanvas.onNodeAlign = function(value, options, event, prev_menu, node) {
new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], {
event: event,
callback: inner_clicked,
parentMenu: prev_menu,
});
function inner_clicked(value) {
LGraphCanvas.alignNodes(LGraphCanvas.active_canvas.selected_nodes, value.toLowerCase(), node);
}
}
LGraphCanvas.onGroupAlign = function(value, options, event, prev_menu) {
new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], {
event: event,
callback: inner_clicked,
parentMenu: prev_menu,
});
function inner_clicked(value) {
LGraphCanvas.alignNodes(LGraphCanvas.active_canvas.selected_nodes, value.toLowerCase());
}
}
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;
value_element.select();
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
var 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();
}, typeof options.hide_on_mouse_leave === "number" ? options.hide_on_mouse_leave : 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(unescape(selected.dataset["type"]));
} else if (first) {
select(first);
} else {
dialog.close();
}
} else {
if (timeout) {
clearInterval(timeout);
}
timeout = setTimeout(refreshHelper, 10);
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 && (!ctor.title || ctor.title.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;
}
const nodeType = LiteGraph.registered_node_types[type];
if (nodeType?.title) {
help.innerText = nodeType?.title;
const typeEl = document.createElement("span");
typeEl.className = "litegraph lite-search-item-type";
typeEl.textContent = type;
help.append(typeEl);
} else {
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) {
var 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 = "&#10005;";
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();
}
if(root.parentNode)
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(var 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;
var 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>&#10005;</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>&#10005;</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 (Object.keys(this.selected_nodes).length > 1) {
options.push({
content: "Align",
has_submenu: true,
callback: LGraphCanvas.onGroupAlign,
})
}
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
},
{
content: "Properties Panel",
callback: function(item, options, e, menu, node) { LGraphCanvas.active_canvas.showShowNodePanel(node) }
},
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
});
if (Object.keys(this.selected_nodes).length > 1) {
options.push({
content: "Align Selected To",
has_submenu: true,
callback: LGraphCanvas.onNodeAlign,
})
}
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 (!disabled && 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] = clamp(x, 0, 1);
else
point[0] = s == 0 ? 0 : 1;
point[1] = 1.0 - 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);
}
}
function clamp(v, a, b) {
return a > v ? a : b < v ? b : v;
};
global.clamp = clamp;
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;
exports.LGraph = this.LGraph;
exports.LLink = this.LLink;
exports.LGraphNode = this.LGraphNode;
exports.LGraphGroup = this.LGraphGroup;
exports.DragAndScale = this.DragAndScale;
exports.LGraphCanvas = this.LGraphCanvas;
exports.ContextMenu = this.ContextMenu;
}