mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 15:40:10 +00:00
1489 lines
44 KiB
TypeScript
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
|
|
}
|
|
}
|