Initial code.

This commit is contained in:
rgthree
2023-08-15 00:44:23 -04:00
parent 3c6891d239
commit 6de20e578f
39 changed files with 3707 additions and 1 deletions
+6
View File
@@ -0,0 +1,6 @@
__pycache__
*.ini
wildcards/**
.vscode/
.idea/
node_modules/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Regis Gaughan, III (rgthree)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+78 -1
View File
@@ -1 +1,78 @@
# rgthree-comfy
# rgthree's ComfyUi Nodes
These are a bunch of comfort and power nodes I created when messing around with Stable Diffusion. I made them for myself to make my workflow easier and cleaner. You're welcome to try them out, but do so at your own risk. Since I made them for myself, I didn't battle test them much outside of my specific use cases.
## Install
1. Install [ComfyUi](https://github.com/comfyanonymous/ComfyUI).
2. Clone this repo into `custom_modules`:
```
cd ComfyUI/custom_nodes
git clone https://github.com/rgthree/rgthree-comfy.git
```
3. Start up ComfyUI.
## Comfort Nodes
### Seed
Finally, an intuitive seed control node for ComfyUI that works very much like Automatic1111's seed control.
- Set the seed value to "-1" to use a random seed every time
- Set any other number in there to use as a static/fixed seed
- Quick actions to randomize, or (re-)use the last queued seed.
- Images metadata will store the seed value _(so dragging an image in, will have the seed field already fixed to its seed)_.
- Note, since we can randomize each time, and quickly grab previous ones
![Router Node](./docs/rgthree_seed.png)
### Reroute (but, like, for real)
I wasn't a fan of how the original re-route wouldn't let me route in different directions (like, upwards, or to the left), so I made my own that can with each direction covered and pre-fixed small sizes.
![Router Node](./docs/rgthree_router.png)
### Context
Allows you to keep a current context of general flow properties, merging in new data. Similar to some other node suites "pipes" but easier merging, is more easily interoperable with standard nodes by both combining and exploding all in a single node.
![Context Node](./docs/rgthree_context.png)
### Display Int
Shows an int _after execution_.
### Lora Loader Stack
A simplified Lora Loader stack. Much like other suites, but more interoperable with standard inputs/outputs.
## Power Nodes
With the following nodes, you can make your ComfyUI experience more streamlined using the **Context Switch** to choose the first non-null **Context** input powered by the **Fast Muter** as a one-press dashboard of toggles to enable and disabled your workflows.
![Context Node](./docs/rgthree_advanced.png)
### Context Switch
_(In aqua blue above)_ Chooses the first non-null context.
### Fast Muter
_(In purple above)_ A collection of all connected nodes allowing a single-spot "dashbaord" to quickly enable and disable nodes.
Also, you can use the **Node Combiner** as UI-only virtual node that allows you to connect any number of nodes as an input, into a single output. As of right now, this is only useful for cleaning up noodles to the Muter node and **any other use will likely not work at all.**
### Putting it together
1. Using the **Context Switch** feed context inputs in order of preference. In the workflow above, the `Upscale Out` context is first so, if that one is enabled, it will be chosen for the output. If not, the second input slot which comes from the context rerouted from above (before the Upscaler booth) will be chosen.
- Notice the `Upscale Preview` is _after_ the `Upscale Out` context node, using the image from it instead of the image from the upscale `VAE Decoder`. This is on purpose so, when we disable the `Upscale Out` context, none of the Upscaler nodes will run, saving precious GPU cycles. If we had the preview hooked up directly to the `VAE Decoder` the upscaler would always run to generate the preview, even if we had the `Upscale Out` context node disabled.
2. We can now disable the `Upscale Out` context node by _muting_ it. Highlighting it and pressing `ctrl + m` will work. By doing so, it's output will be None, and it will not pass anthing onto the further nodes. In the diagram you can see the `Upscale Preview` is red, but that's OK; there are no actual errors to stop execution.
3. Now, let's hook it up to the `Fast Muter` node. `The Fast Muter` node works as dashboard by adding quick toggles for any connected node (ignoring reroutes). In the diagram, we have both the `Upscaler Out` context node, and the `Save File` context node hooked up. So, we can quickly enable and disable those.
- The workflow seen here would be a common one where we can generate a handful of base previews cheaply with a random seed, and then choose one to upscale and save to disk.
4. Lastly, and optionally, you can see the `Node Combiner`. Use it to clean up noodles if you want and connect it to the muter. You can connect anything to it, but doing so may break your workflow's execution.
+41
View File
@@ -0,0 +1,41 @@
from server import PromptServer
import importlib.util
import glob
import os
import sys
import shutil
import inspect
def get_dir(subpath, mkdir=False):
dir = os.path.dirname(inspect.getfile(PromptServer))
dir = os.path.join(dir, subpath)
dir = os.path.abspath(dir)
if not os.path.exists(dir):
if mkdir:
os.makedirs(dir)
else:
raise ValueError('Path not found: %s' % dir)
return dir
DIR_JS = get_dir('custom_nodes/rgthree-comfy/js')
DIR_PY = get_dir('custom_nodes/rgthree-comfy/py')
DIR_WEB = get_dir('web/extensions/rgthree', mkdir=True)
shutil.copytree(DIR_JS, DIR_WEB, dirs_exist_ok=True)
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
for file in glob.glob("*.py", root_dir=DIR_PY, recursive=False):
name = os.path.splitext(file)[0]
spec = importlib.util.spec_from_file_location(name, os.path.join(DIR_PY, file))
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
if hasattr(module, "NODE_CLASS_MAPPINGS") and getattr(module, "NODE_CLASS_MAPPINGS") is not None:
NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS)
if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS") and getattr(module, "NODE_DISPLAY_NAME_MAPPINGS") is not None:
NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS)
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+21
View File
@@ -0,0 +1,21 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { app } from "../../scripts/app.js";
import { addConnectionLayoutSupport } from "./utils.js";
app.registerExtension({
name: "rgthree.Context",
beforeRegisterNodeDef(nodeType, nodeData, app) {
return __awaiter(this, void 0, void 0, function* () {
if (nodeData.name === "Context (rgthree)") {
addConnectionLayoutSupport(nodeType, app, [['Left', 'Right'], ['Right', 'Left']]);
}
});
},
});
+39
View File
@@ -0,0 +1,39 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { app } from "../../scripts/app.js";
import { ComfyWidgets } from "../../scripts/widgets.js";
import { addConnectionLayoutSupport } from "./utils.js";
app.registerExtension({
name: "rgthree.DisplayInt",
beforeRegisterNodeDef(nodeType, nodeData, app) {
return __awaiter(this, void 0, void 0, function* () {
if (nodeData.name === "Display Int (rgthree)") {
let showValueWidget;
nodeType.title_mode = LiteGraph.NO_TITLE;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
showValueWidget = ComfyWidgets["STRING"](this, "output", ["STRING", { multiline: true }], app).widget;
showValueWidget.inputEl.readOnly = true;
showValueWidget.serializeValue = (node, index) => __awaiter(this, void 0, void 0, function* () {
node.widgets_values[index] = '';
return '';
});
};
addConnectionLayoutSupport(nodeType, app, [['Left'], ['Right']]);
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted === null || onExecuted === void 0 ? void 0 : onExecuted.apply(this, [message]);
showValueWidget.value = message.text[0];
};
}
});
},
});
+113
View File
@@ -0,0 +1,113 @@
import { app } from "../../scripts/app.js";
import { addConnectionLayoutSupport, addMenuItem } from "./utils.js";
const MUTE_MODE = 2;
const ALWAYS_MODE = 0;
app.registerExtension({
name: "rgthree.Muter",
registerCustomNodes() {
class CustomNode extends LGraphNode {
constructor(title = CustomNode.title) {
super(title);
this.debouncer = 0;
this.schedulePromise = null;
this.isVirtualNode = true;
this.properties = this.properties || {};
this.connections = [];
this.addInput("", "*");
}
doChainLookup(startNode = this) {
let rootNodes = [];
const type = startNode.constructor.type;
if (startNode === this || (type === null || type === void 0 ? void 0 : type.includes('Reroute')) || (type === null || type === void 0 ? void 0 : type.includes('Combiner'))) {
for (const input of startNode.inputs) {
const linkId = input.link;
if (!linkId) {
continue;
}
const link = app.graph.links[linkId];
const originNode = app.graph.getNodeById(link.origin_id);
const foundNodes = this.doChainLookup(originNode);
rootNodes = rootNodes.concat(foundNodes);
}
}
else if (!(type === null || type === void 0 ? void 0 : type.includes('Reroute')) && !(type === null || type === void 0 ? void 0 : type.includes('Combiner'))) {
rootNodes.push(startNode);
}
return rootNodes;
}
scheduleRefreshMutables() {
if (!this.schedulePromise) {
this.schedulePromise = new Promise((resolve) => {
setTimeout(() => {
resolve(this.refreshMutables());
this.schedulePromise = null;
}, 100);
});
}
return this.schedulePromise;
}
refreshMutables() {
this.stabilizeInputsOutputs();
const mutables = this.doChainLookup();
for (const [index, node] of mutables.entries()) {
let widget = this.widgets && this.widgets[index];
if (!widget) {
widget = this.addWidget("toggle", 'title', false, '', { "on": 'yes', "off": 'no' });
}
const muted = node.mode === MUTE_MODE;
widget.name = `Enable ${node.title}`;
widget.value = !muted;
widget.callback = () => {
const muted = node.mode === MUTE_MODE;
node.mode = muted ? ALWAYS_MODE : MUTE_MODE;
widget.value = muted;
};
}
this.widgets.length = mutables.length;
app.graph.setDirtyCanvas(true, true);
}
onConnectionsChainChange() {
this.scheduleRefreshMutables();
}
onConnectionsChange(_type, _index, _connected, _linkInfo, _ioSlot) {
this.scheduleRefreshMutables();
}
stabilizeInputsOutputs() {
for (let index = this.inputs.length - 1; index >= 0; index--) {
const input = this.inputs[index];
if (!input.link) {
this.removeInput(index);
}
}
this.addInput('', '*');
}
computeSize(out) {
var _a, _b;
let size = super.computeSize(out);
if (this.properties['collapse_connections']) {
const rows = Math.max(((_a = this.inputs) === null || _a === void 0 ? void 0 : _a.length) || 0, ((_b = this.outputs) === null || _b === void 0 ? void 0 : _b.length) || 0, 1) - 1;
size[1] = size[1] - (rows * LiteGraph.NODE_SLOT_HEIGHT);
}
setTimeout(() => {
app.graph.setDirtyCanvas(true, true);
}, 16);
return size;
}
}
CustomNode.title = "Fast Muter (rgthree)";
CustomNode.collapsible = false;
CustomNode.category = "rgthree/utils";
addConnectionLayoutSupport(CustomNode, app, [['Left'], ['Right']]);
addMenuItem(CustomNode, app, {
name: (node) => { var _a; return (`${((_a = node.properties) === null || _a === void 0 ? void 0 : _a['collapse_connections']) ? 'Show' : 'Collapse'} Connections`); },
property: 'collapse_connections',
prepareValue: (_value, node) => { var _a; return !((_a = node.properties) === null || _a === void 0 ? void 0 : _a['collapse_connections']); },
callback: (_node) => { app.graph.setDirtyCanvas(true, true); }
});
addMenuItem(CustomNode, app, {
name: 'Refresh',
callback: (node) => { node.scheduleRefreshMutables(); }
});
LiteGraph.registerNodeType(CustomNode.title, CustomNode);
},
});
+62
View File
@@ -0,0 +1,62 @@
import { app } from "../../scripts/app.js";
import { addConnectionLayoutSupport } from "./utils.js";
app.registerExtension({
name: "rgthree.NodeCombiner",
registerCustomNodes() {
class CombinerNode extends LGraphNode {
constructor(title = CombinerNode.title) {
super(title);
this.isVirtualNode = true;
this.properties = this.properties || {};
this.connections = [];
this.addInput("", "*");
this.addOutput("Output", "*");
}
clone() {
const cloned = super.clone();
return cloned;
}
updateOutputLinks(startNode = this) {
const type = startNode.constructor.type;
if (startNode.onConnectionsChainChange) {
startNode.onConnectionsChainChange();
}
if (startNode === this || (type === null || type === void 0 ? void 0 : type.includes('Reroute')) || (type === null || type === void 0 ? void 0 : type.includes('Combiner'))) {
for (const output of startNode.outputs) {
if (!output.links || !output.links.length)
continue;
for (const linkId of output.links) {
const link = app.graph.links[linkId];
const targetNode = app.graph.getNodeById(link.target_id);
targetNode && this.updateOutputLinks(targetNode);
}
}
}
}
onConnectionsChange(_type, _slotIndex, _isConnected, link_info, _ioSlot) {
if (!link_info)
return;
this.stabilizeInputsOutputs();
this.updateOutputLinks();
}
stabilizeInputsOutputs() {
var _a, _b;
for (let index = this.inputs.length - 1; index >= 0; index--) {
const input = this.inputs[index];
if (!input.link) {
this.removeInput(index);
}
}
this.addInput('', '*');
const outputLength = ((_b = (_a = this.outputs[0]) === null || _a === void 0 ? void 0 : _a.links) === null || _b === void 0 ? void 0 : _b.length) || 0;
if (outputLength > 1) {
this.outputs[0].links.length = 1;
}
}
}
CombinerNode.title = "Node Combiner (rgthree)";
CombinerNode.category = "rgthree";
addConnectionLayoutSupport(CombinerNode, app, [['Left', 'Right'], ['Right', 'Left']]);
LiteGraph.registerNodeType(CombinerNode.title, CombinerNode);
},
});
+170
View File
@@ -0,0 +1,170 @@
import { app } from "../../scripts/app.js";
import { addConnectionLayoutSupport, addMenuSubMenu } from "./utils.js";
app.registerExtension({
name: "rgthree.Reroute",
registerCustomNodes() {
class RerouteNode extends LGraphNode {
constructor(title = RerouteNode.title) {
super(title);
this.isVirtualNode = true;
this.resizable = false;
this.size = RerouteNode.size;
this.addInput("", "*");
this.addOutput("", "*");
setTimeout(() => this.applyNodeSize(), 20);
}
configure(info) {
super.configure(info);
this.applyNodeSize();
}
clone() {
const cloned = super.clone();
cloned.inputs[0].type = '*';
cloned.outputs[0].type = '*';
return cloned;
}
onConnectionsChange(type, _slotIndex, connected, _link_info, _ioSlot) {
var _a, _b;
if (connected && type === LiteGraph.OUTPUT) {
const types = new Set(this.outputs[0].links.map((l) => app.graph.links[l].type).filter((t) => t !== "*"));
if (types.size > 1) {
const linksToDisconnect = [];
for (let i = 0; i < this.outputs[0].links.length - 1; i++) {
const linkId = this.outputs[0].links[i];
const link = app.graph.links[linkId];
linksToDisconnect.push(link);
}
for (const link of linksToDisconnect) {
const node = app.graph.getNodeById(link.target_id);
node.disconnectInput(link.target_slot);
}
}
}
let currentNode = this;
let updateNodes = [];
let inputType = null;
let inputNode = null;
while (currentNode) {
updateNodes.unshift(currentNode);
const linkId = currentNode.inputs[0].link;
if (linkId !== null) {
const link = app.graph.links[linkId];
const node = app.graph.getNodeById(link.origin_id);
const type = node.constructor.type;
if (type === null || type === void 0 ? void 0 : type.includes("Reroute")) {
if (node === this) {
currentNode.disconnectInput(link.target_slot);
currentNode = null;
}
else {
currentNode = node;
}
}
else {
inputNode = currentNode;
inputType = (_b = (_a = node.outputs[link.origin_slot]) === null || _a === void 0 ? void 0 : _a.type) !== null && _b !== void 0 ? _b : null;
break;
}
}
else {
currentNode = null;
break;
}
}
const nodes = [this];
let outputType = null;
while (nodes.length) {
currentNode = nodes.pop();
const outputs = (currentNode.outputs ? currentNode.outputs[0].links : []) || [];
if (outputs.length) {
for (const linkId of outputs) {
const link = app.graph.links[linkId];
if (!link)
continue;
const node = app.graph.getNodeById(link.target_id);
const type = node.constructor.type;
if (type === null || type === void 0 ? void 0 : type.includes("Reroute")) {
nodes.push(node);
updateNodes.push(node);
}
else {
const nodeOutType = node.inputs && node.inputs[link === null || link === void 0 ? void 0 : link.target_slot] && node.inputs[link.target_slot].type ? node.inputs[link.target_slot].type : null;
if (inputType && nodeOutType !== inputType && nodeOutType !== '*') {
node.disconnectInput(link.target_slot);
}
else {
outputType = nodeOutType;
}
}
}
}
else {
}
}
const displayType = inputType || outputType || "*";
const color = LGraphCanvas.link_type_colors[displayType];
for (const node of updateNodes) {
node.outputs[0].type = inputType || "*";
node.__outputType = displayType;
node.outputs[0].name = node.properties.showOutputText ? displayType : "";
node.size = node.computeSize();
node.applyNodeSize();
for (const l of node.outputs[0].links || []) {
const link = app.graph.links[l];
if (link) {
link.color = color;
}
}
}
if (inputNode) {
const link = app.graph.links[inputNode.inputs[0].link];
if (link) {
link.color = color;
}
}
app.graph.setDirtyCanvas(true, true);
}
applyNodeSize() {
this.properties['size'] = this.properties['size'] || RerouteNode.size;
this.properties['size'] = [Number(this.properties['size'][0]), Number(this.properties['size'][1])];
this.size = this.properties['size'];
app.graph.setDirtyCanvas(true, true);
}
}
RerouteNode.title = "Reroute (rgthree)";
RerouteNode.title_mode = LiteGraph.NO_TITLE;
RerouteNode.collapsable = false;
RerouteNode.layout_slot_offset = 0;
RerouteNode.size = [40, 30];
addConnectionLayoutSupport(RerouteNode, app, [
["Left", "Right"],
["Left", "Top"],
["Left", "Bottom"],
["Right", "Left"],
["Right", "Top"],
["Right", "Bottom"],
["Top", "Left"],
["Top", "Right"],
["Top", "Bottom"],
["Bottom", "Left"],
["Bottom", "Right"],
["Bottom", "Top"],
], (node) => { node.applyNodeSize(); });
addMenuSubMenu(RerouteNode, app, {
name: 'Size',
property: 'size',
options: (() => {
const options = [];
for (let w = 6; w > 1; w--) {
for (let h = 6; h > 1; h--) {
options.push(`${w * 10} x ${h * 10}`);
}
}
return options;
})(),
prepareValue: (value) => value.split(' x ').map(n => Number(n)),
callback: (node) => node.applyNodeSize()
});
LiteGraph.registerNodeType(RerouteNode.title, RerouteNode);
},
});
+129
View File
@@ -0,0 +1,129 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { app } from "../../scripts/app.js";
import { ComfyWidgets } from "../../scripts/widgets.js";
const LAST_SEED_BUTTON_LABEL = '♻️ (Use Last Queued Seed)';
class SeedControl {
constructor(node) {
this.lastSeed = -1;
this.serializedCtx = {};
this.lastSeedValue = null;
this.node = node;
this.node.properties = this.node.properties || {};
for (const [i, w] of this.node.widgets.entries()) {
if (w.name === 'seed') {
this.seedWidget = w;
}
else if (w.name === 'control_after_generate') {
this.node.widgets.splice(i, 1);
}
}
if (!this.seedWidget) {
throw new Error('Something\'s wrong; expected seed widget');
}
const max = Math.min(1125899906842624, this.seedWidget.options.max);
const min = Math.max(-1125899906842624, this.seedWidget.options.min);
const range = (max - min) / (this.seedWidget.options.step / 10);
this.node.addWidget('button', '🎲 Randomize Each Time', null, () => {
this.seedWidget.value = -1;
}, { serialize: false });
this.node.addWidget('button', '🎲 New Fixed Random', null, () => {
this.seedWidget.value = Math.floor(Math.random() * range) * (this.seedWidget.options.step / 10) + min;
}, { serialize: false });
this.lastSeedButton = this.node.addWidget("button", LAST_SEED_BUTTON_LABEL, null, () => {
this.seedWidget.value = this.lastSeed;
this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL;
this.lastSeedButton.disabled = true;
}, { width: 50, serialize: false });
this.lastSeedButton.disabled = true;
this.seedWidget.serializeValue = (node, index) => __awaiter(this, void 0, void 0, function* () {
const currentSeed = this.seedWidget.value;
this.serializedCtx = {
wasRandom: currentSeed == -1,
};
if (this.serializedCtx.wasRandom) {
this.serializedCtx.seedUsed = Math.floor(Math.random() * range) * (this.seedWidget.options.step / 10) + min;
}
else {
this.serializedCtx.seedUsed = this.seedWidget.value;
}
node.widgets_values[index] = this.serializedCtx.seedUsed;
this.seedWidget.value = this.serializedCtx.seedUsed;
if (this.serializedCtx.wasRandom) {
this.lastSeed = this.serializedCtx.seedUsed;
this.lastSeedButton.name = `♻️ ${this.serializedCtx.seedUsed}`;
this.lastSeedButton.disabled = false;
if (this.lastSeedValue) {
this.lastSeedValue.value = `Last Seed: ${this.serializedCtx.seedUsed}`;
}
}
else {
this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL;
this.lastSeedButton.disabled = true;
}
return this.serializedCtx.seedUsed;
});
this.seedWidget.afterQueued = () => {
if (this.serializedCtx.wasRandom) {
this.seedWidget.value = -1;
}
this.serializedCtx = {};
};
this.node.getExtraMenuOptions = (_, options) => {
options.splice(options.length - 1, 0, {
content: "Show/Hide Last Seed Value",
callback: (_value, _options, _event, _parentMenu, _node) => {
this.node.properties['showLastSeed'] = !this.node.properties['showLastSeed'];
if (this.node.properties['showLastSeed']) {
this.addLastSeedValue();
}
else {
this.removeLastSeedValue();
}
}
});
};
}
addLastSeedValue() {
if (this.lastSeedValue)
return;
this.lastSeedValue = ComfyWidgets["STRING"](this.node, "last_seed", ["STRING", { multiline: true }], app).widget;
this.lastSeedValue.inputEl.readOnly = true;
this.lastSeedValue.inputEl.style.fontSize = '0.75rem';
this.lastSeedValue.inputEl.style.textAlign = 'center';
this.lastSeedValue.serializeValue = (node, index) => __awaiter(this, void 0, void 0, function* () {
node.widgets_values[index] = '';
return '';
});
this.node.computeSize();
}
removeLastSeedValue() {
if (!this.lastSeedValue)
return;
this.lastSeedValue.inputEl.remove();
this.node.widgets.splice(this.node.widgets.indexOf(this.lastSeedValue), 1);
this.lastSeedValue = null;
this.node.computeSize();
}
}
app.registerExtension({
name: "rgthree.Seed",
beforeRegisterNodeDef(nodeType, nodeData, _app) {
return __awaiter(this, void 0, void 0, function* () {
if (nodeData.name === "Seed (rgthree)") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
this.seedControl = new SeedControl(this);
};
}
});
},
});
+1
View File
@@ -0,0 +1 @@
"use strict";
+186
View File
@@ -0,0 +1,186 @@
const PADDING = 0;
export const LAYOUT_LABEL_TO_DATA = {
'Left': [LiteGraph.LEFT, [0, 0.5], [PADDING, 0]],
'Right': [LiteGraph.RIGHT, [1, 0.5], [-PADDING, 0]],
'Top': [LiteGraph.UP, [0.5, 0], [0, PADDING]],
'Bottom': [LiteGraph.DOWN, [0.5, 1], [0, -PADDING]],
};
const OPPOSITE_LABEL = {
'Left': 'Right',
'Right': 'Left',
'Top': 'Bottom',
'Bottom': 'Top',
};
export function addMenuItem(node, _app, config) {
const oldGetExtraMenuOptions = node.prototype.getExtraMenuOptions;
node.prototype.getExtraMenuOptions = function (canvas, menuOptions) {
oldGetExtraMenuOptions && oldGetExtraMenuOptions.apply(this, [canvas, menuOptions]);
const idx = menuOptions.findIndex(option => option === null || option === void 0 ? void 0 : option.content.includes('Shape')) + 1;
menuOptions.splice((idx > 0 ? idx : menuOptions.length - 1), 0, {
content: typeof config.name == 'function' ? config.name(this) : config.name,
callback: (_value, _options, _event, _parentMenu, _node) => {
this.properties = this.properties || {};
this.properties[config.property] = config.prepareValue ? config.prepareValue(this.properties[config.property], this) : !this.properties[config.property];
config.callback && config.callback(this);
}
});
};
}
export function addMenuSubMenu(node, _app, config) {
const oldGetExtraMenuOptions = node.prototype.getExtraMenuOptions;
node.prototype.getExtraMenuOptions = function (canvas, menuOptions) {
oldGetExtraMenuOptions && oldGetExtraMenuOptions.apply(this, [canvas, menuOptions]);
const idx = menuOptions.findIndex(option => option === null || option === void 0 ? void 0 : option.content.includes('Shape')) + 1;
menuOptions.splice((idx > 0 ? idx : menuOptions.length - 1), 0, {
content: typeof config.name == 'function' ? config.name(this) : config.name,
has_submenu: true,
callback: (_value, _options, event, parentMenu, _node) => {
new LiteGraph.ContextMenu(config.options.map(option => ({ content: option })), {
event,
parentMenu,
callback: (value, _options, _event, _parentMenu, _node) => {
this.properties = this.properties || {};
this.properties[config.property] = config.prepareValue ? config.prepareValue(value.content, this) : value.content;
config.callback && config.callback(this);
},
});
}
});
};
}
export function addConnectionLayoutSupport(node, app, options = [['Left', 'Right'], ['Right', 'Left']], callback) {
addMenuSubMenu(node, app, {
name: 'Connections Layout',
property: 'connections_layout',
options: options.map(option => option[0] + (option[1] ? ' -> ' + option[1] : '')),
prepareValue: (value, node) => {
var _a;
const values = value.split(' -> ');
if (!values[1] && !((_a = node.outputs) === null || _a === void 0 ? void 0 : _a.length)) {
values[1] = OPPOSITE_LABEL[values[0]];
}
if (!LAYOUT_LABEL_TO_DATA[values[0]] || !LAYOUT_LABEL_TO_DATA[values[1]]) {
throw new Error(`New Layout invalid: [${values[0]}, ${values[1]}]`);
}
return values;
},
callback: (node) => {
callback && callback(node);
app.graph.setDirtyCanvas(true, true);
},
});
node.prototype.getConnectionPos = function (isInput, slotNumber, out) {
return getConnectionPosForLayout(this, isInput, slotNumber, out);
};
}
export function setConnectionsLayout(node, newLayout = ['Left', 'Right']) {
var _a;
if (!newLayout[1] && !((_a = node.outputs) === null || _a === void 0 ? void 0 : _a.length)) {
newLayout[1] = OPPOSITE_LABEL[newLayout[0]];
}
if (!LAYOUT_LABEL_TO_DATA[newLayout[0]] || !LAYOUT_LABEL_TO_DATA[newLayout[1]]) {
throw new Error(`New Layout invalid: [${newLayout[0]}, ${newLayout[1]}]`);
}
node.properties = node.properties || {};
node.properties['connections_layout'] = newLayout;
}
export function setConnectionsCollapse(node, collapseConnections = null) {
node.properties = node.properties || {};
collapseConnections = collapseConnections !== null ? collapseConnections : !node.properties['collapse_connections'];
node.properties['collapse_connections'] = collapseConnections;
}
export function getConnectionPosForLayout(node, isInput, slotNumber, out) {
var _a, _b, _c;
out = out || new Float32Array(2);
node.properties = node.properties || {};
const layout = node.properties['connections_layout'] || ['Left', 'Right'];
const collapseConnections = node.properties['collapse_connections'] || false;
const offset = (_a = node.constructor.layout_slot_offset) !== null && _a !== void 0 ? _a : (LiteGraph.NODE_SLOT_HEIGHT * 0.5);
const side = isInput ? layout[0] : layout[1];
const data = LAYOUT_LABEL_TO_DATA[side];
const cxn = node[isInput ? 'inputs' : 'outputs'][slotNumber];
if (!cxn) {
console.log('No connection found.. weird', isInput, slotNumber);
return out;
}
cxn.dir = data[0];
if (side === 'Left') {
if (node.flags.collapsed) {
var w = node._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH;
out[0] = node.pos[0];
out[1] = node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5;
}
else {
if (!isInput && !cxn.has_old_label) {
cxn.has_old_label = true;
cxn.old_label = cxn.label;
cxn.label = ' ';
}
else if (isInput && cxn.has_old_label) {
cxn.has_old_label = false;
cxn.label = cxn.old_label;
cxn.old_label = undefined;
}
out[0] = node.pos[0] + offset;
if ((_b = node.constructor) === null || _b === void 0 ? void 0 : _b.type.includes('Reroute')) {
out[1] = node.pos[1] + (node.size[1] * .5);
}
else {
const displaySlot = collapseConnections ? 0 : slotNumber;
out[1] =
node.pos[1] +
(displaySlot + 0.7) * LiteGraph.NODE_SLOT_HEIGHT +
(node.constructor.slot_start_y || 0);
}
}
}
else if (side === 'Right') {
if (node.flags.collapsed) {
var w = node._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH;
out[0] = node.pos[0] + w;
out[1] = node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5;
}
else {
if (isInput && !cxn.has_old_label) {
cxn.has_old_label = true;
cxn.old_label = cxn.label;
cxn.label = ' ';
}
else if (!isInput && cxn.has_old_label) {
cxn.has_old_label = false;
cxn.label = cxn.old_label;
cxn.old_label = undefined;
}
out[0] = node.pos[0] + node.size[0] + 1 - offset;
if ((_c = node.constructor) === null || _c === void 0 ? void 0 : _c.type.includes('Reroute')) {
out[1] = node.pos[1] + (node.size[1] * .5);
}
else {
const displaySlot = collapseConnections ? 0 : slotNumber;
out[1] =
node.pos[1] +
(displaySlot + 0.7) * LiteGraph.NODE_SLOT_HEIGHT +
(node.constructor.slot_start_y || 0);
}
}
}
else if (side === 'Top') {
if (!cxn.has_old_label) {
cxn.has_old_label = true;
cxn.old_label = cxn.label;
cxn.label = ' ';
}
out[0] = node.pos[0] + (node.size[0] * .5);
out[1] = node.pos[1] + offset;
}
else if (side === 'Bottom') {
if (!cxn.has_old_label) {
cxn.has_old_label = true;
cxn.old_label = cxn.label;
cxn.label = ' ';
}
out[0] = node.pos[0] + (node.size[0] * .5);
out[1] = node.pos[1] + node.size[1] - offset;
}
return out;
}
+25
View File
@@ -0,0 +1,25 @@
{
"name": "rgthree-comfy",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"typescript": "^5.1.6"
}
},
"node_modules/typescript": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
"integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"devDependencies": {
"typescript": "^5.1.6"
}
}
+4
View File
@@ -0,0 +1,4 @@
category_prefix = 'rgthree'
def get_name(name):
return '{} ({})'.format(name, category_prefix)
+58
View File
@@ -0,0 +1,58 @@
from constants import category_prefix, get_name
ctx_keys = ["model", "clip", "vae", "positive", "negative", "latent", "images", "seed"]
def new_context(context=None, model=None, clip=None, vae=None, positive=None, negative=None, latent=None, images=None, seed=None):
ctx = {}
for key in ctx_keys:
v = None
v = v if v != None else model if key == 'model' else None
v = v if v != None else clip if key == 'clip' else None
v = v if v != None else vae if key == 'vae' else None
v = v if v != None else positive if key == 'positive' else None
v = v if v != None else negative if key == 'negative' else None
v = v if v != None else latent if key == 'latent' else None
v = v if v != None else images if key == 'images' else None
v = v if v != None else seed if key == 'seed' else None
ctx[key] = a_b(v, d_k(context, key))
return ctx
def d_k(dct, key, default=None):
return dct[key] if dct != None and key in dct else default
def a_b(a, b):
return a if a != None else b
class RgthreeContext:
@classmethod
def INPUT_TYPES(s):
return {
"required": {},
"optional": {
"base_ctx": ("RGTHREE_CONTEXT",),
"model": ("MODEL",),
"clip": ("CLIP",),
"vae": ("VAE",),
"positive": ("CONDITIONING",),
"negative": ("CONDITIONING",),
"latent": ("LATENT",),
"images": ("IMAGE", ),
"seed": ("INT", {"forceInput": True}),
},
"hidden": {
"prompt": "PROMPT",
},
}
RETURN_TYPES = ("RGTHREE_CONTEXT", "MODEL", "CLIP", "VAE", "CONDITIONING", "CONDITIONING", "LATENT", "IMAGE", "INT",)
RETURN_NAMES = ("CONTEXT", "MODEL", "CLIP", "VAE", "POSITIVE", "NEGATIVE", "LATENT", "IMAGE", "SEED",)
FUNCTION = "convert"
CATEGORY = "%s/utils" % category_prefix
def convert(self, base_ctx=None, model=None, clip=None, vae=None, positive=None, negative=None, latent=None, images=None, seed=None, prompt=None):
ctx = new_context(context=base_ctx, model=model, clip=clip, vae=vae, positive=positive, negative=negative, latent=latent, images=images, seed=seed)
return (ctx, ctx['model'], ctx['clip'], ctx['vae'], ctx['positive'], ctx['negative'], ctx['latent'], ctx['images'], ctx['seed'],)
NODE_CLASS_MAPPINGS = {}
NODE_CLASS_MAPPINGS[get_name('Context')] = RgthreeContext
+38
View File
@@ -0,0 +1,38 @@
from constants import category_prefix, get_name
class RgthreeContextSwitch:
@classmethod
def INPUT_TYPES(s):
return {
"required": {},
"optional": {
"ctx_01": ("RGTHREE_CONTEXT",),
"ctx_02": ("RGTHREE_CONTEXT",),
"ctx_03": ("RGTHREE_CONTEXT",),
"ctx_04": ("RGTHREE_CONTEXT",),
},
"hidden": {
"prompt": "PROMPT",
},
}
RETURN_TYPES = ("RGTHREE_CONTEXT",)
RETURN_NAMES = ("CONTEXT",)
FUNCTION = "switch"
CATEGORY = "%s/utils" % category_prefix
def switch(self, ctx_01=None, ctx_02=None, ctx_03=None, ctx_04=None, prompt=None):
if ctx_01 != None:
return (ctx_01,)
if ctx_02 != None:
return (ctx_02,)
if ctx_03 != None:
return (ctx_03,)
if ctx_04 != None:
return (ctx_04,)
return (None,)
NODE_CLASS_MAPPINGS = {}
NODE_CLASS_MAPPINGS[get_name('Context Switch')] = RgthreeContextSwitch
+23
View File
@@ -0,0 +1,23 @@
from constants import category_prefix, get_name
class RgthreeDisplayInt:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input": ("INT", {"forceInput": True}),
},
}
RETURN_TYPES = ()
FUNCTION = "main"
OUTPUT_NODE = True
CATEGORY = "%s/utils" % category_prefix
def main(self, input=None):
return {"ui": {"text": (input,)}}
NODE_CLASS_MAPPINGS = {}
NODE_CLASS_MAPPINGS[get_name('Display Int')] = RgthreeDisplayInt
+48
View File
@@ -0,0 +1,48 @@
from constants import category_prefix, get_name
from nodes import LoraLoader
import folder_paths
class RgthreeLoraLoaderStack:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MODEL",),
"clip": ("CLIP", ),
"lora_01": (['None'] + folder_paths.get_filename_list("loras"), ),
"strength_01":("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"lora_02": (['None'] + folder_paths.get_filename_list("loras"), ),
"strength_02":("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"lora_03": (['None'] + folder_paths.get_filename_list("loras"), ),
"strength_03":("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"lora_04": (['None'] + folder_paths.get_filename_list("loras"), ),
"strength_04":("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
}
}
RETURN_TYPES = ("MODEL", "CLIP")
FUNCTION = "load_lora"
CATEGORY = "%s/loaders" % category_prefix
def load_lora(self, model, clip, lora_01, strength_01, lora_02, strength_02, lora_03, strength_03, lora_04, strength_04):
if lora_01 != "None" and strength_01 != 0:
model, clip = LoraLoader().load_lora(model, clip, lora_01, strength_01, strength_01)
if lora_02 != "None" and strength_02 != 0:
model, clip = LoraLoader().load_lora(model, clip, lora_02, strength_02, strength_02)
if lora_03 != "None" and strength_03 != 0:
model, clip = LoraLoader().load_lora(model, clip, lora_03, strength_03, strength_03)
if lora_04 != "None" and strength_04 != 0:
model, clip = LoraLoader().load_lora(model, clip, lora_04, strength_04, strength_04)
return (model, clip)
NODE_CLASS_MAPPINGS = {}
NODE_CLASS_MAPPINGS[get_name('Lora Loader Stack')] = RgthreeLoraLoaderStack
+23
View File
@@ -0,0 +1,23 @@
from constants import category_prefix, get_name
class RgthreeSeed:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"seed": ("INT", {"default": 0, "min": 0, "max": 1125899906842624}),
},
}
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("SEED",)
FUNCTION = "main"
CATEGORY = "%s/utils" % category_prefix
def main(self, seed=0):
return (seed,)
NODE_CLASS_MAPPINGS = {}
NODE_CLASS_MAPPINGS[get_name('Seed')] = RgthreeSeed
View File
+23
View File
@@ -0,0 +1,23 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import {app} from "../../scripts/app.js";
// @ts-ignore
import { ComfyWidgets } from "../../scripts/widgets.js";
import type {LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js';
import type {ComfyApp, ComfyObjectInfo} from './typings/comfy.js'
import { addConnectionLayoutSupport } from "./utils.js";
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
app.registerExtension({
name: "rgthree.Context",
async beforeRegisterNodeDef(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, app: ComfyApp) {
if (nodeData.name === "Context (rgthree)") {
// This isn't super useful, because R->L removes the names in order to work with
// litegraph's hardcoded L->R math.. but, ¯\_(ツ)_/¯
addConnectionLayoutSupport(nodeType, app, [['Left', 'Right'], ['Right', 'Left']]);
}
},
});
+44
View File
@@ -0,0 +1,44 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import {app} from "../../scripts/app.js";
// @ts-ignore
import { ComfyWidgets } from "../../scripts/widgets.js";
import type {SerializedLGraphNode, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js';
import type {ComfyApp, ComfyObjectInfo, ComfyWidget} from './typings/comfy.js'
import { addConnectionLayoutSupport } from "./utils.js";
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
app.registerExtension({
name: "rgthree.DisplayInt",
async beforeRegisterNodeDef(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, app: ComfyApp) {
if (nodeData.name === "Display Int (rgthree)") {
let showValueWidget!: ComfyWidget;
(nodeType as any).title_mode = LiteGraph.NO_TITLE;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
showValueWidget = ComfyWidgets["STRING"](this, "output", ["STRING", { multiline: true }], app).widget;
showValueWidget.inputEl!.readOnly = true;
showValueWidget.serializeValue = async (node: SerializedLGraphNode, index: number) => {
// Since we need a round trip to get the value, the serizalized value means nothing, and
// saving it to the metadata would just be confusing. So, we clear it here.
node.widgets_values![index] = '';
return '';
}
}
addConnectionLayoutSupport(nodeType, app, [['Left'],['Right']]);
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, [message]);
showValueWidget.value = message.text[0];
};
}
},
});
+138
View File
@@ -0,0 +1,138 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import {app} from "../../scripts/app.js";
import type {Vector2, LLink, LGraph, INodeInputSlot, INodeOutputSlot, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js';
import { addConnectionLayoutSupport, addMenuItem } from "./utils.js";
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
const MUTE_MODE = 2;
const ALWAYS_MODE = 0;
app.registerExtension({
name: "rgthree.Muter",
registerCustomNodes() {
class CustomNode extends LGraphNode {
static override title = "Fast Muter (rgthree)";
static collapsible = false;
static category = "rgthree/utils";
debouncer: number = 0;
schedulePromise: Promise<void> | null = null;
isVirtualNode = true;
constructor(title = CustomNode.title) {
super(title);
this.properties = this.properties || {};
this.connections = [];
this.addInput("", "*");
}
private doChainLookup(startNode: TLGraphNode = this) {
let rootNodes: TLGraphNode[] = [];
const type = (startNode.constructor as typeof TLGraphNode).type;
if (startNode === this || type?.includes('Reroute') || type?.includes('Combiner')) {
for (const input of startNode.inputs) {
const linkId: number | null = input!.link;
if (!linkId) {
continue;
}
const link: LLink = (app.graph as LGraph).links[linkId]!;
const originNode: TLGraphNode = (app.graph as LGraph).getNodeById(link.origin_id)!;
const foundNodes = this.doChainLookup(originNode);
rootNodes = rootNodes.concat(foundNodes);
}
} else if (!type?.includes('Reroute') && !type?.includes('Combiner')) {
// We found our node.
rootNodes.push(startNode);
}
return rootNodes;
}
scheduleRefreshMutables() {
if (!this.schedulePromise) {
this.schedulePromise = new Promise((resolve) => {
setTimeout(() => {
resolve(this.refreshMutables());
this.schedulePromise = null;
}, 100);
});
}
return this.schedulePromise;
}
refreshMutables() {
this.stabilizeInputsOutputs();
const mutables = this.doChainLookup();
for (const [index, node] of mutables.entries()) {
let widget = this.widgets && this.widgets[index];
if (!widget) {
widget = this.addWidget("toggle", 'title', false, '', {"on": 'yes', "off": 'no'});
}
const muted = node.mode === MUTE_MODE;
widget.name = `Enable ${node.title}`;
widget.value = !muted;
widget.callback = () => {
const muted = node.mode === MUTE_MODE;
node.mode = muted ? ALWAYS_MODE : MUTE_MODE;
widget!.value = muted;
}
}
this.widgets.length = mutables.length;
app.graph.setDirtyCanvas(true, true);
}
onConnectionsChainChange() {
this.scheduleRefreshMutables();
}
override onConnectionsChange(_type: number, _index: number, _connected: boolean, _linkInfo: LLink, _ioSlot: (INodeOutputSlot | INodeInputSlot)) {
this.scheduleRefreshMutables();
}
private stabilizeInputsOutputs() {
for (let index = this.inputs.length - 1; index >= 0; index--) {
const input = this.inputs[index]!;
if (!input.link) {
this.removeInput(index);
}
}
this.addInput('', '*');
}
override computeSize(out: Vector2) {
let size = super.computeSize(out);
// If we're collapsed, then subtract the total calculated height of the other input slots.
if (this.properties['collapse_connections']) {
const rows = Math.max(this.inputs?.length || 0, this.outputs?.length || 0, 1) - 1;
size[1] = size[1] - (rows * LiteGraph.NODE_SLOT_HEIGHT);
}
setTimeout(() => {
app.graph.setDirtyCanvas(true, true);
}, 16);
return size;
}
}
// @ts-ignore: Fix incorrect litegraph typings.
addConnectionLayoutSupport(CustomNode, app, [['Left'],['Right']]);
// @ts-ignore: Fix incorrect litegraph typings.
addMenuItem(CustomNode, app, {
name: (node) => (`${node.properties?.['collapse_connections'] ? 'Show' : 'Collapse'} Connections`),
property: 'collapse_connections',
prepareValue: (_value, node) => !node.properties?.['collapse_connections'],
callback: (_node) => {app.graph.setDirtyCanvas(true, true)}
});
// @ts-ignore: Fix incorrect litegraph typings.
addMenuItem(CustomNode, app, {
name: 'Refresh',
callback: (node) => {(node as CustomNode).scheduleRefreshMutables()}
});
LiteGraph.registerNodeType(CustomNode.title, CustomNode);
},
});
+81
View File
@@ -0,0 +1,81 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import { app } from "../../scripts/app.js";
import type {LLink, LGraph, INodeInputSlot, INodeOutputSlot, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js';
import { addConnectionLayoutSupport } from "./utils.js";
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
app.registerExtension({
name: "rgthree.NodeCombiner",
registerCustomNodes() {
class CombinerNode extends LGraphNode {
static override title = "Node Combiner (rgthree)";
static category = "rgthree"
isVirtualNode = true;
constructor(title = CombinerNode.title) {
super(title);
this.properties = this.properties || {};
this.connections = [];
this.addInput("", "*");
this.addOutput("Output", "*");
}
override clone() {
const cloned = super.clone();
return cloned;
}
private updateOutputLinks(startNode: TLGraphNode = this) {
const type = (startNode.constructor as typeof TLGraphNode).type;
// @ts-ignore
if (startNode.onConnectionsChainChange) {
// @ts-ignore
startNode.onConnectionsChainChange();
}
if (startNode === this || type?.includes('Reroute') || type?.includes('Combiner')) {
for (const output of startNode.outputs) {
if (!output.links || !output.links.length) continue;
for (const linkId of output.links) {
const link: LLink = (app.graph as LGraph).links[linkId]!;
const targetNode: TLGraphNode = (app.graph as LGraph).getNodeById(link.target_id)!;
targetNode && this.updateOutputLinks(targetNode)
}
}
}
}
override onConnectionsChange(_type: number, _slotIndex: number, _isConnected: boolean, link_info: LLink, _ioSlot: (INodeOutputSlot | INodeInputSlot)) {
if (!link_info) return;
this.stabilizeInputsOutputs();
// Follow outputs to see if we need to trigger an onConnectionChange.
this.updateOutputLinks();
}
private stabilizeInputsOutputs() {
for (let index = this.inputs.length - 1; index >= 0; index--) {
const input = this.inputs[index]!;
if (!input.link) {
this.removeInput(index);
}
}
this.addInput('', '*');
const outputLength = this.outputs[0]?.links?.length || 0;
if (outputLength > 1) {
this.outputs[0]!.links!.length = 1;
}
}
}
// @ts-ignore: Fix incorrect litegraph typings.
addConnectionLayoutSupport(CombinerNode, app, [['Left','Right'],['Right','Left']]);
LiteGraph.registerNodeType(CombinerNode.title, CombinerNode);
},
});
+214
View File
@@ -0,0 +1,214 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import { app } from "../../scripts/app.js";
import type {Vector2, LLink, LGraphCanvas as TLGraphCanvas, LGraph, SerializedLGraphNode, INodeInputSlot, INodeOutputSlot, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js';
import { addConnectionLayoutSupport, addMenuSubMenu } from "./utils.js";
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
declare const LGraphCanvas: typeof TLGraphCanvas;
app.registerExtension({
name: "rgthree.Reroute",
registerCustomNodes() {
class RerouteNode extends LGraphNode {
static override title = "Reroute (rgthree)";
static readonly title_mode = LiteGraph.NO_TITLE;
static collapsable = false;
static layout_slot_offset = 0;
static size: Vector2 = [40, 30]; // Starting size, read from within litegraph.core
readonly isVirtualNode?: boolean;
constructor(title = RerouteNode.title) {
super(title);
this.isVirtualNode = true;
this.resizable = false;
this.size = RerouteNode.size; // Starting size.
this.addInput("", "*");
this.addOutput("", "*");
setTimeout(() => this.applyNodeSize(), 20);
}
override configure(info: SerializedLGraphNode) {
super.configure(info);
this.applyNodeSize();
}
override clone() {
const cloned = super.clone();
cloned.inputs[0]!.type = '*';
cloned.outputs[0]!.type = '*';
return cloned;
}
/**
* Copied a good bunch of this from the original reroute included with comfy.
*/
override onConnectionsChange(type: number, _slotIndex: number, connected: boolean, _link_info: LLink, _ioSlot: (INodeOutputSlot | INodeInputSlot)) {
// Prevent multiple connections to different types when we have no input
if (connected && type === LiteGraph.OUTPUT) {
// Ignore wildcard nodes as these will be updated to real types
const types = new Set(this.outputs[0]!.links!.map((l) => app.graph.links[l].type).filter((t) => t !== "*"));
if (types.size > 1) {
const linksToDisconnect = [];
for (let i = 0; i < this.outputs[0]!.links!.length - 1; i++) {
const linkId = this.outputs[0]!.links![i];
const link = app.graph.links[linkId];
linksToDisconnect.push(link);
}
for (const link of linksToDisconnect) {
const node = app.graph.getNodeById(link.target_id);
node.disconnectInput(link.target_slot);
}
}
}
// Find root input
let currentNode: TLGraphNode|null = this;
let updateNodes = [];
let inputType = null;
let inputNode = null;
while (currentNode) {
updateNodes.unshift(currentNode);
const linkId: number | null = currentNode.inputs[0]!.link;
if (linkId !== null) {
const link: LLink = (app.graph as LGraph).links[linkId]!;
const node: TLGraphNode = (app.graph as LGraph).getNodeById(link.origin_id)!;
const type = (node.constructor as typeof TLGraphNode).type;
if (type?.includes("Reroute")) {
if (node === this) {
// We've found a circle
currentNode.disconnectInput(link.target_slot);
currentNode = null;
}
else {
// Move the previous node
currentNode = node;
}
} else {
// We've found the end
inputNode = currentNode;
inputType = node.outputs[link.origin_slot]?.type ?? null;
break;
}
} else {
// This path has no input node
currentNode = null;
break;
}
}
// Find all outputs
const nodes: TLGraphNode[] = [this];
let outputType = null;
while (nodes.length) {
currentNode = nodes.pop()!;
const outputs = (currentNode.outputs ? currentNode.outputs[0]!.links : []) || [];
if (outputs.length) {
for (const linkId of outputs) {
const link = app.graph.links[linkId];
// When disconnecting sometimes the link is still registered
if (!link) continue;
const node = app.graph.getNodeById(link.target_id);
const type = node.constructor.type;
if (type?.includes("Reroute")) {
// Follow reroute nodes
nodes.push(node);
updateNodes.push(node);
} else {
// We've found an output
const nodeOutType = node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type ? node.inputs[link.target_slot].type : null;
if (inputType && nodeOutType !== inputType && nodeOutType !== '*') {
// The output doesnt match our input so disconnect it
node.disconnectInput(link.target_slot);
} else {
outputType = nodeOutType;
}
}
}
} else {
// No more outputs for this path
}
}
const displayType = inputType || outputType || "*";
const color = LGraphCanvas.link_type_colors[displayType];
// Update the types of each node
for (const node of updateNodes) {
// If we dont have an input type we are always wildcard but we'll show the output type
// This lets you change the output link to a different type and all nodes will update
node.outputs[0].type = inputType || "*";
node.__outputType = displayType;
node.outputs[0].name = node.properties.showOutputText ? displayType : "";
node.size = node.computeSize();
node.applyNodeSize();
for (const l of node.outputs[0].links || []) {
const link = app.graph.links[l];
if (link) {
link.color = color;
}
}
}
if (inputNode) {
const link = app.graph.links[inputNode.inputs[0]!.link];
if (link) {
link.color = color;
}
}
app.graph.setDirtyCanvas(true, true);
}
applyNodeSize() {
this.properties['size'] = this.properties['size'] || RerouteNode.size;
this.properties['size'] = [Number(this.properties['size'][0]), Number(this.properties['size'][1])];
this.size = this.properties['size'];
app.graph.setDirtyCanvas(true, true);
}
}
// @ts-ignore: Fix incorrect litegraph typings.
addConnectionLayoutSupport(RerouteNode, app, [
["Left","Right"],
["Left","Top"],
["Left","Bottom"],
["Right","Left"],
["Right","Top"],
["Right","Bottom"],
["Top","Left"],
["Top","Right"],
["Top","Bottom"],
["Bottom","Left"],
["Bottom","Right"],
["Bottom","Top"],
], (node) => {(node as RerouteNode).applyNodeSize();});
// @ts-ignore: Fix incorrect litegraph typings.
addMenuSubMenu(RerouteNode, app, {
name: 'Size',
property: 'size',
options: (() => {
const options = [];
for (let w = 6; w > 1; w--) {
for (let h = 6; h > 1; h--) {
options.push(`${w * 10} x ${h * 10}`);
}
}
return options;
})(),
prepareValue: (value: string) => value.split(' x ').map(n => Number(n)),
callback: (node) => (node as RerouteNode).applyNodeSize()
});
LiteGraph.registerNodeType(RerouteNode.title, RerouteNode);
},
});
+167
View File
@@ -0,0 +1,167 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import {app} from "../../scripts/app.js";
// @ts-ignore
import { ComfyWidgets } from "../../scripts/widgets.js";
import type {SerializedLGraphNode, ContextMenuItem, IContextMenuOptions, ContextMenu, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, IWidget} from './typings/litegraph.js';
import type {ComfyApp, ComfyObjectInfo, ComfyWidget, ComfyGraphNode} from './typings/comfy.js'
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
const LAST_SEED_BUTTON_LABEL = '♻️ (Use Last Queued Seed)';
interface SeedSerializedCtx {
wasRandom?: boolean;
seedUsed?: number;
}
/** Wraps a node instance keeping closure without mucking the finicky types. */
class SeedControl {
readonly node: ComfyGraphNode;
lastSeed = -1;
serializedCtx: SeedSerializedCtx = {};
seedWidget: ComfyWidget;
lastSeedButton: ComfyWidget;
lastSeedValue: ComfyWidget|null = null;
constructor(node: ComfyGraphNode) {
this.node = node;
this.node.properties = this.node.properties || {};
// Grab the already available widgets, and remove the built-in control_after_generate
for (const [i, w] of this.node.widgets.entries()) {
if (w.name === 'seed') {
this.seedWidget = w as ComfyWidget;
} else if (w.name === 'control_after_generate') {
this.node.widgets.splice(i, 1);
}
}
// @ts-ignore
if (!this.seedWidget) {
throw new Error('Something\'s wrong; expected seed widget');
}
const max = Math.min(1125899906842624, this.seedWidget.options.max);
const min = Math.max(-1125899906842624, this.seedWidget.options.min);
const range = (max - min) / (this.seedWidget.options.step / 10);
this.node.addWidget('button', '🎲 Randomize Each Time', null, () => {
this.seedWidget.value = -1;
}, {serialize: false}) as ComfyWidget;
this.node.addWidget('button', '🎲 New Fixed Random', null, () => {
this.seedWidget.value = Math.floor(Math.random() * range) * (this.seedWidget.options.step / 10) + min;
}, {serialize: false});
this.lastSeedButton = this.node.addWidget("button", LAST_SEED_BUTTON_LABEL, null, () => {
this.seedWidget.value = this.lastSeed;
this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL;
this.lastSeedButton.disabled = true;
}, {width: 50, serialize: false});
this.lastSeedButton.disabled = true;
/**
* When we serialize the value, check if our seed widget is -1 and, if so, generate
* a random number and set that to the input value. Also, set it in the passed graph node
* for serialization, so it's saved in the image metadata. When re-opening the window, the
* seed value will be pre-filled, instead of `-1`.
*/
this.seedWidget.serializeValue = async (node: SerializedLGraphNode, index: number) => {
const currentSeed = this.seedWidget.value;
this.serializedCtx = {
wasRandom: currentSeed == -1,
}
if (this.serializedCtx.wasRandom) {
this.serializedCtx.seedUsed = Math.floor(Math.random() * range) * (this.seedWidget.options.step / 10) + min;
} else {
this.serializedCtx.seedUsed = this.seedWidget.value;
}
node.widgets_values![index] = this.serializedCtx.seedUsed;
this.seedWidget.value = this.serializedCtx.seedUsed;
// Enabled the 'Last seed' Button
if (this.serializedCtx.wasRandom) {
this.lastSeed = this.serializedCtx.seedUsed!;
this.lastSeedButton.name = `♻️ ${this.serializedCtx.seedUsed}`
this.lastSeedButton.disabled = false;
if (this.lastSeedValue) {
this.lastSeedValue.value = `Last Seed: ${this.serializedCtx.seedUsed}`;
}
} else {
this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL;
this.lastSeedButton.disabled = true;
}
return this.serializedCtx.seedUsed;
}
/**
* After the widget has been queued, change back to "-1" if we started as "-1".
*/
this.seedWidget.afterQueued = () => {
if (this.serializedCtx.wasRandom) {
this.seedWidget.value = -1;
}
this.serializedCtx = {};
}
this.node.getExtraMenuOptions = (_: TLGraphNode, options: ContextMenuItem[]) => {
options.splice(options.length - 1, 0,
{
content: "Show/Hide Last Seed Value",
callback: (_value: ContextMenuItem, _options: IContextMenuOptions, _event: MouseEvent, _parentMenu: ContextMenu | undefined, _node: TLGraphNode) => {
this.node.properties['showLastSeed'] = !this.node.properties['showLastSeed'];
if (this.node.properties['showLastSeed']) {
this.addLastSeedValue();
} else {
this.removeLastSeedValue();
}
}
}
);
}
}
addLastSeedValue() {
if (this.lastSeedValue) return;
this.lastSeedValue = ComfyWidgets["STRING"](this.node, "last_seed", ["STRING", { multiline: true }], app).widget;
this.lastSeedValue!.inputEl!.readOnly = true;
this.lastSeedValue!.inputEl!.style.fontSize = '0.75rem';
this.lastSeedValue!.inputEl!.style.textAlign = 'center';
this.lastSeedValue!.serializeValue = async (node: SerializedLGraphNode, index: number) => {
node.widgets_values![index] = '';
return '';
}
this.node.computeSize();
}
removeLastSeedValue() {
if (!this.lastSeedValue) return;
this.lastSeedValue!.inputEl!.remove();
this.node.widgets.splice(this.node.widgets.indexOf(this.lastSeedValue as IWidget), 1);
this.lastSeedValue = null;
this.node.computeSize();
}
}
app.registerExtension({
name: "rgthree.Seed",
async beforeRegisterNodeDef(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, _app: ComfyApp) {
if (nodeData.name === "Seed (rgthree)") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
(this as any).seedControl = new SeedControl(this as ComfyGraphNode);
}
}
},
});
+83
View File
@@ -0,0 +1,83 @@
/**
* This one would be cool, but the server doesn't like it. Would need to patch ComfyUi
* to allow a "trust me" input type in its graph checking.
*/
// // @ts-ignore
// import {app} from "../../scripts/app.js";
// import type {Vector2, LLink, SerializedLGraphNode, ContextMenuItem, IContextMenuOptions, ContextMenu, INodeInputSlot, INodeOutputSlot, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js';
// import type {ComfyObjectInfo, ComfyApp} from './typings/comfy.js'
// declare const LiteGraph: typeof TLiteGraph;
// declare const LGraphNode: typeof TLGraphNode;
// app.registerExtension({
// name: "rgthree.SmartSwitch",
// async beforeRegisterNodeDef(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, app: ComfyApp) {
// if (nodeData.name === "Smart Switch (rgthree)") {
// console.log(nodeData);
// console.dir(nodeType);
// const onNodeCreated = nodeType.prototype.onNodeCreated;
// nodeType.prototype.onNodeCreated = function () {
// const r = onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
// // Add the initial input
// if (!this.inputs || !this.inputs.length) {
// this.addInput("", "*");
// this.outputs[0].name = '';
// }
// }
// const onConnectionsChange = nodeType.prototype.onConnectionsChange;
// nodeType.prototype.onConnectionsChange = function(type: number, slotIndex: number, isConnected: boolean, link_info: LLink, ioSlot: (INodeOutputSlot | INodeInputSlot)) {
// const r = onConnectionsChange ? onConnectionsChange.apply(this, [type, slotIndex, isConnected, link_info,ioSlot]) : undefined;
// if (!link_info) {
// return;
// }
// console.log(type, index, connected, link_info);
// if (type === 1) {
// if (connected) {
// const connectedOutput = app.graph.getNodeById(link_info.origin_id).outputs[link_info.origin_slot];
// console.log(connectedOutput);
// if (this.inputs.length === 1) {
// this.inputs[0].name = connectedOutput.type.toLowerCase() + '_1';
// this.inputs[0].type = connectedOutput.type;
// // Add an output of the same type.
// this.outputs[0].name = this.inputs[0].type;
// this.outputs[0].type = this.inputs[0].type;
// }
// // Add another input of the same type.
// this.addInput(`${connectedOutput.type.toLowerCase()}_${index+2}`, this.inputs[0].type);
// } else {
// this.inputs.splice(index, 1);
// // If we removed the last input and there's no outputs, then clear.
// if (this.inputs.length === 1 && !this.outputs[0].links.length) {
// this.inputs[0].name = '';
// this.inputs[0].type = '*';
// this.outputs[0].name = '';
// this.outputs[0].type = '*';
// }
// }
// } else if (type === 2) {
// if (connected && this.inputs[0].type === '*') {
// const connectedInput = app.graph.getNodeById(link_info.target_id).outputs[link_info.target_slot];
// this.inputs[0].name = connectedInput.type.toLowerCase() + '_1';
// this.inputs[0].type = connectedInput.type;
// this.outputs[0].name = this.inputs[0].type;
// this.outputs[0].type = this.inputs[0].type;
// // If we removed the ouput and there's no connected inputs, then clear.
// } else if (!connected && this.inputs.length === 1 && this.inputs[0].links.length) {
// this.inputs[0].name = '';
// this.inputs[0].type = '*';
// this.outputs[0].name = '';
// this.outputs[0].type = '*';
// }
// }
// }
// }
// },
// });
+3
View File
@@ -0,0 +1,3 @@
The typings in node_modules or in ComfyUI's web/ directory were not that well covered. These typings are hacked together with some of the inconsistencies I found.
To be honest, I have no idea why I needed a bizarre workaround for litegraph's types. Usually the '/// &lt;reference>' comment should have picked up the types, but it wasn't having it. ¯\_(ツ)_/¯
+90
View File
@@ -0,0 +1,90 @@
import { LGraphNode, IWidget, SerializedLGraphNode } from "./litegraph";
import { ComfyApp } from "../../../../web/scripts/app";
export { ComfyApp } from "../../../../web/scripts/app";
export interface ComfyWidget extends IWidget {
serializeValue(nodeType: SerializedLGraphNode, index: number): Promise<TValue>;
afterQueued(): void;
inputEl?: HTMLTextAreaElement;
width: number;
}
export interface ComfyGraphNode extends LGraphNode {
getExtraMenuOptions: (node: TLGraphNode, options: ContextMenuItem[]) => void;
onExecuted(message: any): void;
}
export interface ComfyExtension {
/**
* The name of the extension
*/
name: string;
/**
* Allows any initialisation, e.g. loading resources. Called after the canvas is created but before nodes are added
* @param app The ComfyUI app instance
*/
init(app: ComfyApp): Promise<void>;
/**
* Allows any additonal setup, called after the application is fully set up and running
* @param app The ComfyUI app instance
*/
setup(app: ComfyApp): Promise<void>;
/**
* Called before nodes are registered with the graph
* @param defs The collection of node definitions, add custom ones or edit existing ones
* @param app The ComfyUI app instance
*/
addCustomNodeDefs(defs: Record<string, ComfyObjectInfo>, app: ComfyApp): Promise<void>;
/**
* Allows the extension to add custom widgets
* @param app The ComfyUI app instance
* @returns An array of {[widget name]: widget data}
*/
getCustomWidgets(
app: ComfyApp
): Promise<
Record<string, (node, inputName, inputData, app) => { widget?: IWidget; minWidth?: number; minHeight?: number }>
>;
/**
* Allows the extension to add additional handling to the node before it is registered with LGraph
* @param nodeType The node class (not an instance)
* @param nodeData The original node object info config object
* @param app The ComfyUI app instance
*/
beforeRegisterNodeDef(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, app: ComfyApp): Promise<void>;
/**
* Allows the extension to register additional nodes with LGraph after standard nodes are added
* @param app The ComfyUI app instance
*/
registerCustomNodes(app: ComfyApp): Promise<void>;
/**
* Allows the extension to modify a node that has been reloaded onto the graph.
* If you break something in the backend and want to patch workflows in the frontend
* This is the place to do this
* @param node The node that has been loaded
* @param app The ComfyUI app instance
*/
loadedGraphNode(node: LGraphNode, app: ComfyApp);
/**
* Allows the extension to run code after the constructor of the node
* @param node The node that has been created
* @param app The ComfyUI app instance
*/
nodeCreated(node: LGraphNode, app: ComfyApp);
}
export type ComfyObjectInfo = {
name: string;
display_name?: string;
description?: string;
category: string;
input?: {
required?: Record<string, ComfyObjectInfoConfig>;
optional?: Record<string, ComfyObjectInfoConfig>;
};
output?: string[];
output_name: string[];
};
export type ComfyObjectInfoConfig = [string | any[]] | [string | any[], any];
+1515
View File
File diff suppressed because it is too large Load Diff
+220
View File
@@ -0,0 +1,220 @@
import type {ComfyApp} from './typings/comfy';
import {Vector2, LGraphCanvas as TLGraphCanvas, ContextMenuItem, IContextMenuOptions, ContextMenu, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js';
declare const LGraphNode: typeof TLGraphNode;
declare const LiteGraph: typeof TLiteGraph;
const PADDING = 0;
type LiteGraphDir = typeof LiteGraph.LEFT | typeof LiteGraph.RIGHT | typeof LiteGraph.UP | typeof LiteGraph.DOWN;
export const LAYOUT_LABEL_TO_DATA : {[label: string]: [LiteGraphDir, Vector2, Vector2]} = {
'Left': [LiteGraph.LEFT, [0, 0.5], [PADDING, 0]],
'Right': [LiteGraph.RIGHT, [1, 0.5], [-PADDING, 0]],
'Top': [LiteGraph.UP, [0.5, 0], [0, PADDING]],
'Bottom': [LiteGraph.DOWN, [0.5, 1], [0, -PADDING]],
};
const OPPOSITE_LABEL : {[label: string]: string} = {
'Left':'Right',
'Right':'Left',
'Top':'Bottom',
'Bottom':'Top',
}
interface MenuConfig {
name: string | ((node: TLGraphNode) => string);
property: string;
prepareValue?: (value: string, node: TLGraphNode) => any;
callback?: (node: TLGraphNode) => void;
}
interface SubMenuConfig extends MenuConfig {
options: string[],
}
export function addMenuItem(node: typeof LGraphNode, _app: ComfyApp, config: MenuConfig) {
const oldGetExtraMenuOptions = node.prototype.getExtraMenuOptions;
node.prototype.getExtraMenuOptions = function(canvas: TLGraphCanvas, menuOptions: ContextMenuItem[]) {
oldGetExtraMenuOptions && oldGetExtraMenuOptions.apply(this, [canvas, menuOptions]);
const idx = menuOptions.findIndex(option => option?.content.includes('Shape')) + 1;
menuOptions.splice((idx > 0 ? idx : menuOptions.length - 1), 0, {
content: typeof config.name == 'function' ? config.name(this) : config.name,
callback: (_value: ContextMenuItem, _options: IContextMenuOptions, _event: MouseEvent, _parentMenu: ContextMenu | undefined, _node: TLGraphNode) => {
this.properties = this.properties || {};
this.properties[config.property] = config.prepareValue ? config.prepareValue(this.properties[config.property], this) : !this.properties[config.property];
config.callback && config.callback(this);
}
});
};
}
export function addMenuSubMenu(node: typeof LGraphNode, _app: ComfyApp, config: SubMenuConfig) {
const oldGetExtraMenuOptions = node.prototype.getExtraMenuOptions;
node.prototype.getExtraMenuOptions = function(canvas: TLGraphCanvas, menuOptions: ContextMenuItem[]) {
oldGetExtraMenuOptions && oldGetExtraMenuOptions.apply(this, [canvas, menuOptions]);
const idx = menuOptions.findIndex(option => option?.content.includes('Shape')) + 1;
menuOptions.splice((idx > 0 ? idx : menuOptions.length - 1), 0, {
content: typeof config.name == 'function' ? config.name(this) : config.name,
has_submenu: true,
callback: (_value: ContextMenuItem, _options: IContextMenuOptions, event: MouseEvent, parentMenu: ContextMenu | undefined, _node: TLGraphNode) => {
new LiteGraph.ContextMenu(
config.options.map(option => ({content: option})),
{
event,
parentMenu,
callback: (value: ContextMenuItem, _options: IContextMenuOptions, _event: MouseEvent, _parentMenu: ContextMenu | undefined, _node: TLGraphNode) => {
this.properties = this.properties || {};
this.properties[config.property] = config.prepareValue ? config.prepareValue(value!.content, this) : value!.content;
config.callback && config.callback(this);
},
});
}
});
}
}
export function addConnectionLayoutSupport(node: typeof LGraphNode, app: ComfyApp, options = [['Left', 'Right'], ['Right', 'Left']], callback?: (node: TLGraphNode) => void) {
addMenuSubMenu(node, app, {
name: 'Connections Layout',
property: 'connections_layout',
options: options.map(option => option[0] + (option[1] ? ' -> ' + option[1]: '')),
prepareValue: (value, node) => {
const values = value.split(' -> ');
if (!values[1] && !node.outputs?.length) {
values[1] = OPPOSITE_LABEL[values[0]!]!;
}
if (!LAYOUT_LABEL_TO_DATA[values[0]!] || !LAYOUT_LABEL_TO_DATA[values[1]!]) {
throw new Error(`New Layout invalid: [${values[0]}, ${values[1]}]`);
}
return values;
},
callback: (node) => {
callback && callback(node);
app.graph.setDirtyCanvas(true, true);
},
})
// const oldGetConnectionPos = node.prototype.getConnectionPos;
node.prototype.getConnectionPos = function(isInput: boolean, slotNumber: number, out: Vector2) {
// Purposefully do not need to call the old one.
// oldGetConnectionPos && oldGetConnectionPos.apply(this, [isInput, slotNumber, out]);
return getConnectionPosForLayout(this, isInput, slotNumber, out);
}
}
export function setConnectionsLayout(node: TLGraphNode, newLayout: [string, string] = ['Left', 'Right']) {
// If we didn't supply an output layout, and there's no outputs, then just choose the opposite of the
// input as a safety.
if (!newLayout[1] && !node.outputs?.length) {
newLayout[1] = OPPOSITE_LABEL[newLayout[0]!]!;
}
if (!LAYOUT_LABEL_TO_DATA[newLayout[0]] || !LAYOUT_LABEL_TO_DATA[newLayout[1]]) {
throw new Error(`New Layout invalid: [${newLayout[0]}, ${newLayout[1]}]`);
}
node.properties = node.properties || {};
node.properties['connections_layout'] = newLayout;
}
/** Allows collapsing of connections into one. Pretty unusable, unless you're the muter. */
export function setConnectionsCollapse(node: TLGraphNode, collapseConnections: boolean | null = null) {
node.properties = node.properties || {};
collapseConnections = collapseConnections !== null ? collapseConnections : !node.properties['collapse_connections'];
node.properties['collapse_connections'] = collapseConnections;
}
export function getConnectionPosForLayout(node: TLGraphNode, isInput: boolean, slotNumber: number, out: Vector2) {
out = out || new Float32Array(2);
node.properties = node.properties || {};
const layout = node.properties['connections_layout'] || ['Left', 'Right'];
const collapseConnections = node.properties['collapse_connections'] || false;
const offset = (node.constructor as any).layout_slot_offset ?? (LiteGraph.NODE_SLOT_HEIGHT * 0.5);
const side = isInput ? layout[0] : layout[1];
const data = LAYOUT_LABEL_TO_DATA[side]!;
const cxn = node[isInput ? 'inputs' : 'outputs'][slotNumber];
if (!cxn) {
console.log('No connection found.. weird', isInput, slotNumber);
return out;
}
cxn.dir = data[0];
if (side === 'Left') {
if (node.flags.collapsed) {
var w = (node as any)._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH;
out[0] = node.pos[0];
out[1] = node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5;
} else {
// If we're an output, then the litegraph.core hates us; we need to blank out the name
// because it's not flexible enough to put the text on the inside.
if (!isInput && !(cxn as any).has_old_label) {
(cxn as any).has_old_label = true;
(cxn as any).old_label = cxn.label;
cxn.label = ' ';
} else if (isInput && (cxn as any).has_old_label) {
(cxn as any).has_old_label = false;
cxn.label = (cxn as any).old_label;
(cxn as any).old_label = undefined;
}
out[0] = node.pos[0] + offset;
if ((node.constructor as any)?.type.includes('Reroute')) {
out[1] = node.pos[1] + (node.size[1] * .5);
} else {
const displaySlot = collapseConnections ? 0 : slotNumber;
out[1] =
node.pos[1] +
(displaySlot + 0.7) * LiteGraph.NODE_SLOT_HEIGHT +
((node.constructor as any).slot_start_y || 0);
}
}
} else if (side === 'Right') {
if (node.flags.collapsed) {
var w = (node as any)._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH;
out[0] = node.pos[0] + w;
out[1] = node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5;
} else {
// If we're an input, then the litegraph.core hates us; we need to blank out the name
// because it's not flexible enough to put the text on the inside.
if (isInput && !(cxn as any).has_old_label) {
(cxn as any).has_old_label = true;
(cxn as any).old_label = cxn.label;
cxn.label = ' ';
} else if (!isInput && (cxn as any).has_old_label) {
(cxn as any).has_old_label = false;
cxn.label = (cxn as any).old_label;
(cxn as any).old_label = undefined;
}
out[0] = node.pos[0] + node.size[0] + 1 - offset;
if ((node.constructor as any)?.type.includes('Reroute')) {
out[1] = node.pos[1] + (node.size[1] * .5);
} else {
const displaySlot = collapseConnections ? 0 : slotNumber;
out[1] =
node.pos[1] +
(displaySlot + 0.7) * LiteGraph.NODE_SLOT_HEIGHT +
((node.constructor as any).slot_start_y || 0);
}
}
// Right now, only reroute uses top/bottom, so this may not work for other nodes
// (like, applying to nodes with titles, collapsed, multiple inputs/outputs, etc).
} else if (side === 'Top') {
if (!(cxn as any).has_old_label) {
(cxn as any).has_old_label = true;
(cxn as any).old_label = cxn.label;
cxn.label = ' ';
}
out[0] = node.pos[0] + (node.size[0] * .5);
out[1] = node.pos[1] + offset;
} else if (side === 'Bottom') {
if (!(cxn as any).has_old_label) {
(cxn as any).has_old_label = true;
(cxn as any).old_label = cxn.label;
cxn.label = ' ';
}
out[0] = node.pos[0] + (node.size[0] * .5);
out[1] = node.pos[1] + node.size[1] - offset;
}
return out;
}
+38
View File
@@ -0,0 +1,38 @@
{
"compilerOptions": {
"target": "es2016",
"module": "ESNext",
"typeRoots": [
"./ts/typings",
],
"outDir": "./js/",
"removeComments": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"allowUnusedLabels": true,
"skipLibCheck": true,
},
"include": [
"ts/*.ts",
],
"exclude": [
"**/*.spec.ts",
"**/*.d.ts",
"node_modules/**/*.ts"
]
}