Files
ComfyUI_frontend/src/LGraph.ts
2024-09-27 09:45:43 +09:00

1489 lines
44 KiB
TypeScript

// @ts-nocheck
import { LiteGraph, LGraphCanvas, LGraphGroup, LLink, LGraphNode } from "./litegraph";
//*********************************************************************************
// 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]
*/
export class LGraph {
//default supported types
static supported_types = ["number", "string", "boolean"];
static STATUS_STOPPED = 1;
static STATUS_RUNNING = 2;
constructor(o) {
if (LiteGraph.debug) {
console.log("Graph created");
}
this.list_of_graphcanvas = null;
this.clear();
if (o) {
this.configure(o);
}
}
//used to know which types of connections support this graph (some graphs do not allow certain types)
getSupportedTypes() {
return this.supported_types || LGraph.supported_types;
}
/**
* Removes all nodes from this graph
* @method clear
*/
clear() {
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");
}
get nodes() {
return this._nodes;
}
get groups() {
return this._groups;
}
/**
* Attach Canvas to this graph
* @method attachCanvas
* @param {GraphCanvas} graph_canvas
*/
attachCanvas(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
*/
detachCanvas(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
*/
start(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
*/
stop() {
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)
*/
runStep(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
*/
updateExecutionOrder() {
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
computeExecutionOrder(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
*/
getAncestors(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
*/
arrange(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
*/
getTime() {
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
*/
getFixedTime() {
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
*/
getElapsedTime() {
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
*/
sendEventToAllNodes(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);
}
}
}
sendActionToCanvas(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
*/
add(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
*/
remove(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
*/
getNodeById(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
*/
findNodesByClass(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
*/
findNodesByType(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
*/
findNodeByTitle(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
*/
findNodesByTitle(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
*/
getNodeOnPos(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 | null} the group or null
*/
getGroupOnPos(x, y, { margin = 2 } = {}) {
return this._groups.reverse().find(g => g.isPointInside(x, y, margin, /* skip_title */ true));
}
/**
* 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
*/
checkNodeTypes() {
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 *****************
onAction(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;
}
}
trigger(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]
*/
addInput(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
*/
setInputData(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
*/
getInputData(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
*/
renameInput(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
*/
changeInputType(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
*/
removeInput(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
*/
addOutput(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
*/
setOutputData(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
*/
getOutputData(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
*/
renameOutput(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
*/
changeOutputType(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
*/
removeOutput(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;
}
triggerInput(name, value) {
var nodes = this.findNodesByTitle(name);
for (var i = 0; i < nodes.length; ++i) {
nodes[i].onTrigger(value);
}
}
setCallback(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
beforeChange(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
afterChange(info) {
if (this.onAfterChange) {
this.onAfterChange(this, info);
}
this.sendActionToCanvas("onAfterChange", this);
}
connectionChange(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
*/
isLive() {
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
*/
clearTriggeredSlots() {
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!) */
change() {
if (LiteGraph.debug) {
console.log("Graph changed");
}
this.sendActionToCanvas("setDirty", [true, true]);
if (this.on_change) {
this.on_change(this);
}
}
setDirtyCanvas(fg, bg) {
this.sendActionToCanvas("setDirty", [fg, bg]);
}
/**
* Destroys a link
* @method removeLink
* @param {Number} link_id
*/
removeLink(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
*/
serialize(option = { sortNodes: false }) {
var nodes_info = [];
nodes_info = (
option?.sortNodes ?
[...this._nodes].sort((a, b) => a.id - b.id) :
this._nodes
).map(node => node.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
*/
configure(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;
}
load(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);
};
}
onNodeTrace(node, msg, color) {
//TODO
}
}