diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..410cc3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__ +*.ini +wildcards/** +.vscode/ +.idea/ +node_modules/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5a4eb87 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md index ceaf3d9..4c3e321 100644 --- a/README.md +++ b/README.md @@ -1 +1,78 @@ -# rgthree-comfy \ No newline at end of file +# 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. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..ab45df9 --- /dev/null +++ b/__init__.py @@ -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"] diff --git a/docs/rgthree_advanced.png b/docs/rgthree_advanced.png new file mode 100644 index 0000000..59c77cf Binary files /dev/null and b/docs/rgthree_advanced.png differ diff --git a/docs/rgthree_advanced_metadata.png b/docs/rgthree_advanced_metadata.png new file mode 100644 index 0000000..a9809a5 Binary files /dev/null and b/docs/rgthree_advanced_metadata.png differ diff --git a/docs/rgthree_context.png b/docs/rgthree_context.png new file mode 100644 index 0000000..ee203e5 Binary files /dev/null and b/docs/rgthree_context.png differ diff --git a/docs/rgthree_context_metadata.png b/docs/rgthree_context_metadata.png new file mode 100644 index 0000000..a5fcc34 Binary files /dev/null and b/docs/rgthree_context_metadata.png differ diff --git a/docs/rgthree_router.png b/docs/rgthree_router.png new file mode 100644 index 0000000..f3581d7 Binary files /dev/null and b/docs/rgthree_router.png differ diff --git a/docs/rgthree_seed.png b/docs/rgthree_seed.png new file mode 100644 index 0000000..32c392f Binary files /dev/null and b/docs/rgthree_seed.png differ diff --git a/js/context.js b/js/context.js new file mode 100644 index 0000000..5069716 --- /dev/null +++ b/js/context.js @@ -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']]); + } + }); + }, +}); diff --git a/js/display_int.js b/js/display_int.js new file mode 100644 index 0000000..14e0554 --- /dev/null +++ b/js/display_int.js @@ -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]; + }; + } + }); + }, +}); diff --git a/js/muter.js b/js/muter.js new file mode 100644 index 0000000..7e0cd3a --- /dev/null +++ b/js/muter.js @@ -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); + }, +}); diff --git a/js/node_combiner.js b/js/node_combiner.js new file mode 100644 index 0000000..d1c6eb2 --- /dev/null +++ b/js/node_combiner.js @@ -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); + }, +}); diff --git a/js/reroute.js b/js/reroute.js new file mode 100644 index 0000000..9995e3e --- /dev/null +++ b/js/reroute.js @@ -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); + }, +}); diff --git a/js/seed.js b/js/seed.js new file mode 100644 index 0000000..c71906b --- /dev/null +++ b/js/seed.js @@ -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); + }; + } + }); + }, +}); diff --git a/js/smartswitch.js b/js/smartswitch.js new file mode 100644 index 0000000..3918c74 --- /dev/null +++ b/js/smartswitch.js @@ -0,0 +1 @@ +"use strict"; diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 0000000..3133fd3 --- /dev/null +++ b/js/utils.js @@ -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; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8204c26 --- /dev/null +++ b/package-lock.json @@ -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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3f60d28 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "typescript": "^5.1.6" + } +} diff --git a/py/constants.py b/py/constants.py new file mode 100644 index 0000000..989ee18 --- /dev/null +++ b/py/constants.py @@ -0,0 +1,4 @@ +category_prefix = 'rgthree' + +def get_name(name): + return '{} ({})'.format(name, category_prefix) diff --git a/py/context.py b/py/context.py new file mode 100644 index 0000000..21fbeb7 --- /dev/null +++ b/py/context.py @@ -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 diff --git a/py/context_switch.py b/py/context_switch.py new file mode 100644 index 0000000..9a38058 --- /dev/null +++ b/py/context_switch.py @@ -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 diff --git a/py/display_int.py b/py/display_int.py new file mode 100644 index 0000000..69852db --- /dev/null +++ b/py/display_int.py @@ -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 diff --git a/py/lora_stack.py b/py/lora_stack.py new file mode 100644 index 0000000..c47bce4 --- /dev/null +++ b/py/lora_stack.py @@ -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 diff --git a/py/seed.py b/py/seed.py new file mode 100644 index 0000000..83c16e5 --- /dev/null +++ b/py/seed.py @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/ts/context.ts b/ts/context.ts new file mode 100644 index 0000000..10ba6d6 --- /dev/null +++ b/ts/context.ts @@ -0,0 +1,23 @@ +// / +// @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']]); + } + }, +}); \ No newline at end of file diff --git a/ts/display_int.ts b/ts/display_int.ts new file mode 100644 index 0000000..ee8e4be --- /dev/null +++ b/ts/display_int.ts @@ -0,0 +1,44 @@ +// / +// @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]; + }; + } + }, +}); \ No newline at end of file diff --git a/ts/muter.ts b/ts/muter.ts new file mode 100644 index 0000000..59aa7dc --- /dev/null +++ b/ts/muter.ts @@ -0,0 +1,138 @@ +// / +// @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 | 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); + }, +}); diff --git a/ts/node_combiner.ts b/ts/node_combiner.ts new file mode 100644 index 0000000..9ff0445 --- /dev/null +++ b/ts/node_combiner.ts @@ -0,0 +1,81 @@ +// / +// @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); + }, +}); + diff --git a/ts/reroute.ts b/ts/reroute.ts new file mode 100644 index 0000000..ac858bb --- /dev/null +++ b/ts/reroute.ts @@ -0,0 +1,214 @@ +// / +// @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); + }, +}); + diff --git a/ts/seed.ts b/ts/seed.ts new file mode 100644 index 0000000..02ec1c2 --- /dev/null +++ b/ts/seed.ts @@ -0,0 +1,167 @@ +// / +// @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); + } + } + }, +}); \ No newline at end of file diff --git a/ts/smartswitch.ts b/ts/smartswitch.ts new file mode 100644 index 0000000..e59fcd5 --- /dev/null +++ b/ts/smartswitch.ts @@ -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 = '*'; +// } +// } +// } +// } +// }, +// }); \ No newline at end of file diff --git a/ts/typings/README.md b/ts/typings/README.md new file mode 100644 index 0000000..3e4c19b --- /dev/null +++ b/ts/typings/README.md @@ -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. ¯\_(ツ)_/¯ \ No newline at end of file diff --git a/ts/typings/comfy.d.ts b/ts/typings/comfy.d.ts new file mode 100644 index 0000000..48927c6 --- /dev/null +++ b/ts/typings/comfy.d.ts @@ -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; + 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; + /** + * Allows any additonal setup, called after the application is fully set up and running + * @param app The ComfyUI app instance + */ + setup(app: ComfyApp): Promise; + /** + * 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, app: ComfyApp): Promise; + /** + * 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 { 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; + /** + * Allows the extension to register additional nodes with LGraph after standard nodes are added + * @param app The ComfyUI app instance + */ + registerCustomNodes(app: ComfyApp): Promise; + /** + * 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; + optional?: Record; + }; + output?: string[]; + output_name: string[]; +}; + +export type ComfyObjectInfoConfig = [string | any[]] | [string | any[], any]; diff --git a/ts/typings/litegraph.d.ts b/ts/typings/litegraph.d.ts new file mode 100644 index 0000000..3029395 --- /dev/null +++ b/ts/typings/litegraph.d.ts @@ -0,0 +1,1515 @@ +// Type definitions for litegraph.js 0.7.0 +// Project: litegraph.js +// Definitions by: NateScarlet + +export type Vector2 = [number, number]; +export type Vector4 = [number, number, number, number]; +export type widgetTypes = + | "number" + | "slider" + | "combo" + | "text" + | "toggle" + | "button"; +export type SlotShape = + | typeof LiteGraph.BOX_SHAPE + | typeof LiteGraph.CIRCLE_SHAPE + | typeof LiteGraph.ARROW_SHAPE + | typeof LiteGraph.SQUARE_SHAPE + | number; // For custom shapes + +/** https://github.com/jagenjo/litegraph.js/tree/master/guides#node-slots */ +export interface INodeSlot { + name: string; + type: string | -1; + label?: string; + dir?: + | typeof LiteGraph.UP + | typeof LiteGraph.RIGHT + | typeof LiteGraph.DOWN + | typeof LiteGraph.LEFT; + color_on?: string; + color_off?: string; + shape?: SlotShape; + locked?: boolean; + nameLocked?: boolean; + pos?: Vector2; +} + +export interface INodeInputSlot extends INodeSlot { + link: LLink["id"] | null; +} +export interface INodeOutputSlot extends INodeSlot { + links: LLink["id"][] | null; +} + +export type WidgetCallback = ( + this: T, + value: T["value"], + graphCanvas: LGraphCanvas, + node: LGraphNode, + pos: Vector2, + event?: MouseEvent +) => void; + +export interface IWidget { + name: string | null; + value: TValue; + options?: TOptions; + type?: widgetTypes; + y?: number; + property?: string; + last_y?: number; + clicked?: boolean; + marker?: boolean; + disabled?: boolean; + callback?: WidgetCallback; + /** Called by `LGraphCanvas.drawNodeWidgets` */ + draw?( + ctx: CanvasRenderingContext2D, + node: LGraphNode, + width: number, + posY: number, + height: number + ): void; + /** + * Called by `LGraphCanvas.processNodeWidgets` + * https://github.com/jagenjo/litegraph.js/issues/76 + */ + mouse?( + event: MouseEvent, + pos: Vector2, + node: LGraphNode + ): boolean; + /** Called by `LGraphNode.computeSize` */ + computeSize?(width: number): [number, number]; +} +export interface IButtonWidget extends IWidget { + type: "button"; +} +export interface IToggleWidget + extends IWidget { + type: "toggle"; +} +export interface ISliderWidget + extends IWidget { + type: "slider"; +} +export interface INumberWidget extends IWidget { + type: "number"; +} +export interface IComboWidget + extends IWidget< + string[], + { + values: + | string[] + | ((widget: IComboWidget, node: LGraphNode) => string[]); + } + > { + type: "combo"; +} + +export interface ITextWidget extends IWidget { + type: "text"; +} + +export interface IContextMenuItem { + content: string; + value?: string; + callback?: ContextMenuEventListener; + /** Used as innerHTML for extra child element */ + title?: string; + disabled?: boolean; + has_submenu?: boolean; + submenu?: { + options: ContextMenuItem[]; + } & IContextMenuOptions; + className?: string; +} +export interface IContextMenuOptions { + callback?: ContextMenuEventListener; + ignore_item_callbacks?: Boolean; + event?: MouseEvent | CustomEvent; + parentMenu?: ContextMenu; + autoopen?: boolean; + title?: string; + extra?: any; +} + +export type ContextMenuItem = IContextMenuItem | null; +export type ContextMenuEventListener = ( + value: ContextMenuItem, + options: IContextMenuOptions, + event: MouseEvent, + parentMenu: ContextMenu | undefined, + node: LGraphNode +) => boolean | void; + +export const LiteGraph: { + VERSION: number; + + CANVAS_GRID_SIZE: number; + + NODE_TITLE_HEIGHT: number; + NODE_TITLE_TEXT_Y: number; + NODE_SLOT_HEIGHT: number; + NODE_WIDGET_HEIGHT: number; + NODE_WIDTH: number; + NODE_MIN_WIDTH: number; + NODE_COLLAPSED_RADIUS: number; + NODE_COLLAPSED_WIDTH: number; + NODE_TITLE_COLOR: string; + NODE_TEXT_SIZE: number; + NODE_TEXT_COLOR: string; + NODE_SUBTEXT_SIZE: number; + NODE_DEFAULT_COLOR: string; + NODE_DEFAULT_BGCOLOR: string; + NODE_DEFAULT_BOXCOLOR: string; + NODE_DEFAULT_SHAPE: string; + DEFAULT_SHADOW_COLOR: string; + DEFAULT_GROUP_FONT: number; + + LINK_COLOR: string; + EVENT_LINK_COLOR: string; + CONNECTING_LINK_COLOR: string; + + MAX_NUMBER_OF_NODES: number; //avoid infinite loops + DEFAULT_POSITION: Vector2; //default node position + VALID_SHAPES: ["default", "box", "round", "card"]; //,"circle" + + //shapes are used for nodes but also for slots + BOX_SHAPE: 1; + ROUND_SHAPE: 2; + CIRCLE_SHAPE: 3; + CARD_SHAPE: 4; + ARROW_SHAPE: 5; + SQUARE_SHAPE: 6; + + //enums + INPUT: 1; + OUTPUT: 2; + + EVENT: -1; //for outputs + ACTION: -1; //for inputs + + ALWAYS: 0; + ON_EVENT: 1; + NEVER: 2; + ON_TRIGGER: 3; + + UP: 1; + DOWN: 2; + LEFT: 3; + RIGHT: 4; + CENTER: 5; + + STRAIGHT_LINK: 0; + LINEAR_LINK: 1; + SPLINE_LINK: 2; + + NORMAL_TITLE: 0; + NO_TITLE: 1; + TRANSPARENT_TITLE: 2; + AUTOHIDE_TITLE: 3; + + node_images_path: string; + + debug: boolean; + catch_exceptions: boolean; + throw_errors: boolean; + /** if set to true some nodes like Formula would be allowed to evaluate code that comes from unsafe sources (like node configuration), which could lead to exploits */ + allow_scripts: boolean; + /** node types by string */ + registered_node_types: Record; + /** used for dropping files in the canvas */ + node_types_by_file_extension: Record; + /** node types by class name */ + Nodes: Record; + + /** used to add extra features to the search box */ + searchbox_extras: Record< + string, + { + data: { outputs: string[][]; title: string }; + desc: string; + type: string; + } + >; + + createNode(type: string): T; + /** Register a node class so it can be listed when the user wants to create a new one */ + registerNodeType(type: string, base: { new (title?: string): T }): void; + /** removes a node type from the system */ + unregisterNodeType(type: string): void; + /** Removes all previously registered node's types. */ + clearRegisteredTypes(): void; + /** + * Create a new node type by passing a function, it wraps it with a proper class and generates inputs according to the parameters of the function. + * Useful to wrap simple methods that do not require properties, and that only process some input to generate an output. + * @param name node name with namespace (p.e.: 'math/sum') + * @param func + * @param param_types an array containing the type of every parameter, otherwise parameters will accept any type + * @param return_type string with the return type, otherwise it will be generic + * @param properties properties to be configurable + */ + wrapFunctionAsNode( + name: string, + func: (...args: any[]) => any, + param_types?: string[], + return_type?: string, + properties?: object + ): void; + + /** + * Adds this method to all node types, existing and to be created + * (You can add it to LGraphNode.prototype but then existing node types wont have it) + */ + addNodeMethod(name: string, func: (...args: any[]) => any): void; + + /** + * Create a node of a given type with a name. The node is not attached to any graph yet. + * @param type full name of the node class. p.e. "math/sin" + * @param name a name to distinguish from other nodes + * @param options to set options + */ + createNode( + type: string, + title: string, + options: object + ): T; + + /** + * Returns a registered node type with a given name + * @param type full name of the node class. p.e. "math/sin" + */ + getNodeType(type: string): LGraphNodeConstructor; + + /** + * Returns a list of node types matching one category + * @method getNodeTypesInCategory + * @param {String} category category name + * @param {String} filter only nodes with ctor.filter equal can be shown + * @return {Array} array with all the node classes + */ + getNodeTypesInCategory( + category: string, + filter: string + ): LGraphNodeConstructor[]; + + /** + * Returns a list with all the node type categories + * @method getNodeTypesCategories + * @param {String} filter only nodes with ctor.filter equal can be shown + * @return {Array} array with all the names of the categories + */ + getNodeTypesCategories(filter: string): string[]; + + /** debug purposes: reloads all the js scripts that matches a wildcard */ + reloadNodes(folder_wildcard: string): void; + + getTime(): number; + LLink: typeof LLink; + LGraph: typeof LGraph; + DragAndScale: typeof DragAndScale; + compareObjects(a: object, b: object): boolean; + distance(a: Vector2, b: Vector2): number; + colorToString(c: string): string; + isInsideRectangle( + x: number, + y: number, + left: number, + top: number, + width: number, + height: number + ): boolean; + growBounding(bounding: Vector4, x: number, y: number): Vector4; + isInsideBounding(p: Vector2, bb: Vector4): boolean; + hex2num(hex: string): [number, number, number]; + num2hex(triplet: [number, number, number]): string; + ContextMenu: typeof ContextMenu; + extendClass(target: A, origin: B): A & B; + getParameterNames(func: string): string[]; +}; + +export type serializedLGraph< + TNode = ReturnType, + // https://github.com/jagenjo/litegraph.js/issues/74 + TLink = [number, number, number, number, number, string], + TGroup = ReturnType +> = { + last_node_id: LGraph["last_node_id"]; + last_link_id: LGraph["last_link_id"]; + nodes: TNode[]; + links: TLink[]; + groups: TGroup[]; + config: LGraph["config"]; + version: typeof LiteGraph.VERSION; +}; + +export declare class LGraph { + static supported_types: string[]; + static STATUS_STOPPED: 1; + static STATUS_RUNNING: 2; + + constructor(o?: object); + + filter: string; + catch_errors: boolean; + /** custom data */ + config: object; + elapsed_time: number; + fixedtime: number; + fixedtime_lapse: number; + globaltime: number; + inputs: any; + iteration: number; + last_link_id: number; + last_node_id: number; + last_update_time: number; + links: Record; + list_of_graphcanvas: LGraphCanvas[]; + outputs: any; + runningtime: number; + starttime: number; + status: typeof LGraph.STATUS_RUNNING | typeof LGraph.STATUS_STOPPED; + + private _nodes: LGraphNode[]; + private _groups: LGraphGroup[]; + private _nodes_by_id: Record; + /** nodes that are executable sorted in execution order */ + private _nodes_executable: + | (LGraphNode & { onExecute: NonNullable }[]) + | null; + /** nodes that contain onExecute */ + private _nodes_in_order: LGraphNode[]; + private _version: number; + + getSupportedTypes(): string[]; + /** Removes all nodes from this graph */ + clear(): void; + /** Attach Canvas to this graph */ + attachCanvas(graphCanvas: LGraphCanvas): void; + /** Detach Canvas to this graph */ + detachCanvas(graphCanvas: LGraphCanvas): void; + /** + * Starts running this graph every interval milliseconds. + * @param interval amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate + */ + start(interval?: number): void; + /** Stops the execution loop of the graph */ + stop(): void; + /** + * Run N steps (cycles) of the graph + * @param num number of steps to run, default is 1 + */ + runStep(num?: number, do_not_catch_errors?: boolean): void; + /** + * Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than + * nodes with only inputs. + */ + updateExecutionOrder(): void; + /** This is more internal, it computes the executable nodes in order and returns it */ + computeExecutionOrder(only_onExecute: boolean, set_level: any): T; + /** + * Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively. + * It doesn't include the node itself + * @return an array with all the LGraphNodes that affect this node, in order of execution + */ + getAncestors(node: LGraphNode): LGraphNode[]; + /** + * Positions every node in a more readable manner + */ + arrange(margin?: number,layout?: string): void; + /** + * Returns the amount of time the graph has been running in milliseconds + * @return number of milliseconds the graph has been running + */ + getTime(): number; + + /** + * Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant + * @return number of milliseconds the graph has been running + */ + getFixedTime(): number; + + /** + * Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct + * if the nodes are using graphical actions + * @return number of milliseconds it took the last cycle + */ + getElapsedTime(): number; + /** + * Sends an event to all the nodes, useful to trigger stuff + * @param eventName the name of the event (function to be called) + * @param params parameters in array format + */ + sendEventToAllNodes(eventName: string, params: any[], mode?: any): void; + + sendActionToCanvas(action: any, params: any[]): void; + /** + * Adds a new node instance to this graph + * @param node the instance of the node + */ + add(node: LGraphNode, skip_compute_order?: boolean): void; + /** + * Called when a new node is added + * @param node the instance of the node + */ + onNodeAdded(node: LGraphNode): void; + /** Removes a node from the graph */ + remove(node: LGraphNode): void; + /** Returns a node by its id. */ + getNodeById(id: number): LGraphNode | undefined; + /** + * Returns a list of nodes that matches a class + * @param classObject the class itself (not an string) + * @return a list with all the nodes of this type + */ + findNodesByClass( + classObject: LGraphNodeConstructor + ): T[]; + /** + * Returns a list of nodes that matches a type + * @param type the name of the node type + * @return a list with all the nodes of this type + */ + findNodesByType(type: string): T[]; + /** + * Returns the first node that matches a name in its title + * @param title the name of the node to search + * @return the node or null + */ + findNodeByTitle(title: string): T | null; + /** + * Returns a list of nodes that matches a name + * @param title the name of the node to search + * @return a list with all the nodes with this name + */ + findNodesByTitle(title: string): T[]; + /** + * Returns the top-most node in this position of the canvas + * @param x the x coordinate in canvas space + * @param y the y coordinate in canvas space + * @param nodes_list a list with all the nodes to search from, by default is all the nodes in the graph + * @return the node at this position or null + */ + getNodeOnPos( + x: number, + y: number, + node_list?: LGraphNode[], + margin?: number + ): T | null; + /** + * Returns the top-most group in that position + * @param x the x coordinate in canvas space + * @param y the y coordinate in canvas space + * @return the group or null + */ + getGroupOnPos(x: number, y: number): LGraphGroup | null; + + onAction(action: any, param: any): void; + trigger(action: any, param: any): void; + /** Tell this graph it has a global graph input of this type */ + addInput(name: string, type: string, value?: any): void; + /** Assign a data to the global graph input */ + setInputData(name: string, data: any): void; + /** Returns the current value of a global graph input */ + getInputData(name: string): T; + /** Changes the name of a global graph input */ + renameInput(old_name: string, name: string): false | undefined; + /** Changes the type of a global graph input */ + changeInputType(name: string, type: string): false | undefined; + /** Removes a global graph input */ + removeInput(name: string): boolean; + /** Creates a global graph output */ + addOutput(name: string, type: string, value: any): void; + /** Assign a data to the global output */ + setOutputData(name: string, value: string): void; + /** Returns the current value of a global graph output */ + getOutputData(name: string): T; + + /** Renames a global graph output */ + renameOutput(old_name: string, name: string): false | undefined; + /** Changes the type of a global graph output */ + changeOutputType(name: string, type: string): false | undefined; + /** Removes a global graph output */ + removeOutput(name: string): boolean; + triggerInput(name: string, value: any): void; + setCallback(name: string, func: (...args: any[]) => any): void; + beforeChange(info?: LGraphNode): void; + afterChange(info?: LGraphNode): void; + connectionChange(node: LGraphNode): void; + /** returns if the graph is in live mode */ + isLive(): boolean; + /** clears the triggered slot animation in all links (stop visual animation) */ + clearTriggeredSlots(): void; + /* Called when something visually changed (not the graph!) */ + change(): void; + setDirtyCanvas(fg: boolean, bg: boolean): void; + /** Destroys a link */ + removeLink(link_id: number): void; + /** Creates a Object containing all the info about this graph, it can be serialized */ + serialize(): T; + /** + * Configure a graph from a JSON string + * @param data configure a graph from a JSON string + * @returns if there was any error parsing + */ + configure(data: object, keep_old?: boolean): boolean | undefined; + load(url: string): void; +} + +export type SerializedLLink = [number, string, number, number, number, number]; +export declare class LLink { + id: number; + type: string; + origin_id: number; + origin_slot: number; + target_id: number; + target_slot: number; + constructor( + id: number, + type: string, + origin_id: number, + origin_slot: number, + target_id: number, + target_slot: number + ); + configure(o: LLink | SerializedLLink): void; + serialize(): SerializedLLink; +} + +export type SerializedLGraphNode = { + id: T["id"]; + type: T["type"]; + pos: T["pos"]; + size: T["size"]; + flags: T["flags"]; + mode: T["mode"]; + inputs: T["inputs"]; + outputs: T["outputs"]; + title: T["title"]; + properties: T["properties"]; + widgets_values?: IWidget["value"][]; +}; + +/** https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#lgraphnode */ +export declare class LGraphNode { + static title_color: string; + static title: string; + static type: null | string; + static widgets_up: boolean; + constructor(title?: string); + + title: string; + type: null | string; + size: Vector2; + graph: null | LGraph; + graph_version: number; + pos: Vector2; + is_selected: boolean; + mouseOver: boolean; + + id: number; + + widgets: IWidget[]; + //inputs available: array of inputs + inputs: INodeInputSlot[]; + outputs: INodeOutputSlot[]; + connections: any[]; + + //local data + properties: Record; + properties_info: any[]; + + flags: Partial<{ + collapsed: boolean + }>; + + color: string; + bgcolor: string; + boxcolor: string; + shape: + | typeof LiteGraph.BOX_SHAPE + | typeof LiteGraph.ROUND_SHAPE + | typeof LiteGraph.CIRCLE_SHAPE + | typeof LiteGraph.CARD_SHAPE + | typeof LiteGraph.ARROW_SHAPE; + + serialize_widgets: boolean; + skip_list: boolean; + + /** Used in `LGraphCanvas.onMenuNodeMode` */ + mode?: + | typeof LiteGraph.ON_EVENT + | typeof LiteGraph.ON_TRIGGER + | typeof LiteGraph.NEVER + | typeof LiteGraph.ALWAYS; + + /** If set to true widgets do not start after the slots */ + widgets_up: boolean; + /** widgets start at y distance from the top of the node */ + widgets_start_y: number; + /** if you render outside the node, it will be clipped */ + clip_area: boolean; + /** if set to false it wont be resizable with the mouse */ + resizable: boolean; + /** slots are distributed horizontally */ + horizontal: boolean; + /** if true, the node will show the bgcolor as 'red' */ + has_errors?: boolean; + + /** configure a node from an object containing the serialized info */ + configure(info: SerializedLGraphNode): void; + /** serialize the content */ + serialize(): SerializedLGraphNode; + /** Creates a clone of this node */ + clone(): this; + /** serialize and stringify */ + toString(): string; + /** get the title string */ + getTitle(): string; + /** sets the value of a property */ + setProperty(name: string, value: any): void; + /** sets the output data */ + setOutputData(slot: number, data: any): void; + /** sets the output data */ + setOutputDataType(slot: number, type: string): void; + /** + * Retrieves the input data (data traveling through the connection) from one slot + * @param slot + * @param force_update if set to true it will force the connected node of this slot to output data into this link + * @return data or if it is not connected returns undefined + */ + getInputData(slot: number, force_update?: boolean): T; + /** + * Retrieves the input data type (in case this supports multiple input types) + * @param slot + * @return datatype in string format + */ + getInputDataType(slot: number): string; + /** + * Retrieves the input data from one slot using its name instead of slot number + * @param slot_name + * @param force_update if set to true it will force the connected node of this slot to output data into this link + * @return data or if it is not connected returns null + */ + getInputDataByName(slot_name: string, force_update?: boolean): T; + /** tells you if there is a connection in one input slot */ + isInputConnected(slot: number): boolean; + /** tells you info about an input connection (which node, type, etc) */ + getInputInfo( + slot: number + ): { link: number; name: string; type: string | 0 } | null; + /** returns the node connected in the input slot */ + getInputNode(slot: number): LGraphNode | null; + /** returns the value of an input with this name, otherwise checks if there is a property with that name */ + getInputOrProperty(name: string): T; + /** tells you the last output data that went in that slot */ + getOutputData(slot: number): T | null; + /** tells you info about an output connection (which node, type, etc) */ + getOutputInfo( + slot: number + ): { name: string; type: string; links: number[] } | null; + /** tells you if there is a connection in one output slot */ + isOutputConnected(slot: number): boolean; + /** tells you if there is any connection in the output slots */ + isAnyOutputConnected(): boolean; + /** retrieves all the nodes connected to this output slot */ + getOutputNodes(slot: number): LGraphNode[]; + /** Triggers an event in this node, this will trigger any output with the same name */ + trigger(action: string, param: any): void; + /** + * Triggers an slot event in this node + * @param slot the index of the output slot + * @param param + * @param link_id in case you want to trigger and specific output link in a slot + */ + triggerSlot(slot: number, param: any, link_id?: number): void; + /** + * clears the trigger slot animation + * @param slot the index of the output slot + * @param link_id in case you want to trigger and specific output link in a slot + */ + clearTriggeredSlot(slot: number, link_id?: number): void; + /** + * add a new property to this node + * @param name + * @param default_value + * @param type string defining the output type ("vec3","number",...) + * @param extra_info this can be used to have special properties of the property (like values, etc) + */ + addProperty( + name: string, + default_value: any, + type: string, + extra_info?: object + ): T; + /** + * add a new output slot to use in this node + * @param name + * @param type string defining the output type ("vec3","number",...) + * @param extra_info this can be used to have special properties of an output (label, special color, position, etc) + */ + addOutput( + name: string, + type: string | -1, + extra_info?: Partial + ): INodeOutputSlot; + /** + * add a new output slot to use in this node + * @param array of triplets like [[name,type,extra_info],[...]] + */ + addOutputs( + array: [string, string | -1, Partial | undefined][] + ): void; + /** remove an existing output slot */ + removeOutput(slot: number): void; + /** + * add a new input slot to use in this node + * @param name + * @param type string defining the input type ("vec3","number",...), it its a generic one use 0 + * @param extra_info this can be used to have special properties of an input (label, color, position, etc) + */ + addInput( + name: string, + type: string | -1, + extra_info?: Partial + ): INodeInputSlot; + /** + * add several new input slots in this node + * @param array of triplets like [[name,type,extra_info],[...]] + */ + addInputs( + array: [string, string | -1, Partial | undefined][] + ): void; + /** remove an existing input slot */ + removeInput(slot: number): void; + /** + * add an special connection to this node (used for special kinds of graphs) + * @param name + * @param type string defining the input type ("vec3","number",...) + * @param pos position of the connection inside the node + * @param direction if is input or output + */ + addConnection( + name: string, + type: string, + pos: Vector2, + direction: string + ): { + name: string; + type: string; + pos: Vector2; + direction: string; + links: null; + }; + setValue(v: any): void; + /** computes the size of a node according to its inputs and output slots */ + computeSize(out?: Vector2): [number, number]; + /** + * https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#node-widgets + * @return created widget + */ + addWidget( + type: T["type"], + name: string, + value: T["value"], + callback?: WidgetCallback | string, + options?: T["options"] + ): T; + + addCustomWidget(customWidget: T): T; + + /** + * returns the bounding of the object, used for rendering purposes + * @return [x, y, width, height] + */ + getBounding(): Vector4; + /** checks if a point is inside the shape of a node */ + isPointInside( + x: number, + y: number, + margin?: number, + skipTitle?: boolean + ): boolean; + /** checks if a point is inside a node slot, and returns info about which slot */ + getSlotInPosition( + x: number, + y: number + ): { + input?: INodeInputSlot; + output?: INodeOutputSlot; + slot: number; + link_pos: Vector2; + }; + /** + * returns the input slot with a given name (used for dynamic slots), -1 if not found + * @param name the name of the slot + * @return the slot (-1 if not found) + */ + findInputSlot(name: string): number; + /** + * returns the output slot with a given name (used for dynamic slots), -1 if not found + * @param name the name of the slot + * @return the slot (-1 if not found) + */ + findOutputSlot(name: string): number; + /** + * connect this node output to the input of another node + * @param slot (could be the number of the slot or the string with the name of the slot) + * @param targetNode the target node + * @param targetSlot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger) + * @return {Object} the link_info is created, otherwise null + */ + connect( + slot: number | string, + targetNode: LGraphNode, + targetSlot: number | string + ): T | null; + /** + * disconnect one output to an specific node + * @param slot (could be the number of the slot or the string with the name of the slot) + * @param target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected] + * @return if it was disconnected successfully + */ + disconnectOutput(slot: number | string, targetNode?: LGraphNode): boolean; + /** + * disconnect one input + * @param slot (could be the number of the slot or the string with the name of the slot) + * @return if it was disconnected successfully + */ + disconnectInput(slot: number | string): boolean; + /** + * returns the center of a connection point in canvas coords + * @param is_input true if if a input slot, false if it is an output + * @param slot (could be the number of the slot or the string with the name of the slot) + * @param out a place to store the output, to free garbage + * @return the position + **/ + getConnectionPos( + is_input: boolean, + slot: number | string, + out?: Vector2 + ): Vector2; + /** Force align to grid */ + alignToGrid(): void; + /** Console output */ + trace(msg: string): void; + /** Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */ + setDirtyCanvas(fg: boolean, bg: boolean): void; + loadImage(url: string): void; + /** Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */ + captureInput(v: any): void; + /** Collapse the node to make it smaller on the canvas */ + collapse(force: boolean): void; + /** Forces the node to do not move or realign on Z */ + pin(v?: boolean): void; + localToScreen(x: number, y: number, graphCanvas: LGraphCanvas): Vector2; + + // https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-appearance + onDrawBackground?( + ctx: CanvasRenderingContext2D, + canvas: HTMLCanvasElement + ): void; + onDrawForeground?( + ctx: CanvasRenderingContext2D, + canvas: HTMLCanvasElement + ): void; + + // https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-behaviour + onMouseDown?( + event: MouseEvent, + pos: Vector2, + graphCanvas: LGraphCanvas + ): void; + onMouseMove?( + event: MouseEvent, + pos: Vector2, + graphCanvas: LGraphCanvas + ): void; + onMouseUp?( + event: MouseEvent, + pos: Vector2, + graphCanvas: LGraphCanvas + ): void; + onMouseEnter?( + event: MouseEvent, + pos: Vector2, + graphCanvas: LGraphCanvas + ): void; + onMouseLeave?( + event: MouseEvent, + pos: Vector2, + graphCanvas: LGraphCanvas + ): void; + onKey?(event: KeyboardEvent, pos: Vector2, graphCanvas: LGraphCanvas): void; + + onExecuted(message: any): void; + /** Called by `LGraphNode.createNode` */ + onNodeCreated?(): void; + /** Called by `LGraphCanvas.selectNodes` */ + onSelected?(): void; + /** Called by `LGraphCanvas.deselectNode` */ + onDeselected?(): void; + /** Called by `LGraph.runStep` `LGraphNode.getInputData` */ + onExecute?(): void; + /** Called by `LGraph.serialize` */ + onSerialize?(o: SerializedLGraphNode): void; + /** Called by `LGraph.configure` */ + onConfigure?(o: SerializedLGraphNode): void; + /** + * when added to graph (warning: this is called BEFORE the node is configured when loading) + * Called by `LGraph.add` + */ + onAdded?(graph: LGraph): void; + /** + * when removed from graph + * Called by `LGraph.remove` `LGraph.clear` + */ + onRemoved?(): void; + /** + * if returns false the incoming connection will be canceled + * Called by `LGraph.connect` + * @param inputIndex target input slot number + * @param outputType type of output slot + * @param outputSlot output slot object + * @param outputNode node containing the output + * @param outputIndex index of output slot + */ + onConnectInput?( + inputIndex: number, + outputType: INodeOutputSlot["type"], + outputSlot: INodeOutputSlot, + outputNode: LGraphNode, + outputIndex: number + ): boolean; + /** + * if returns false the incoming connection will be canceled + * Called by `LGraph.connect` + * @param outputIndex target output slot number + * @param inputType type of input slot + * @param inputSlot input slot object + * @param inputNode node containing the input + * @param inputIndex index of input slot + */ + onConnectOutput?( + outputIndex: number, + inputType: INodeInputSlot["type"], + inputSlot: INodeInputSlot, + inputNode: LGraphNode, + inputIndex: number + ): boolean; + + /** + * Called just before connection (or disconnect - if input is linked). + * A convenient place to switch to another input, or create new one. + * This allow for ability to automatically add slots if needed + * @param inputIndex + * @return selected input slot index, can differ from parameter value + */ + onBeforeConnectInput?( + inputIndex: number + ): number; + + /** a connection changed (new one or removed) (LiteGraph.INPUT or LiteGraph.OUTPUT, slot, true if connected, link_info, input_info or output_info ) */ + onConnectionsChange( + type: number, + slotIndex: number, + isConnected: boolean, + link: LLink, + ioSlot: (INodeOutputSlot | INodeInputSlot) + ): void; + + /** + * if returns false, will abort the `LGraphNode.setProperty` + * Called when a property is changed + * @param property + * @param value + * @param prevValue + */ + onPropertyChanged?(property: string, value: any, prevValue: any): void | boolean; + + /** Called by `LGraphCanvas.processContextMenu` */ + getMenuOptions?(graphCanvas: LGraphCanvas): ContextMenuItem[]; + getSlotMenuOptions?(slot: INodeSlot): ContextMenuItem[]; + + getExtraMenuOptions?(canvas: LGraphCanvas, options: ContextMenuItem[]): void; +} + +export type LGraphNodeConstructor = { + new (): T; +}; + +export type SerializedLGraphGroup = { + title: LGraphGroup["title"]; + bounding: LGraphGroup["_bounding"]; + color: LGraphGroup["color"]; + font: LGraphGroup["font"]; +}; +export declare class LGraphGroup { + title: string; + private _bounding: Vector4; + color: string; + font: string; + + configure(o: SerializedLGraphGroup): void; + serialize(): SerializedLGraphGroup; + move(deltaX: number, deltaY: number, ignoreNodes?: boolean): void; + recomputeInsideNodes(): void; + isPointInside: LGraphNode["isPointInside"]; + setDirtyCanvas: LGraphNode["setDirtyCanvas"]; +} + +export declare class DragAndScale { + constructor(element?: HTMLElement, skipEvents?: boolean); + offset: [number, number]; + scale: number; + max_scale: number; + min_scale: number; + onredraw: Function | null; + enabled: boolean; + last_mouse: Vector2; + element: HTMLElement | null; + visible_area: Vector4; + bindEvents(element: HTMLElement): void; + computeVisibleArea(): void; + onMouse(e: MouseEvent): void; + toCanvasContext(ctx: CanvasRenderingContext2D): void; + convertOffsetToCanvas(pos: Vector2): Vector2; + convertCanvasToOffset(pos: Vector2): Vector2; + mouseDrag(x: number, y: number): void; + changeScale(value: number, zooming_center?: Vector2): void; + changeDeltaScale(value: number, zooming_center?: Vector2): void; + reset(): void; +} + +/** + * This class is in charge of rendering one graph inside a canvas. And provides all the interaction required. + * Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked + * + * @param canvas the canvas where you want to render (it accepts a selector in string format or the canvas element itself) + * @param graph + * @param options { skip_rendering, autoresize } + */ +export declare class LGraphCanvas { + static node_colors: Record< + string, + { + color: string; + bgcolor: string; + groupcolor: string; + } + >; + static link_type_colors: Record; + static gradients: object; + static search_limit: number; + + static getFileExtension(url: string): string; + static decodeHTML(str: string): string; + + static onMenuCollapseAll(): void; + static onMenuNodeEdit(): void; + static onShowPropertyEditor( + item: any, + options: any, + e: any, + menu: any, + node: any + ): void; + /** Create menu for `Add Group` */ + static onGroupAdd: ContextMenuEventListener; + /** Create menu for `Add Node` */ + static onMenuAdd: ContextMenuEventListener; + static showMenuNodeOptionalInputs: ContextMenuEventListener; + static showMenuNodeOptionalOutputs: ContextMenuEventListener; + static onShowMenuNodeProperties: ContextMenuEventListener; + static onResizeNode: ContextMenuEventListener; + static onMenuNodeCollapse: ContextMenuEventListener; + static onMenuNodePin: ContextMenuEventListener; + static onMenuNodeMode: ContextMenuEventListener; + static onMenuNodeColors: ContextMenuEventListener; + static onMenuNodeShapes: ContextMenuEventListener; + static onMenuNodeRemove: ContextMenuEventListener; + static onMenuNodeClone: ContextMenuEventListener; + + constructor( + canvas: HTMLCanvasElement | string, + graph?: LGraph, + options?: { + skip_render?: boolean; + autoresize?: boolean; + } + ); + + static active_canvas: HTMLCanvasElement; + + allow_dragcanvas: boolean; + allow_dragnodes: boolean; + /** allow to control widgets, buttons, collapse, etc */ + allow_interaction: boolean; + /** allows to change a connection with having to redo it again */ + allow_reconnect_links: boolean; + /** allow selecting multi nodes without pressing extra keys */ + multi_select: boolean; + /** No effect */ + allow_searchbox: boolean; + always_render_background: boolean; + autoresize?: boolean; + background_image: string; + bgcanvas: HTMLCanvasElement; + bgctx: CanvasRenderingContext2D; + canvas: HTMLCanvasElement; + canvas_mouse: Vector2; + clear_background: boolean; + connecting_node: LGraphNode | null; + connections_width: number; + ctx: CanvasRenderingContext2D; + current_node: LGraphNode | null; + default_connection_color: { + input_off: string; + input_on: string; + output_off: string; + output_on: string; + }; + default_link_color: string; + dirty_area: Vector4 | null; + dirty_bgcanvas?: boolean; + dirty_canvas?: boolean; + drag_mode: boolean; + dragging_canvas: boolean; + dragging_rectangle: Vector4 | null; + ds: DragAndScale; + /** used for transition */ + editor_alpha: number; + filter: any; + fps: number; + frame: number; + graph: LGraph; + highlighted_links: Record; + highquality_render: boolean; + inner_text_font: string; + is_rendering: boolean; + last_draw_time: number; + last_mouse: Vector2; + /** + * Possible duplicated with `last_mouse` + * https://github.com/jagenjo/litegraph.js/issues/70 + */ + last_mouse_position: Vector2; + /** Timestamp of last mouse click, defaults to 0 */ + last_mouseclick: number; + links_render_mode: + | typeof LiteGraph.STRAIGHT_LINK + | typeof LiteGraph.LINEAR_LINK + | typeof LiteGraph.SPLINE_LINK; + live_mode: boolean; + node_capturing_input: LGraphNode | null; + node_dragged: LGraphNode | null; + node_in_panel: LGraphNode | null; + node_over: LGraphNode | null; + node_title_color: string; + node_widget: [LGraphNode, IWidget] | null; + /** Called by `LGraphCanvas.drawBackCanvas` */ + onDrawBackground: + | ((ctx: CanvasRenderingContext2D, visibleArea: Vector4) => void) + | null; + /** Called by `LGraphCanvas.drawFrontCanvas` */ + onDrawForeground: + | ((ctx: CanvasRenderingContext2D, visibleArea: Vector4) => void) + | null; + onDrawOverlay: ((ctx: CanvasRenderingContext2D) => void) | null; + /** Called by `LGraphCanvas.processMouseDown` */ + onMouse: ((event: MouseEvent) => boolean) | null; + /** Called by `LGraphCanvas.drawFrontCanvas` and `LGraphCanvas.drawLinkTooltip` */ + onDrawLinkTooltip: ((ctx: CanvasRenderingContext2D, link: LLink, _this: this) => void) | null; + /** Called by `LGraphCanvas.selectNodes` */ + onNodeMoved: ((node: LGraphNode) => void) | null; + /** Called by `LGraphCanvas.processNodeSelected` */ + onNodeSelected: ((node: LGraphNode) => void) | null; + /** Called by `LGraphCanvas.deselectNode` */ + onNodeDeselected: ((node: LGraphNode) => void) | null; + /** Called by `LGraphCanvas.processNodeDblClicked` */ + onShowNodePanel: ((node: LGraphNode) => void) | null; + /** Called by `LGraphCanvas.processNodeDblClicked` */ + onNodeDblClicked: ((node: LGraphNode) => void) | null; + /** Called by `LGraphCanvas.selectNodes` */ + onSelectionChange: ((nodes: Record) => void) | null; + /** Called by `LGraphCanvas.showSearchBox` */ + onSearchBox: + | (( + helper: Element, + value: string, + graphCanvas: LGraphCanvas + ) => string[]) + | null; + onSearchBoxSelection: + | ((name: string, event: MouseEvent, graphCanvas: LGraphCanvas) => void) + | null; + pause_rendering: boolean; + render_canvas_border: boolean; + render_collapsed_slots: boolean; + render_connection_arrows: boolean; + render_connections_border: boolean; + render_connections_shadows: boolean; + render_curved_connections: boolean; + render_execution_order: boolean; + render_only_selected: boolean; + render_shadows: boolean; + render_title_colored: boolean; + round_radius: number; + selected_group: null | LGraphGroup; + selected_group_resizing: boolean; + selected_nodes: Record; + show_info: boolean; + title_text_font: string; + /** set to true to render title bar with gradients */ + use_gradients: boolean; + visible_area: DragAndScale["visible_area"]; + visible_links: LLink[]; + visible_nodes: LGraphNode[]; + zoom_modify_alpha: boolean; + + /** clears all the data inside */ + clear(): void; + /** assigns a graph, you can reassign graphs to the same canvas */ + setGraph(graph: LGraph, skipClear?: boolean): void; + /** opens a graph contained inside a node in the current graph */ + openSubgraph(graph: LGraph): void; + /** closes a subgraph contained inside a node */ + closeSubgraph(): void; + /** assigns a canvas */ + setCanvas(canvas: HTMLCanvasElement, skipEvents?: boolean): void; + /** binds mouse, keyboard, touch and drag events to the canvas */ + bindEvents(): void; + /** unbinds mouse events from the canvas */ + unbindEvents(): void; + + /** + * this function allows to render the canvas using WebGL instead of Canvas2D + * this is useful if you plant to render 3D objects inside your nodes, it uses litegl.js for webgl and canvas2DtoWebGL to emulate the Canvas2D calls in webGL + **/ + enableWebGL(): void; + + /** + * marks as dirty the canvas, this way it will be rendered again + * @param fg if the foreground canvas is dirty (the one containing the nodes) + * @param bg if the background canvas is dirty (the one containing the wires) + */ + setDirty(fg: boolean, bg: boolean): void; + + /** + * Used to attach the canvas in a popup + * @return the window where the canvas is attached (the DOM root node) + */ + getCanvasWindow(): Window; + /** starts rendering the content of the canvas when needed */ + startRendering(): void; + /** stops rendering the content of the canvas (to save resources) */ + stopRendering(): void; + + processMouseDown(e: MouseEvent): boolean | undefined; + processMouseMove(e: MouseEvent): boolean | undefined; + processMouseUp(e: MouseEvent): boolean | undefined; + processMouseWheel(e: MouseEvent): boolean | undefined; + + /** returns true if a position (in graph space) is on top of a node little corner box */ + isOverNodeBox(node: LGraphNode, canvasX: number, canvasY: number): boolean; + /** returns true if a position (in graph space) is on top of a node input slot */ + isOverNodeInput( + node: LGraphNode, + canvasX: number, + canvasY: number, + slotPos: Vector2 + ): boolean; + + /** process a key event */ + processKey(e: KeyboardEvent): boolean | undefined; + + copyToClipboard(): void; + pasteFromClipboard(): void; + processDrop(e: DragEvent): void; + checkDropItem(e: DragEvent): void; + processNodeDblClicked(n: LGraphNode): void; + processNodeSelected(n: LGraphNode, e: MouseEvent): void; + processNodeDeselected(node: LGraphNode): void; + + /** selects a given node (or adds it to the current selection) */ + selectNode(node: LGraphNode, add?: boolean): void; + /** selects several nodes (or adds them to the current selection) */ + selectNodes(nodes?: LGraphNode[], add?: boolean): void; + /** removes a node from the current selection */ + deselectNode(node: LGraphNode): void; + /** removes all nodes from the current selection */ + deselectAllNodes(): void; + /** deletes all nodes in the current selection from the graph */ + deleteSelectedNodes(): void; + + /** centers the camera on a given node */ + centerOnNode(node: LGraphNode): void; + /** changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom */ + setZoom(value: number, center: Vector2): void; + /** brings a node to front (above all other nodes) */ + bringToFront(node: LGraphNode): void; + /** sends a node to the back (below all other nodes) */ + sendToBack(node: LGraphNode): void; + /** checks which nodes are visible (inside the camera area) */ + computeVisibleNodes(nodes: LGraphNode[]): LGraphNode[]; + /** renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes) */ + draw(forceFG?: boolean, forceBG?: boolean): void; + /** draws the front canvas (the one containing all the nodes) */ + drawFrontCanvas(): void; + /** draws some useful stats in the corner of the canvas */ + renderInfo(ctx: CanvasRenderingContext2D, x: number, y: number): void; + /** draws the back canvas (the one containing the background and the connections) */ + drawBackCanvas(): void; + /** draws the given node inside the canvas */ + drawNode(node: LGraphNode, ctx: CanvasRenderingContext2D): void; + /** draws graphic for node's slot */ + drawSlotGraphic(ctx: CanvasRenderingContext2D, pos: number[], shape: SlotShape, horizontal: boolean): void; + /** draws the shape of the given node in the canvas */ + drawNodeShape( + node: LGraphNode, + ctx: CanvasRenderingContext2D, + size: [number, number], + fgColor: string, + bgColor: string, + selected: boolean, + mouseOver: boolean + ): void; + /** draws every connection visible in the canvas */ + drawConnections(ctx: CanvasRenderingContext2D): void; + /** + * draws a link between two points + * @param a start pos + * @param b end pos + * @param link the link object with all the link info + * @param skipBorder ignore the shadow of the link + * @param flow show flow animation (for events) + * @param color the color for the link + * @param startDir the direction enum + * @param endDir the direction enum + * @param numSublines number of sublines (useful to represent vec3 or rgb) + **/ + renderLink( + a: Vector2, + b: Vector2, + link: object, + skipBorder: boolean, + flow: boolean, + color?: string, + startDir?: number, + endDir?: number, + numSublines?: number + ): void; + + computeConnectionPoint( + a: Vector2, + b: Vector2, + t: number, + startDir?: number, + endDir?: number + ): void; + + drawExecutionOrder(ctx: CanvasRenderingContext2D): void; + /** draws the widgets stored inside a node */ + drawNodeWidgets( + node: LGraphNode, + posY: number, + ctx: CanvasRenderingContext2D, + activeWidget: object + ): void; + /** process an event on widgets */ + processNodeWidgets( + node: LGraphNode, + pos: Vector2, + event: Event, + activeWidget: object + ): void; + /** draws every group area in the background */ + drawGroups(canvas: any, ctx: CanvasRenderingContext2D): void; + adjustNodesSize(): void; + /** resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode */ + resize(width?: number, height?: number): void; + /** + * switches to live mode (node shapes are not rendered, only the content) + * this feature was designed when graphs where meant to create user interfaces + **/ + switchLiveMode(transition?: boolean): void; + onNodeSelectionChange(): void; + touchHandler(event: TouchEvent): void; + + showLinkMenu(link: LLink, e: any): false; + prompt( + title: string, + value: any, + callback: Function, + event: any + ): HTMLDivElement; + showSearchBox(event?: MouseEvent): void; + showEditPropertyValue(node: LGraphNode, property: any, options: any): void; + createDialog( + html: string, + options?: { position?: Vector2; event?: MouseEvent } + ): void; + + convertOffsetToCanvas: DragAndScale["convertOffsetToCanvas"]; + convertCanvasToOffset: DragAndScale["convertCanvasToOffset"]; + /** converts event coordinates from canvas2D to graph coordinates */ + convertEventToCanvasOffset(e: MouseEvent): Vector2; + /** adds some useful properties to a mouse event, like the position in graph coordinates */ + adjustMouseEvent(e: MouseEvent): void; + + getCanvasMenuOptions(): ContextMenuItem[]; + getNodeMenuOptions(node: LGraphNode): ContextMenuItem[]; + getGroupMenuOptions(): ContextMenuItem[]; + /** Called by `getCanvasMenuOptions`, replace default options */ + getMenuOptions?(): ContextMenuItem[]; + /** Called by `getCanvasMenuOptions`, append to default options */ + getExtraMenuOptions?(): ContextMenuItem[]; + /** Called when mouse right click */ + processContextMenu(node: LGraphNode, event: Event): void; +} + +declare class ContextMenu { + static trigger( + element: HTMLElement, + event_name: string, + params: any, + origin: any + ): void; + static isCursorOverElement(event: MouseEvent, element: HTMLElement): void; + static closeAllContextMenus(window: Window): void; + constructor(values: ContextMenuItem[], options?: IContextMenuOptions, window?: Window); + options: IContextMenuOptions; + parentMenu?: ContextMenu; + lock: boolean; + current_submenu?: ContextMenu; + addItem( + name: string, + value: ContextMenuItem, + options?: IContextMenuOptions + ): void; + close(e?: MouseEvent, ignore_parent_menu?: boolean): void; + getTopMenu(): void; + getFirstEvent(): void; +} + +declare global { + interface CanvasRenderingContext2D { + /** like rect but rounded corners */ + roundRect( + x: number, + y: number, + width: number, + height: number, + radius: number, + radiusLow: number + ): void; + } + + interface Math { + clamp(v: number, min: number, max: number): number; + } +} diff --git a/ts/utils.ts b/ts/utils.ts new file mode 100644 index 0000000..0824e2b --- /dev/null +++ b/ts/utils.ts @@ -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; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3d51bd0 --- /dev/null +++ b/tsconfig.json @@ -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" + ] +}