Files
Trung0246-ComfyUI-0246/web/js/nodes.js
T
2023-11-25 03:46:33 -08:00

826 lines
25 KiB
JavaScript

import { app } from "../../../scripts/app.js";
import { api } from "../../../scripts/api.js";
import { ComfyDialog } from "../../../scripts/ui.js";
import { ComfyWidgets } from "../../../scripts/widgets.js";
(async () => {
async function try_import(name) {
try {
return await import(name);
} catch (e) {
return null;
}
}
function rgthree_exec(name, ...args) {
return rgthree_utils?.[name]?.(...args);
}
// Shamelessly imported :3
const rgthree_utils = await try_import("../../../extensions/rgthree-comfy/utils.js");
function error_popup(msg) {
let dialog = new ComfyDialog();
dialog.show(`<p>${msg}</p>`);
}
function hijack(obj, key, before_func, after_func) {
const old_func = obj[key] ?? (() => {});
obj[key] = function () {
const self = this;
const pass_obj = {
self: self,
stop: false,
pre: before_func,
post: after_func,
old: old_func
};
before_func.apply(pass_obj, arguments);
if (pass_obj.old === old_func && !pass_obj.stop)
pass_obj.res = pass_obj.old.apply(this, arguments);
after_func.apply(pass_obj, arguments);
return pass_obj.res;
};
}
async function async_hijack(obj, key, before_func, after_func) {
const old_func = obj[key] ?? (() => {});
obj[key] = async function () {
const self = this;
const pass_obj = {
self: self,
stop: false,
pre: before_func,
post: after_func,
old: old_func
};
await before_func.apply(pass_obj, arguments);
if (pass_obj.old === old_func && !pass_obj.stop)
pass_obj.res = await pass_obj.old.apply(this, arguments);
await after_func.apply(pass_obj, arguments);
return pass_obj.res;
};
}
function remove_elem_arr(array, isValid) {
let shift = 0;
for (let i = 0; i < array.length; ++ i) {
if (isValid(array[i]))
++ shift;
else if (shift > 0)
array[i - shift] = array[i];
}
array.length -= shift;
}
function random_id() {
return Date.now().toString(36) + Math.random().toString(36).substring(2);
}
function indent_str(strings, ...values) {
// Build the string as normal, combining all parts
let fullString = strings.reduce((acc, str, i) => acc + (values[i - 1] || '') + str);
// Split the string into lines
let lines = fullString.split('\n');
// Remove the first line if it is empty (caused by a newline at the beginning of a template literal)
if (lines[0].match(/^\s*$/))
lines.shift();
// Find the smallest indentation (spaces or tabs) from the remaining lines that have content
const smallestIndent = lines.length > 0
? Math.min(...lines.filter(line => line.trim()).map(line => line.match(/^[ \t]*/)[0].length))
: 0;
// Remove the smallest indentation from all lines
lines = lines.map(line => line.substring(smallestIndent));
// Combine the trimmed lines
return lines.join('\n');
}
/////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////
function init_type(node) {
node.addCustomWidget({
name: "_type",
computeSize: () => [0, -4],
async serializeValue (node, index_str) {
return serialize_type(node);
}
});
}
async function init_update_raw(node, widget, callback) {
if (node.__update || !node.__hash_update)
node.__hash_update = random_id();
node.__update = false;
const temp = {
data: widget.value,
update: node.__hash_update,
};
if (callback)
await callback(node, widget, temp);
return temp;
}
function init_update(node, name) {
node.__update = false;
for (let i = 0; i < node.widgets.length; ++ i) {
if (node.widgets[i].name === name) {
node.widgets[i].serializeValue = async function (inner_node, index_str) {
return await init_update_raw(node, node.widgets[i]);
};
return;
}
}
}
function init_update_direct(node, name, callback) {
node.__update = false;
node.addCustomWidget({
name: name,
computeSize: () => [0, -4],
async serializeValue (inner_node, index_str) {
return await init_update_raw(node, this, callback);
}
});
}
function serialize_type(node) {
let data = {
in: [],
out: []
}
if (node.inputs)
for (let i = 0; i < node.inputs.length; ++ i) {
if (BLACKLIST.includes(node.inputs[i].name))
continue;
data.in.push({
name: node.inputs[i].orig_name,
full_name: node.inputs[i].name,
type: node.inputs[i].type,
});
}
if (node.outputs)
for (let i = 0; i < node.outputs.length; ++ i) {
if (BLACKLIST.includes(node.outputs[i].name))
continue;
data.out.push({
name: node.outputs[i].orig_name,
full_name: node.outputs[i].name,
type: node.outputs[i].type,
});
}
return data;
}
function link_shift_up(node, arr, index, flag, link_callback) {
// Disconnect event handler
const old_func = node.onConnectionsChange;
node.onConnectionsChange = null;
const old_in_func = node.onConnectInput;
node.onConnectInput = null;
const old_out_func = node.onConnectOutput;
node.onConnectOutput = null;
// Shift up all links
if (flag) {
if (arr[index].links.length === 0) {
node.removeOutput(index);
for (let i = 0, c = 0; i < arr.length; ++ i) {
if (BLACKLIST.includes(arr[i].name))
continue;
arr[i].name = `${arr[i].type}:${c}`;
++ c;
}
}
} else {
node.removeInput(index);
for (let i = 0, c = 0; i < arr.length; ++ i) {
if (BLACKLIST.includes(arr[i].name))
continue;
arr[i].name = `${c}:${arr[i].type}`;
++ c;
}
}
// Revert things back
node.onConnectionsChange = old_func;
node.onConnectInput = old_in_func;
node.onConnectOutput = old_out_func;
return;
}
const BLACKLIST = [
"_way_in",
"_way_out",
"_junc_in",
"_junc_out",
"_query",
"_offset",
// "_node_head",
// "_node_tail",
"_node",
"_event",
"..."
];
const LEGACY_BLACKLIST = {
prev: ["_pipe_in", "_pipe_out"],
next: ["_way_in", "_way_out"],
};
/////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////
function setup_sole_pin(node, name, side_name, side_mode, shape) {
const upper_name = side_name.charAt(0).toUpperCase() + side_name.slice(1),
more_name = side_name + "s";
hijack(node, "onConnect" + upper_name, () => {}, function () {
if (this.self[more_name][arguments[0]].name !== name)
return true;
this.self[more_name][arguments[0]].shape = shape;
this.self[more_name][arguments[0]].type = arguments[2].type;
});
hijack(node, "onConnectionsChange", () => {}, function (type, index, connected, link_info) {
if (link_info === null) {
// Clean up when copy paste or template load
if (this.self[more_name])
for (let i = 0; i < this.self[more_name].length; ++ i)
if (this.self[more_name][i].name === name) {
this.self[more_name][i].type = "*";
break;
}
return;
}
if (!connected && index < this.self[more_name].length && this.self[more_name][index].name === name && type === side_mode)
this.self[more_name][index].type = "*";
});
}
function setup_expand(node, name, real, pin, shape, callback) {
const upper_name = name.charAt(0).toUpperCase() + name.slice(1),
more_name = name + "s";
node["add" + upper_name](pin, "*");
if (node[more_name])
for (let i = 0; i < node[more_name].length; ++ i)
if (node[more_name][i].name === pin) {
node[more_name][i].shape = shape;
break;
}
hijack(node, "configure", () => {}, function (data) {
if (this.self[more_name])
for (let i = 0; i < this.self[more_name].length; ++ i) {
if (!BLACKLIST.includes(this.self[more_name][i].name))
++ real[name];
}
});
node["onConnect" + upper_name] = function (
this_slot_index,
other_slot_type,
other_slot_obj,
other_node,
other_slot_index
) {
this.__update = true;
if (
BLACKLIST.includes(this[more_name][this_slot_index].name) &&
this[more_name][this_slot_index].name !== pin
)
return true;
if (callback.apply({ self: this, name: name, mode: 0 }, arguments))
return true;
callback.call({ self: this, name: name, mode: 1 }, real[name] ++, this[more_name][this_slot_index].type, this_slot_index);
this["add" + upper_name](pin, "*");
this[more_name][this[more_name].length - 1].shape = shape;
return true;
};
hijack(node, "onConnectionsChange", () => {}, function (type, index, connected, link_info) {
if (link_info === null) {
// Clean up when copy paste or template load
if (this.self[more_name])
remove_elem_arr(this.self[more_name], (e) => !BLACKLIST.includes(e.name));
this.self.computeSize();
return;
}
if (!connected)
callback.apply({ self: this.self, name: name, mode: 2 }, arguments);
});
}
function junction_impl(nodeType, nodeData, app, name, shape_in, shape_out) {
nodeType.prototype.onNodeCreated = function () {
init_type(this);
if (typeof name === "string")
init_update(this, name);
const real = {
input: 0,
output: 0,
};
setup_expand(this, "input", real, "...", shape_in, function () {
switch (this.mode) {
case 0: {
if (this.self.inputs[arguments[0]].link !== null) {
app.graph.links[this.self.inputs[arguments[0]].link].replaced = true;
return true;
}
this.self.inputs[arguments[0]].type = arguments[2].type;
} break;
case 1: {
this.self.inputs[arguments[2]].name = `${arguments[0]}:${arguments[1]}`;
} break;
case 2: {
if (arguments[0] === 1) {
let link_info = arguments[3];
if (BLACKLIST.includes(this.self.inputs[link_info.target_slot].name) || link_info.replaced)
return;
link_shift_up(this.self, this.self.inputs, link_info.target_slot, false, (link_index, extra_link_index) => {
return this.self.inputs[link_index].link;
});
-- real.input;
}
} break;
}
});
setup_expand(this, "output", real, "...", shape_out, function () {
switch (this.mode) {
case 0: {
if (this.self.outputs[arguments[0]].links && this.self.outputs[arguments[0]].links.length > 0)
return true;
if (arguments[2].__outputType) // Reroute
this.self.outputs[arguments[0]].type = arguments[2].__outputType;
// else if (arguments[2].defaultConnectionsLayout) // Reroute (rgthree)
// this.self.outputs[arguments[0]].type = arguments[2].type;
else
this.self.outputs[arguments[0]].type = arguments[2].type;
} break;
case 1: {
this.self.outputs[arguments[2]].name = `${arguments[1]}:${arguments[0]}`;
} break;
case 2: {
if (arguments[0] === 2) {
let link_info = arguments[3];
if (BLACKLIST.includes(this.self.outputs[link_info.origin_slot].name))
return;
if (!this.self.outputs[link_info.origin_slot].links || this.self.outputs[link_info.origin_slot].links.length === 0) {
link_shift_up(this.self, this.self.outputs, link_info.origin_slot, true, (link_index, extra_link_index) => {
return this.self.outputs[link_index].links[extra_link_index];
});
-- real.output;
}
}
} break;
}
});
};
rgthree_exec("addHelp", nodeType, app);
rgthree_exec("addConnectionLayoutSupport", nodeType, app);
nodeType.help = indent_str `
_offset is used to skip data ahead for specific type (since internally it's a sequence of data).
_offset is persistent and will retains information across linked Junction.
The _offset syntax goes as follow:
type,1
- type is the type (usually LATENT, MODEL, VAE, etc.) and 1 is the index being set.
type,+2
- Same as above but instead of set offset, it increase the offset instead.
type,-2
- Decrease offset.
type1, -1; type2, +2; type3, 4
- Multiple offset.
`;
}
function setup_loop_update(node) {
for (let i = 0; i < node.widgets.length; ++ i) {
if (node.widgets[i].name === "_update") {
node.widgets.splice(i, 1);
init_update_direct(node, "_update");
break;
}
}
}
function single_impl(nodeType, nodeData, app, shape_in, pin_list) {
nodeType.prototype.onNodeCreated = function () {
init_type(this);
const real = {
input: 0,
};
setup_loop_update(this);
if (shape_in !== null)
setup_expand(this, "input", real, "...", shape_in, function () {
switch (this.mode) {
case 0: {
if (this.self.inputs[arguments[0]].link !== null) {
app.graph.links[this.self.inputs[arguments[0]].link].replaced = true;
return true;
}
this.self.inputs[arguments[0]].type = arguments[2].type;
} break;
case 1: {
this.self.inputs[arguments[2]].name = `${arguments[0]}:${arguments[1]}`;
} break;
case 2: {
if (arguments[0] === 1) {
let link_info = arguments[3];
if (BLACKLIST.includes(this.self.inputs[link_info.target_slot].name) || link_info.replaced)
return;
link_shift_up(this.self, this.self.inputs, link_info.target_slot, false, (link_index, extra_link_index) => {
return this.self.inputs[link_index].link;
});
-- real.input;
}
} break;
}
});
for (let i = 0; i < pin_list.length; i += 4)
setup_sole_pin(this, pin_list[i], pin_list[i + 1], pin_list[i + 2], pin_list[i + 3]);
}
}
function raw_setup_log(self) {
self.log_widget = ComfyWidgets["STRING"](self, "output", ["STRING", { multiline: true }], app).widget;
self.log_widget.inputEl.readOnly = true;
self.log_widget.serializeValue = async (node, index) => {
node.widgets_values[index] = "";
return "";
};
}
function setup_log(nodeType) {
hijack(nodeType.prototype, "onNodeCreated", () => {}, function () {
raw_setup_log(this.self);
});
hijack(nodeType.prototype, "onExecuted", () => {}, function (message) {
this.self.log_widget.value = message.text[0];
});
}
app.registerExtension({
name: "0246.Node",
nodeCreated(node) {
switch (node.comfyClass) {
case "Highway": {
node.color = LGraphCanvas.node_colors.brown.color;
node.bgcolor = LGraphCanvas.node_colors.brown.bgcolor;
} break;
case "Junction": {
node.color = LGraphCanvas.node_colors.blue.color;
node.bgcolor = LGraphCanvas.node_colors.blue.bgcolor;
} break;
case "JunctionBatch": {
node.color = LGraphCanvas.node_colors.blue.color;
node.bgcolor = LGraphCanvas.node_colors.pale_blue.bgcolor;
} break;
case "Mimic": {
node.color = LGraphCanvas.node_colors.yellow.color;
node.bgcolor = LGraphCanvas.node_colors.red.bgcolor;
} break;
case "Loop": {
node.color = LGraphCanvas.node_colors.yellow.color;
node.bgcolor = LGraphCanvas.node_colors.yellow.bgcolor;
} break;
case "Count": {
node.color = LGraphCanvas.node_colors.yellow.color;
node.bgcolor = LGraphCanvas.node_colors.green.bgcolor;
} break;
case "Hold": {
node.color = LGraphCanvas.node_colors.yellow.color;
node.bgcolor = LGraphCanvas.node_colors.pale_blue.bgcolor;
} break;
case "Beautify": {
node.color = "#652069";
node.bgcolor = "#764378";
} break;
case "Random": {
node.color = LGraphCanvas.node_colors.green.color;
} break;
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeData.category === "0246") {
switch (nodeData.name) {
case "Highway": {
nodeType.prototype.onNodeMoved = function () {};
nodeType.prototype.onNodeCreated = function () {
hijack(this, "configure", () => {}, function (data) {
// Patch legacy nodes
for (let i = 0; i < this.self.inputs.length; ++ i) {
if (LEGACY_BLACKLIST.prev.includes(this.self.inputs[i].name))
this.self.inputs[i].name = LEGACY_BLACKLIST.next[i];
}
for (let i = 0; i < this.self.outputs.length; ++ i) {
if (LEGACY_BLACKLIST.prev.includes(this.self.outputs[i].name))
this.self.outputs[i].name = LEGACY_BLACKLIST.next[i];
}
});
let last_query = "";
init_type(this);
init_update(this, "_query");
this.addWidget("button", "Update", null, () => {
const query = this.widgets.find(w => w.name === "_query");
// POST query to server
fetch(window.location.origin + "/0246-parse", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
"input": query.value,
}),
}).then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error("Network response was not ok.");
}
}).then(data => {
if (data.error.length > 0) {
error_popup(data.error.join("\n"));
query.value = last_query;
return;
}
this.__update = true;
last_query = query.value;
let prev = [];
// Save previous inputs and outputs
for (let i = 0; i < this.inputs.length; ++ i) {
if (!BLACKLIST.includes(this.inputs[i].name) && this.inputs[i].link !== null)
prev.push({
flag: false,
name: this.inputs[i].orig_name,
node_id: app.graph.links[this.inputs[i].link].origin_id,
slot_id: app.graph.links[this.inputs[i].link].origin_slot,
});
}
for (let i = 0; i < this.outputs.length; ++ i) {
if (!BLACKLIST.includes(this.outputs[i].name) && this.outputs[i].links !== null)
for (let j = 0; j < this.outputs[i].links.length; ++ j)
prev.push({
flag: true,
name: this.outputs[i].orig_name,
node_id: app.graph.links[this.outputs[i].links[j]].target_id,
slot_id: app.graph.links[this.outputs[i].links[j]].target_slot,
});
}
// Clear all inputs and outputs except any that in BLACKLIST
for (let i = this.inputs.length; i -- > 0;) {
if (!BLACKLIST.includes(this.inputs[i].name))
this.removeInput(i);
}
for (let i = this.outputs.length; i -- > 0;) {
if (!BLACKLIST.includes(this.outputs[i].name))
this.removeOutput(i);
}
// Add new inputs and outputs
for (let i = 0; i < data.order.length; ++ i) {
switch (data.order[i][0]) {
case "set": {
this.addInput(`+${data.order[i][1]}`, "*");
} break;
case "get":{
this.addOutput(`-${data.order[i][1]}`, "*");
} break;
case "eat": {
this.addOutput(`!${data.order[i][1]}`, "*");
} break;
}
}
for (let i = 0; i < this.inputs.length; ++ i)
this.inputs[i].orig_name = this.inputs[i].name;
for (let i = 0; i < this.outputs.length; ++ i)
this.outputs[i].orig_name = this.outputs[i].name;
// Restore previous inputs and outputs
for (let i = 0; i < prev.length; ++ i) {
// Check if input/output still exists
if (prev[i].flag) {
for (let j = 0; j < this.outputs.length; ++ j) {
if (this.outputs[j].orig_name === prev[i].name) {
this.connect(
j,
prev[i].node_id,
prev[i].slot_id
);
break;
}
}
} else {
for (let j = 0; j < this.inputs.length; ++ j) {
if (this.inputs[j].orig_name === prev[i].name) {
app.graph.getNodeById(prev[i].node_id).connect(
prev[i].slot_id,
this,
j
);
break;
}
}
}
}
});
});
this.onConnectInput = function (
this_target_slot_index,
other_origin_slot_type,
other_origin_slot_obj,
other_origin_node,
other_origin_slot_index
) {
this.__update = true;
if (BLACKLIST.includes(this.inputs[this_target_slot_index].name))
return true;
if (this.inputs[this_target_slot_index].link !== null) {
// Prevent premature link kill
app.graph.links[this.inputs[this_target_slot_index].link].replaced = true;
return true;
}
let curr_pin = this.inputs[this_target_slot_index];
curr_pin.type = other_origin_slot_obj.type;
curr_pin.name = `${curr_pin.orig_name}:${curr_pin.type}`;
return true;
};
this.onConnectOutput = function (
this_origin_slot_index,
other_target_slot_type,
other_target_slot_obj,
other_target_node,
other_target_slot_index
) {
// We detect if we're connecting to Reroute here by checking other_target_node.type === "Reroute"
// return false for not allowing connection
this.__update = true;
if (BLACKLIST.includes(this.outputs[this_origin_slot_index].name))
return true;
let curr_pin = this.outputs[this_origin_slot_index];
if (other_target_node.__outputType) // Reroute
curr_pin.type = other_target_node.__outputType;
else if (other_target_node.defaultConnectionsLayout) // Reroute (rgthree)
// rgthree accept this anyways so whatever since too lazy to properly do graph traversal
// EDIT: I was wrong, I have to do it, but not here :(
curr_pin.type = other_target_slot_obj.type;
else
curr_pin.type = other_target_slot_obj.type;
curr_pin.name = `${curr_pin.type}:${curr_pin.orig_name}`;
return true;
};
this.onConnectionsChange = function (type, index, connected, link_info) {
if (link_info === null) {
// Clean up when copy paste or template load
for (let i = 0; i < this.inputs.length; ++ i)
if (!BLACKLIST.includes(this.inputs[i].name)) {
this.inputs[i].name = this.inputs[i].orig_name;
this.inputs[i].type = "*";
}
for (let i = 0; i < this.outputs.length; ++ i)
if (!BLACKLIST.includes(this.outputs[i].name)) {
this.outputs[i].name = this.outputs[i].orig_name;
this.outputs[i].type = "*";
}
this.computeSize();
return;
}
if (!connected) {
switch (type) {
case 1: {
if (BLACKLIST.includes(this.inputs[link_info.target_slot].name) || link_info.replaced)
return;
this.inputs[link_info.target_slot].name = this.inputs[link_info.target_slot].orig_name;
this.inputs[link_info.target_slot].type = "*";
} break;
case 2: {
if (this.outputs[link_info.origin_slot].links.length === 0 && !BLACKLIST.includes(this.outputs[link_info.origin_slot].name)) {
this.outputs[link_info.origin_slot].name = this.outputs[link_info.origin_slot].orig_name;
this.outputs[link_info.origin_slot].type = "*";
}
} break;
default: {
throw new Error("Unsuported type: " + type);
}
}
}
};
};
rgthree_exec("addHelp", nodeType, app);
rgthree_exec("addConnectionLayoutSupport", nodeType, app);
nodeType.help = indent_str `
The _query syntax goes as follow:
>name
- Input variable.
<name
- Output variable.
>\`n!ce n@me\`
- Input variable but with special character and spaces (except \`, obviously).
!name
- Output variable, but also delete itself, preventing from being referenced further.
- CURRENTLY BROKEN DUE TO HOW COMFYUI UPDATE THE NODES.
<name1; >name2; !name3
- Multiple input and outputs together.
`;
} break;
case "Junction": {
junction_impl(nodeType, nodeData, app, "_offset", LiteGraph.CIRCLE_SHAPE, LiteGraph.CIRCLE_SHAPE);
} break;
case "JunctionBatch": {
// [TODO] Dynamically change shape when _mode is changed
junction_impl(nodeType, nodeData, app, "_offset", 6, 6);
} break;
case "Loop": {
single_impl(nodeType, nodeData, app, LiteGraph.CIRCLE_SHAPE, []);
} break;
case "Count": {
single_impl(nodeType, nodeData, app, null, [
"_node", "input", 1, LiteGraph.CIRCLE_SHAPE
]);
setup_log(nodeType);
} break;
case "Hold": {
single_impl(nodeType, nodeData, app, null, [
"_data_in", "input", 1, LiteGraph.CIRCLE_SHAPE,
"_data_out", "output", 2, 6
]);
setup_log(nodeType);
} break;
case "Random": {
setup_log(nodeType);
} break;
case "Beautify": {
setup_log(nodeType);
} break;
}
}
},
});
// var curr_node = LiteGraph.createNode(LiteGraph.getNodeTypesInCategory(LiteGraph.getNodeTypesCategories()[2])[2].type);
})();