Initial code.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
__pycache__
|
||||
*.ini
|
||||
wildcards/**
|
||||
.vscode/
|
||||
.idea/
|
||||
node_modules/
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||
### 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 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
@@ -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 |
@@ -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']]);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
"use strict";
|
||||
+186
@@ -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;
|
||||
}
|
||||
Generated
+25
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
category_prefix = 'rgthree'
|
||||
|
||||
def get_name(name):
|
||||
return '{} ({})'.format(name, category_prefix)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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']]);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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 = '*';
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
@@ -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 '/// <reference>' comment should have picked up the types, but it wasn't having it. ¯\_(ツ)_/¯
|
||||
Vendored
+90
@@ -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];
|
||||
Vendored
+1515
File diff suppressed because it is too large
Load Diff
+220
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user