mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-02-11 05:52:33 +08:00
wip group nodes
This commit is contained in:
parent
c6df6d642a
commit
f40f910801
375
web/extensions/core/groupNode.js
Normal file
375
web/extensions/core/groupNode.js
Normal file
@ -0,0 +1,375 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { getWidgetType } from "../../scripts/widgets.js";
|
||||
|
||||
const IS_GROUP_NODE = Symbol();
|
||||
const GROUP_DATA = Symbol();
|
||||
const GROUP_SLOTS = Symbol();
|
||||
|
||||
function getLinks(config) {
|
||||
const linksFrom = {};
|
||||
const linksTo = {};
|
||||
|
||||
// Extract links for easy lookup
|
||||
for (const l of config.links) {
|
||||
const [outputNodeId, outputNodeSlot, inputNodeId, inputNodeSlot] = l;
|
||||
|
||||
// Skip links outside the copy config
|
||||
if (outputNodeId == null) continue;
|
||||
|
||||
if (!linksFrom[outputNodeId]) {
|
||||
linksFrom[outputNodeId] = {};
|
||||
}
|
||||
linksFrom[outputNodeId][outputNodeSlot] = l;
|
||||
|
||||
if (!linksTo[inputNodeId]) {
|
||||
linksTo[inputNodeId] = {};
|
||||
}
|
||||
linksTo[inputNodeId][inputNodeSlot] = l;
|
||||
}
|
||||
return { linksTo, linksFrom };
|
||||
}
|
||||
|
||||
// function getInnerLinkType(config, link) {
|
||||
// const [outputNodeId, outputNodeSlot] = link;
|
||||
// return config.nodes[outputNodeId].outputs[outputNodeSlot].type;
|
||||
// }
|
||||
|
||||
function buildNodeDef(config, nodeName, defs, workflow) {
|
||||
const slots = {
|
||||
inputs: {},
|
||||
widgets: {},
|
||||
outputs: {},
|
||||
};
|
||||
|
||||
const newDef = {
|
||||
output: [],
|
||||
output_name: [],
|
||||
output_is_list: [],
|
||||
name: nodeName,
|
||||
display_name: nodeName,
|
||||
category: "group nodes" + (workflow ? "/workflow" : ""),
|
||||
input: { required: {} },
|
||||
|
||||
[IS_GROUP_NODE]: true,
|
||||
[GROUP_DATA]: config,
|
||||
[GROUP_SLOTS]: slots,
|
||||
};
|
||||
const links = getLinks(config);
|
||||
|
||||
console.log(
|
||||
"Building group node",
|
||||
nodeName,
|
||||
config.nodes.map((n) => n.type)
|
||||
);
|
||||
|
||||
let inputCount = 0;
|
||||
for (let nodeId = 0; nodeId < config.nodes.length; nodeId++) {
|
||||
const node = config.nodes[nodeId];
|
||||
console.log("Processing inner node", nodeId, node.type);
|
||||
let def = defs[node.type];
|
||||
|
||||
const linksTo = links.linksTo[nodeId];
|
||||
const linksFrom = links.linksFrom[nodeId];
|
||||
|
||||
if (!def) {
|
||||
// Special handling for reroutes to allow them to be used as inputs/outputs
|
||||
if (node.type === "Reroute") {
|
||||
if (linksTo && linksFrom) {
|
||||
// Being used internally
|
||||
// TODO: does anything actually need doing here?
|
||||
continue;
|
||||
}
|
||||
|
||||
let rerouteType;
|
||||
if (linksFrom) {
|
||||
const [, , id, slot] = linksFrom["0"];
|
||||
rerouteType = config.nodes[id].inputs[slot].type;
|
||||
} else {
|
||||
const [id, slot] = linksTo["0"];
|
||||
rerouteType = config.nodes[id].outputs[slot].type;
|
||||
}
|
||||
|
||||
def = {
|
||||
input: {
|
||||
required: {
|
||||
[rerouteType]: [rerouteType, {}],
|
||||
},
|
||||
},
|
||||
output: [rerouteType],
|
||||
output_name: [],
|
||||
output_is_list: [],
|
||||
};
|
||||
} else {
|
||||
// Front end only node
|
||||
// TODO: check these should all be ignored
|
||||
debugger;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const inputs = { ...def.input?.required, ...def.input?.optional };
|
||||
|
||||
// Add inputs / widgets
|
||||
const inputNames = Object.keys(inputs);
|
||||
let linkInputId = 0;
|
||||
for (let inputId = 0; inputId < inputNames.length; inputId++) {
|
||||
const inputName = inputNames[inputId];
|
||||
console.log("\t", "> Processing input", inputId, inputName);
|
||||
const widgetType = getWidgetType(inputs[inputName], inputName);
|
||||
let name = nodeId + ":" + inputName;
|
||||
if (widgetType) {
|
||||
console.log("\t\t", "Widget", widgetType);
|
||||
|
||||
// Store mapping to get a group widget name from an inner id + name
|
||||
if (!slots.widgets[nodeId]) slots.widgets[nodeId] = {};
|
||||
slots.widgets[nodeId][inputName] = name;
|
||||
} else {
|
||||
if (linksTo?.[linkInputId]) {
|
||||
linkInputId++;
|
||||
console.info("\t\t", "Link skipped as has internal connection");
|
||||
continue;
|
||||
}
|
||||
|
||||
console.info("\t\t", "Link", linkInputId + " -> outer input " + inputCount);
|
||||
|
||||
// Store a mapping to let us get the group node input for a specific slot on an inner node
|
||||
if (!slots.inputs[nodeId]) slots.inputs[nodeId] = {};
|
||||
slots.inputs[nodeId][linkInputId++] = inputCount++;
|
||||
}
|
||||
|
||||
let inputDef = inputs[inputName];
|
||||
if (inputName === "seed" || inputName === "noise_seed") {
|
||||
inputDef = [...inputDef];
|
||||
inputDef[1] = { control_after_generate: true, ...inputDef[1] };
|
||||
}
|
||||
newDef.input.required[name] = inputDef;
|
||||
}
|
||||
|
||||
// Add outputs
|
||||
for (let outputId = 0; outputId < def.output.length; outputId++) {
|
||||
console.log("\t", "< Processing output", outputId, def.output_name?.[outputId] ?? def.output[outputId]);
|
||||
|
||||
if (linksFrom?.[outputId]) {
|
||||
console.info("\t\t", "Skipping as has internal connection");
|
||||
continue;
|
||||
}
|
||||
|
||||
slots.outputs[newDef.output.length] = {
|
||||
node: nodeId,
|
||||
slot: outputId,
|
||||
};
|
||||
|
||||
newDef.output.push(def.output[outputId]);
|
||||
newDef.output_is_list.push(def.output_is_list[outputId]);
|
||||
newDef.output_name.push(nodeId + ":" + (def.output_name?.[outputId] ?? def.output[outputId]));
|
||||
}
|
||||
}
|
||||
|
||||
return newDef;
|
||||
}
|
||||
|
||||
const id = "Comfy.GroupNode";
|
||||
let globalDefs;
|
||||
const ext = {
|
||||
name: id,
|
||||
setup() {
|
||||
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
|
||||
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
||||
const options = orig.apply(this, arguments);
|
||||
|
||||
options.push(null);
|
||||
options.push({
|
||||
content: `Convert to Group Node`,
|
||||
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
||||
callback: async () => {
|
||||
const name = prompt("Enter group name");
|
||||
if (!name) return;
|
||||
|
||||
let extra = app.graph.extra;
|
||||
if (!extra) app.graph.extra = extra = {};
|
||||
let groupNodes = extra.groupNodes;
|
||||
if (!groupNodes) extra.groupNodes = groupNodes = {};
|
||||
|
||||
if (name in groupNodes) {
|
||||
if (app.graph._nodes.find((n) => n.type === name)) {
|
||||
alert(
|
||||
"An in use group node with this name already exists embedded in this workflow, please remove any instances or use a new name."
|
||||
);
|
||||
return;
|
||||
} else if (
|
||||
!confirm(
|
||||
"An group node with this name already exists embedded in this workflow, are you sure you want to overwrite it?"
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Use the built in copyToClipboard function to generate the node data we need
|
||||
const backup = localStorage.getItem("litegrapheditor_clipboard");
|
||||
app.canvas.copyToClipboard();
|
||||
const config = JSON.parse(localStorage.getItem("litegrapheditor_clipboard"));
|
||||
localStorage.setItem("litegrapheditor_clipboard", backup);
|
||||
const def = buildNodeDef(config, name, globalDefs, true);
|
||||
await app.registerNodeDef("workflow/" + name, def);
|
||||
|
||||
groupNodes[name] = config;
|
||||
|
||||
let top;
|
||||
let left;
|
||||
for (const id in app.canvas.selected_nodes) {
|
||||
const node = app.graph.getNodeById(id);
|
||||
if (left == null || node.pos[0] < left) {
|
||||
left = node.pos[0];
|
||||
}
|
||||
if (top == null || node.pos[1] < top) {
|
||||
top = node.pos[1];
|
||||
}
|
||||
app.graph.remove(node);
|
||||
}
|
||||
|
||||
const newNode = LiteGraph.createNode("workflow/" + name);
|
||||
newNode.pos = [left, top];
|
||||
app.graph.add(newNode);
|
||||
},
|
||||
});
|
||||
|
||||
return options;
|
||||
};
|
||||
},
|
||||
async beforeConfigureGraph(graphData) {
|
||||
const groupNodes = graphData?.extra?.groupNodes;
|
||||
if (!groupNodes) return;
|
||||
|
||||
for (const name in groupNodes) {
|
||||
const def = buildNodeDef(groupNodes[name], name, globalDefs, true);
|
||||
await app.registerNodeDef("workflow/" + name, def);
|
||||
}
|
||||
},
|
||||
addCustomNodeDefs(defs) {
|
||||
globalDefs = defs;
|
||||
},
|
||||
nodeCreated(node) {
|
||||
if (node.constructor.nodeData?.[IS_GROUP_NODE]) {
|
||||
const config = node.constructor.nodeData[GROUP_DATA];
|
||||
const slots = node.constructor.nodeData[GROUP_SLOTS];
|
||||
|
||||
const onNodeCreated = node.onNodeCreated;
|
||||
node.onNodeCreated = function () {
|
||||
for (let innerNodeId = 0; innerNodeId < config.nodes.length; innerNodeId++) {
|
||||
const values = config.nodes[innerNodeId].widgets_values;
|
||||
if (!values) continue;
|
||||
const widgets = slots.widgets?.[innerNodeId];
|
||||
if (!widgets) continue;
|
||||
|
||||
const names = Object.values(widgets);
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
if (values[i] == null) continue;
|
||||
const widget = this.widgets.find((w) => w.name === names[i]);
|
||||
if (widget) {
|
||||
widget.value = values[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return onNodeCreated?.apply(this, arguments);
|
||||
};
|
||||
|
||||
node.updateLink = function (link, innerNodes) {
|
||||
// Replace the group node reference with the internal node
|
||||
link = { ...link };
|
||||
const output = slots.outputs[link.origin_slot];
|
||||
let innerNode = this.innerNodes[output.node];
|
||||
let l;
|
||||
while (innerNode.type === "Reroute") {
|
||||
l = innerNode.getInputLink(0);
|
||||
innerNode = innerNode.getInputNode(0);
|
||||
}
|
||||
|
||||
link.origin_id = innerNode.id;
|
||||
link.origin_slot = l?.origin_slot ?? output.slot;
|
||||
return link;
|
||||
};
|
||||
|
||||
node.getInnerNodes = function () {
|
||||
console.log("Expanding group node", this.comfyClass, this.id);
|
||||
const links = getLinks(config);
|
||||
|
||||
const innerNodes = config.nodes.map((n, i) => {
|
||||
const innerNode = LiteGraph.createNode(n.type);
|
||||
innerNode.configure(n);
|
||||
|
||||
for (const innerWidget of innerNode.widgets ?? []) {
|
||||
const groupWidgetName = slots.widgets[i][innerWidget.name];
|
||||
const groupWidget = node.widgets.find((w) => w.name === groupWidgetName);
|
||||
if (groupWidget) {
|
||||
console.log("Set widget value", groupWidgetName + " -> " + innerWidget.name, groupWidget.value);
|
||||
innerWidget.value = groupWidget.value;
|
||||
}
|
||||
}
|
||||
|
||||
innerNode.id = node.id + ":" + i;
|
||||
innerNode.getInputNode = function (slot) {
|
||||
if (!innerNode.comfyClass) slot = 0;
|
||||
console.log("Get input node", innerNode.comfyClass, slot, innerNode.inputs[slot]?.name);
|
||||
const outerSlot = slots.inputs[i]?.[slot];
|
||||
if (outerSlot != null) {
|
||||
// Our inner node has a mapping to the group node inputs
|
||||
// return the input node from there
|
||||
console.log("\t", "Getting from group node input", outerSlot);
|
||||
const inputNode = node.getInputNode(outerSlot);
|
||||
console.log("\t", "Result", inputNode?.id, inputNode?.comfyClass);
|
||||
return inputNode;
|
||||
}
|
||||
|
||||
// Internal link
|
||||
const innerLink = links.linksTo[i][slot];
|
||||
console.log("\t", "Internal link", innerLink);
|
||||
const inputNode = innerNodes[innerLink[0]];
|
||||
console.log("\t", "Result", inputNode?.id, inputNode?.comfyClass);
|
||||
return inputNode;
|
||||
};
|
||||
innerNode.getInputLink = function (slot) {
|
||||
console.log("Get input link", innerNode.comfyClass, slot, innerNode.inputs[slot]?.name);
|
||||
const outerSlot = slots.inputs[i]?.[slot];
|
||||
if (outerSlot != null) {
|
||||
// The inner node is connected via the group node inputs
|
||||
console.log("\t", "Getting from group node input", outerSlot);
|
||||
const linkId = node.inputs[outerSlot].link;
|
||||
let link = app.graph.links[linkId];
|
||||
|
||||
// Use the outer link, but update the target to the inner node
|
||||
link = {
|
||||
target_id: innerNode.id,
|
||||
target_slot: slot,
|
||||
...link,
|
||||
};
|
||||
console.log("\t", "Result", link);
|
||||
return link;
|
||||
}
|
||||
|
||||
let link = links.linksTo[i][slot];
|
||||
// Use the inner link, but update the origin node to be inner node id
|
||||
link = {
|
||||
origin_id: node.id + ":" + link[0],
|
||||
origin_slot: link[1],
|
||||
target_id: node.id + ":" + i,
|
||||
target_slot: slot,
|
||||
};
|
||||
console.log("\t", "Internal link", link);
|
||||
|
||||
return link;
|
||||
};
|
||||
|
||||
return innerNode;
|
||||
});
|
||||
|
||||
this.innerNodes = innerNodes;
|
||||
|
||||
return innerNodes;
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
app.registerExtension(ext);
|
||||
@ -416,7 +416,7 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
|
||||
if (widget.type === "number" || widget.type === "combo") {
|
||||
if ((widget.type === "number" && !inputData?.[1]?.control_after_generate) || widget.type === "combo") {
|
||||
addValueControlWidget(this, widget, "fixed");
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ComfyLogging } from "./logging.js";
|
||||
import { ComfyWidgets } from "./widgets.js";
|
||||
import { ComfyWidgets, getWidgetType } from "./widgets.js";
|
||||
import { ComfyUI, $el } from "./ui.js";
|
||||
import { api } from "./api.js";
|
||||
import { defaultGraph } from "./defaultGraph.js";
|
||||
@ -747,7 +747,7 @@ export class ComfyApp {
|
||||
* Adds a handler on paste that extracts and loads images or workflows from pasted JSON data
|
||||
*/
|
||||
#addPasteHandler() {
|
||||
document.addEventListener("paste", (e) => {
|
||||
document.addEventListener("paste", async (e) => {
|
||||
// ctrl+shift+v is used to paste nodes with connections
|
||||
// this is handled by litegraph
|
||||
if(this.shiftDown) return;
|
||||
@ -795,7 +795,7 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
if (workflow && workflow.version && workflow.nodes && workflow.extra) {
|
||||
this.loadGraphData(workflow);
|
||||
await this.loadGraphData(workflow);
|
||||
}
|
||||
else {
|
||||
if (e.target.type === "text" || e.target.type === "textarea") {
|
||||
@ -1285,7 +1285,7 @@ export class ComfyApp {
|
||||
const json = localStorage.getItem("workflow");
|
||||
if (json) {
|
||||
const workflow = JSON.parse(json);
|
||||
this.loadGraphData(workflow);
|
||||
await this.loadGraphData(workflow);
|
||||
restored = true;
|
||||
}
|
||||
} catch (err) {
|
||||
@ -1294,7 +1294,7 @@ export class ComfyApp {
|
||||
|
||||
// We failed to restore a workflow so load the default
|
||||
if (!restored) {
|
||||
this.loadGraphData();
|
||||
await this.loadGraphData();
|
||||
}
|
||||
|
||||
// Save current workflow automatically
|
||||
@ -1322,11 +1322,81 @@ export class ComfyApp {
|
||||
await this.#invokeExtensionsAsync("registerCustomNodes");
|
||||
}
|
||||
|
||||
async registerNodeDef(nodeId, nodeData) {
|
||||
const self = this;
|
||||
const node = Object.assign(
|
||||
function ComfyNode() {
|
||||
var inputs = nodeData["input"]["required"];
|
||||
if (nodeData["input"]["optional"] != undefined) {
|
||||
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"]);
|
||||
}
|
||||
const config = { minWidth: 1, minHeight: 1 };
|
||||
for (const inputName in inputs) {
|
||||
const inputData = inputs[inputName];
|
||||
const type = inputData[0];
|
||||
|
||||
let widgetCreated = true;
|
||||
const widgetType = getWidgetType(inputData, inputName);
|
||||
if(widgetType) {
|
||||
if(widgetType === "COMBO") {
|
||||
Object.assign(config, self.widgets.COMBO(this, inputName, inputData, app) || {});
|
||||
} else {
|
||||
Object.assign(config, self.widgets[widgetType](this, inputName, inputData, app) || {});
|
||||
}
|
||||
} else {
|
||||
// Node connection inputs
|
||||
this.addInput(inputName, type);
|
||||
widgetCreated = false;
|
||||
}
|
||||
|
||||
if(widgetCreated && inputData[1]?.forceInput && config?.widget) {
|
||||
if (!config.widget.options) config.widget.options = {};
|
||||
config.widget.options.forceInput = inputData[1].forceInput;
|
||||
}
|
||||
if(widgetCreated && inputData[1]?.defaultInput && config?.widget) {
|
||||
if (!config.widget.options) config.widget.options = {};
|
||||
config.widget.options.defaultInput = inputData[1].defaultInput;
|
||||
}
|
||||
}
|
||||
|
||||
for (const o in nodeData["output"]) {
|
||||
let output = nodeData["output"][o];
|
||||
if(output instanceof Array) output = "COMBO";
|
||||
const outputName = nodeData["output_name"][o] || output;
|
||||
const outputShape = nodeData["output_is_list"][o] ? LiteGraph.GRID_SHAPE : LiteGraph.CIRCLE_SHAPE ;
|
||||
this.addOutput(outputName, output, { shape: outputShape });
|
||||
}
|
||||
|
||||
const s = this.computeSize();
|
||||
s[0] = Math.max(config.minWidth, s[0] * 1.5);
|
||||
s[1] = Math.max(config.minHeight, s[1]);
|
||||
this.size = s;
|
||||
this.serialize_widgets = true;
|
||||
|
||||
app.#invokeExtensionsAsync("nodeCreated", this);
|
||||
},
|
||||
{
|
||||
title: nodeData.display_name || nodeData.name,
|
||||
comfyClass: nodeData.name,
|
||||
nodeData
|
||||
}
|
||||
);
|
||||
node.prototype.comfyClass = nodeData.name;
|
||||
|
||||
this.#addNodeContextMenuHandler(node);
|
||||
this.#addDrawBackgroundHandler(node, app);
|
||||
this.#addNodeKeyHandler(node);
|
||||
|
||||
await this.#invokeExtensionsAsync("beforeRegisterNodeDef", node, nodeData);
|
||||
LiteGraph.registerNodeType(nodeId, node);
|
||||
node.category = nodeData.category;
|
||||
}
|
||||
|
||||
async registerNodesFromDefs(defs) {
|
||||
await this.#invokeExtensionsAsync("addCustomNodeDefs", defs);
|
||||
|
||||
// Generate list of known widgets
|
||||
const widgets = Object.assign(
|
||||
this.widgets = Object.assign(
|
||||
{},
|
||||
ComfyWidgets,
|
||||
...(await this.#invokeExtensionsAsync("getCustomWidgets")).filter(Boolean)
|
||||
@ -1334,75 +1404,7 @@ export class ComfyApp {
|
||||
|
||||
// Register a node for each definition
|
||||
for (const nodeId in defs) {
|
||||
const nodeData = defs[nodeId];
|
||||
const node = Object.assign(
|
||||
function ComfyNode() {
|
||||
var inputs = nodeData["input"]["required"];
|
||||
if (nodeData["input"]["optional"] != undefined){
|
||||
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
|
||||
}
|
||||
const config = { minWidth: 1, minHeight: 1 };
|
||||
for (const inputName in inputs) {
|
||||
const inputData = inputs[inputName];
|
||||
const type = inputData[0];
|
||||
|
||||
let widgetCreated = true;
|
||||
if (Array.isArray(type)) {
|
||||
// Enums
|
||||
Object.assign(config, widgets.COMBO(this, inputName, inputData, app) || {});
|
||||
} else if (`${type}:${inputName}` in widgets) {
|
||||
// Support custom widgets by Type:Name
|
||||
Object.assign(config, widgets[`${type}:${inputName}`](this, inputName, inputData, app) || {});
|
||||
} else if (type in widgets) {
|
||||
// Standard type widgets
|
||||
Object.assign(config, widgets[type](this, inputName, inputData, app) || {});
|
||||
} else {
|
||||
// Node connection inputs
|
||||
this.addInput(inputName, type);
|
||||
widgetCreated = false;
|
||||
}
|
||||
|
||||
if(widgetCreated && inputData[1]?.forceInput && config?.widget) {
|
||||
if (!config.widget.options) config.widget.options = {};
|
||||
config.widget.options.forceInput = inputData[1].forceInput;
|
||||
}
|
||||
if(widgetCreated && inputData[1]?.defaultInput && config?.widget) {
|
||||
if (!config.widget.options) config.widget.options = {};
|
||||
config.widget.options.defaultInput = inputData[1].defaultInput;
|
||||
}
|
||||
}
|
||||
|
||||
for (const o in nodeData["output"]) {
|
||||
let output = nodeData["output"][o];
|
||||
if(output instanceof Array) output = "COMBO";
|
||||
const outputName = nodeData["output_name"][o] || output;
|
||||
const outputShape = nodeData["output_is_list"][o] ? LiteGraph.GRID_SHAPE : LiteGraph.CIRCLE_SHAPE ;
|
||||
this.addOutput(outputName, output, { shape: outputShape });
|
||||
}
|
||||
|
||||
const s = this.computeSize();
|
||||
s[0] = Math.max(config.minWidth, s[0] * 1.5);
|
||||
s[1] = Math.max(config.minHeight, s[1]);
|
||||
this.size = s;
|
||||
this.serialize_widgets = true;
|
||||
|
||||
app.#invokeExtensionsAsync("nodeCreated", this);
|
||||
},
|
||||
{
|
||||
title: nodeData.display_name || nodeData.name,
|
||||
comfyClass: nodeData.name,
|
||||
nodeData
|
||||
}
|
||||
);
|
||||
node.prototype.comfyClass = nodeData.name;
|
||||
|
||||
this.#addNodeContextMenuHandler(node);
|
||||
this.#addDrawBackgroundHandler(node, app);
|
||||
this.#addNodeKeyHandler(node);
|
||||
|
||||
await this.#invokeExtensionsAsync("beforeRegisterNodeDef", node, nodeData);
|
||||
LiteGraph.registerNodeType(nodeId, node);
|
||||
node.category = nodeData.category;
|
||||
this.registerNodeDef(nodeId, defs[nodeId]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1410,7 +1412,7 @@ export class ComfyApp {
|
||||
* Populates the graph with the specified workflow data
|
||||
* @param {*} graphData A serialized graph object
|
||||
*/
|
||||
loadGraphData(graphData) {
|
||||
async loadGraphData(graphData) {
|
||||
this.clean();
|
||||
|
||||
let reset_invalid_values = false;
|
||||
@ -1425,6 +1427,7 @@ export class ComfyApp {
|
||||
reset_invalid_values = true;
|
||||
}
|
||||
|
||||
await this.#invokeExtensionsAsync("beforeConfigureGraph", graphData);
|
||||
const missingNodeTypes = [];
|
||||
for (let n of graphData.nodes) {
|
||||
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
|
||||
@ -1542,83 +1545,89 @@ export class ComfyApp {
|
||||
const workflow = this.graph.serialize();
|
||||
const output = {};
|
||||
// Process nodes in order of execution
|
||||
for (const node of this.graph.computeExecutionOrder(false)) {
|
||||
const n = workflow.nodes.find((n) => n.id === node.id);
|
||||
|
||||
if (node.isVirtualNode) {
|
||||
// Don't serialize frontend only nodes but let them make changes
|
||||
if (node.applyToGraph) {
|
||||
node.applyToGraph(workflow);
|
||||
for (const outerNode of this.graph.computeExecutionOrder(false)) {
|
||||
const n = workflow.nodes.find((n) => n.id === outerNode.id);
|
||||
const innerNodes = outerNode.getInnerNodes ? outerNode.getInnerNodes() : [outerNode];
|
||||
for (const node of innerNodes) {
|
||||
console.log(node.id, node.title ?? node.comfyClass);
|
||||
if (node.isVirtualNode) {
|
||||
// Don't serialize frontend only nodes but let them make changes
|
||||
if (node.applyToGraph) {
|
||||
node.applyToGraph(workflow);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.mode === 2 || node.mode === 4) {
|
||||
// Don't serialize muted nodes
|
||||
continue;
|
||||
}
|
||||
if (node.mode === 2 || node.mode === 4) {
|
||||
// Don't serialize muted nodes
|
||||
continue;
|
||||
}
|
||||
|
||||
const inputs = {};
|
||||
const widgets = node.widgets;
|
||||
const inputs = {};
|
||||
const widgets = node.widgets;
|
||||
|
||||
// Store all widget values
|
||||
if (widgets) {
|
||||
for (const i in widgets) {
|
||||
const widget = widgets[i];
|
||||
if (!widget.options || widget.options.serialize !== false) {
|
||||
inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(n, i) : widget.value;
|
||||
// Store all widget values
|
||||
if (widgets) {
|
||||
for (const i in widgets) {
|
||||
const widget = widgets[i];
|
||||
if (!widget.options || widget.options.serialize !== false) {
|
||||
inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(n, i) : widget.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store all node links
|
||||
for (let i in node.inputs) {
|
||||
let parent = node.getInputNode(i);
|
||||
if (parent) {
|
||||
let link = node.getInputLink(i);
|
||||
while (parent.mode === 4 || parent.isVirtualNode) {
|
||||
let found = false;
|
||||
if (parent.isVirtualNode) {
|
||||
link = parent.getInputLink(link.origin_slot);
|
||||
if (link) {
|
||||
parent = parent.getInputNode(link.target_slot);
|
||||
if (parent) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
} else if (link && parent.mode === 4) {
|
||||
let all_inputs = [link.origin_slot];
|
||||
if (parent.inputs) {
|
||||
all_inputs = all_inputs.concat(Object.keys(parent.inputs))
|
||||
for (let parent_input in all_inputs) {
|
||||
parent_input = all_inputs[parent_input];
|
||||
if (parent.inputs[parent_input].type === node.inputs[i].type) {
|
||||
link = parent.getInputLink(parent_input);
|
||||
if (link) {
|
||||
parent = parent.getInputNode(parent_input);
|
||||
}
|
||||
// Store all node links
|
||||
for (let i in node.inputs) {
|
||||
let parent = node.getInputNode(i);
|
||||
if (parent) {
|
||||
let link = node.getInputLink(i);
|
||||
while (parent.mode === 4 || parent.isVirtualNode) {
|
||||
let found = false;
|
||||
if (parent.isVirtualNode) {
|
||||
link = parent.getInputLink(link.origin_slot);
|
||||
if (link) {
|
||||
parent = parent.getInputNode(link.target_slot);
|
||||
if (parent) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (link && parent.mode === 4) {
|
||||
let all_inputs = [link.origin_slot];
|
||||
if (parent.inputs) {
|
||||
all_inputs = all_inputs.concat(Object.keys(parent.inputs))
|
||||
for (let parent_input in all_inputs) {
|
||||
parent_input = all_inputs[parent_input];
|
||||
if (parent.inputs[parent_input].type === node.inputs[i].type) {
|
||||
link = parent.getInputLink(parent_input);
|
||||
if (link) {
|
||||
parent = parent.getInputNode(parent_input);
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
break;
|
||||
if (link) {
|
||||
if (parent.updateLink) {
|
||||
link = parent.updateLink(link);
|
||||
}
|
||||
inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)];
|
||||
}
|
||||
}
|
||||
|
||||
if (link) {
|
||||
inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output[String(node.id)] = {
|
||||
inputs,
|
||||
class_type: node.comfyClass,
|
||||
};
|
||||
output[String(node.id)] = {
|
||||
inputs,
|
||||
class_type: node.comfyClass,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Remove inputs connected to removed nodes
|
||||
@ -1738,21 +1747,21 @@ export class ComfyApp {
|
||||
const pngInfo = await getPngMetadata(file);
|
||||
if (pngInfo) {
|
||||
if (pngInfo.workflow) {
|
||||
this.loadGraphData(JSON.parse(pngInfo.workflow));
|
||||
await this.loadGraphData(JSON.parse(pngInfo.workflow));
|
||||
} else if (pngInfo.parameters) {
|
||||
importA1111(this.graph, pngInfo.parameters);
|
||||
}
|
||||
}
|
||||
} else if (file.type === "application/json" || file.name?.endsWith(".json")) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
this.loadGraphData(JSON.parse(reader.result));
|
||||
reader.onload = async () => {
|
||||
await this.loadGraphData(JSON.parse(reader.result));
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} else if (file.name?.endsWith(".latent") || file.name?.endsWith(".safetensors")) {
|
||||
const info = await getLatentMetadata(file);
|
||||
if (info.workflow) {
|
||||
this.loadGraphData(JSON.parse(info.workflow));
|
||||
await this.loadGraphData(JSON.parse(info.workflow));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -462,8 +462,8 @@ class ComfyList {
|
||||
return $el("div", {textContent: item.prompt[0] + ": "}, [
|
||||
$el("button", {
|
||||
textContent: "Load",
|
||||
onclick: () => {
|
||||
app.loadGraphData(item.prompt[3].extra_pnginfo.workflow);
|
||||
onclick: async () => {
|
||||
await app.loadGraphData(item.prompt[3].extra_pnginfo.workflow);
|
||||
if (item.outputs) {
|
||||
app.nodeOutputs = item.outputs;
|
||||
}
|
||||
@ -782,9 +782,9 @@ export class ComfyUI {
|
||||
}
|
||||
}),
|
||||
$el("button", {
|
||||
id: "comfy-load-default-button", textContent: "Load Default", onclick: () => {
|
||||
id: "comfy-load-default-button", textContent: "Load Default", onclick: async () => {
|
||||
if (!confirmClear.value || confirm("Load default workflow?")) {
|
||||
app.loadGraphData()
|
||||
await app.loadGraphData()
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
@ -22,13 +22,26 @@ function getNumberDefaults(inputData, defaultStep, precision, enable_rounding) {
|
||||
return { val: defaultVal, config: { min, max, step: 10.0 * step, round, precision } };
|
||||
}
|
||||
|
||||
export function getWidgetType(inputData, inputName) {
|
||||
const type = inputData[0];
|
||||
|
||||
if (Array.isArray(type)) {
|
||||
return "COMBO";
|
||||
} else if (`${type}:${inputName}` in ComfyWidgets) {
|
||||
return `${type}:${inputName}`;
|
||||
} else if (type in ComfyWidgets) {
|
||||
return type;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function addValueControlWidget(node, targetWidget, defaultValue = "randomize", values) {
|
||||
const valueControl = node.addWidget("combo", "control_after_generate", defaultValue, function (v) { }, {
|
||||
values: ["fixed", "increment", "decrement", "randomize"],
|
||||
serialize: false, // Don't include this in prompt.
|
||||
});
|
||||
valueControl.afterQueued = () => {
|
||||
|
||||
var v = valueControl.value;
|
||||
|
||||
if (targetWidget.type == "combo" && v !== "fixed") {
|
||||
@ -77,26 +90,46 @@ export function addValueControlWidget(node, targetWidget, defaultValue = "random
|
||||
default:
|
||||
break;
|
||||
}
|
||||
/*check if values are over or under their respective
|
||||
* ranges and set them to min or max.*/
|
||||
if (targetWidget.value < min)
|
||||
targetWidget.value = min;
|
||||
/*check if values are over or under their respective
|
||||
* ranges and set them to min or max.*/
|
||||
if (targetWidget.value < min) targetWidget.value = min;
|
||||
|
||||
if (targetWidget.value > max)
|
||||
targetWidget.value = max;
|
||||
if (targetWidget.value > max) targetWidget.value = max;
|
||||
}
|
||||
}
|
||||
};
|
||||
return valueControl;
|
||||
};
|
||||
}
|
||||
|
||||
function seedWidget(node, inputName, inputData, app) {
|
||||
const seed = ComfyWidgets.INT(node, inputName, inputData, app);
|
||||
const seed = createIntWidget(node, inputName, inputData, app, true);
|
||||
const seedControl = addValueControlWidget(node, seed.widget, "randomize");
|
||||
|
||||
seed.widget.linkedWidgets = [seedControl];
|
||||
return seed;
|
||||
}
|
||||
|
||||
function createIntWidget(node, inputName, inputData, app, isSeedInput) {
|
||||
if (!isSeedInput && inputData[1]?.control_after_generate) {
|
||||
return seedWidget(node, inputName, inputData, app);
|
||||
}
|
||||
|
||||
let widgetType = isSlider(inputData[1]["display"], app);
|
||||
const { val, config } = getNumberDefaults(inputData, 1, 0, true);
|
||||
Object.assign(config, { precision: 0 });
|
||||
return {
|
||||
widget: node.addWidget(
|
||||
widgetType,
|
||||
inputName,
|
||||
val,
|
||||
function (v) {
|
||||
const s = this.options.step / 10;
|
||||
this.value = Math.round(v / s) * s;
|
||||
},
|
||||
config
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const MultilineSymbol = Symbol();
|
||||
const MultilineResizeSymbol = Symbol();
|
||||
|
||||
@ -175,22 +208,22 @@ function addMultilineWidget(node, name, opts, app) {
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(margin, margin + y);
|
||||
|
||||
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
|
||||
Object.assign(this.inputEl.style, {
|
||||
transformOrigin: "0 0",
|
||||
transform: scale,
|
||||
left: `${transform.a + transform.e}px`,
|
||||
top: `${transform.d + transform.f}px`,
|
||||
width: `${widgetWidth - (margin * 2)}px`,
|
||||
height: `${this.parent.inputHeight - (margin * 2)}px`,
|
||||
position: "absolute",
|
||||
background: (!node.color)?'':node.color,
|
||||
color: (!node.color)?'':'white',
|
||||
zIndex: app.graph._nodes.indexOf(node),
|
||||
});
|
||||
this.inputEl.hidden = !visible;
|
||||
},
|
||||
};
|
||||
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
|
||||
Object.assign(this.inputEl.style, {
|
||||
transformOrigin: "0 0",
|
||||
transform: scale,
|
||||
left: `${transform.a + transform.e}px`,
|
||||
top: `${transform.d + transform.f}px`,
|
||||
width: `${widgetWidth - (margin * 2)}px`,
|
||||
height: `${this.parent.inputHeight - (margin * 2)}px`,
|
||||
position: "absolute",
|
||||
background: (!node.color)?'':node.color,
|
||||
color: (!node.color)?'':'white',
|
||||
zIndex: app.graph._nodes.indexOf(node),
|
||||
});
|
||||
this.inputEl.hidden = !visible;
|
||||
},
|
||||
};
|
||||
widget.inputEl = document.createElement("textarea");
|
||||
widget.inputEl.className = "comfy-multiline-input";
|
||||
widget.inputEl.value = opts.defaultVal;
|
||||
@ -287,21 +320,7 @@ export const ComfyWidgets = {
|
||||
}, config) };
|
||||
},
|
||||
INT(node, inputName, inputData, app) {
|
||||
let widgetType = isSlider(inputData[1]["display"], app);
|
||||
const { val, config } = getNumberDefaults(inputData, 1, 0, true);
|
||||
Object.assign(config, { precision: 0 });
|
||||
return {
|
||||
widget: node.addWidget(
|
||||
widgetType,
|
||||
inputName,
|
||||
val,
|
||||
function (v) {
|
||||
const s = this.options.step / 10;
|
||||
this.value = Math.round(v / s) * s;
|
||||
},
|
||||
config
|
||||
),
|
||||
};
|
||||
return createIntWidget(node, inputName, inputData, app);
|
||||
},
|
||||
BOOLEAN(node, inputName, inputData) {
|
||||
let defaultVal = inputData[1]["default"];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user