wip group nodes

This commit is contained in:
pythongosssss 2023-10-13 20:26:44 +01:00
parent c6df6d642a
commit f40f910801
5 changed files with 589 additions and 186 deletions

View 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);

View File

@ -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");
}

View File

@ -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));
}
}
}

View File

@ -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()
}
}
}),

View File

@ -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"];