Add Mute / Bypass Relay and Repeater

This commit is contained in:
rgthree
2023-08-31 22:05:57 -04:00
parent f53ce39b51
commit 4cfeb6878e
21 changed files with 544 additions and 138 deletions
+9
View File
@@ -1,3 +1,10 @@
"""
@author: rgthree
@title: Comfy Nodes
@nickname: rgthree
@description: A bunch of nodes I created that I also find useful.
"""
import glob
import inspect
import os
@@ -14,6 +21,7 @@ from .py.seed import RgthreeSeed
from .py.sdxl_empty_latent_image import RgthreeSDXLEmptyLatentImage
from .py.power_prompt import RgthreePowerPrompt
from .py.power_prompt_simple import RgthreePowerPromptSimple
from .py.image_inset_crop import RgthreeImageInsetCrop
NODE_CLASS_MAPPINGS = {
RgthreeContext.NAME: RgthreeContext,
@@ -24,6 +32,7 @@ NODE_CLASS_MAPPINGS = {
RgthreeSDXLEmptyLatentImage.NAME: RgthreeSDXLEmptyLatentImage,
RgthreePowerPrompt.NAME: RgthreePowerPrompt,
RgthreePowerPromptSimple.NAME: RgthreePowerPromptSimple,
RgthreeImageInsetCrop.NAME: RgthreeImageInsetCrop
}
THIS_DIR=os.path.dirname(os.path.abspath(__file__))
+24
View File
@@ -0,0 +1,24 @@
export class RgthreeBaseNode extends LGraphNode {
constructor(title = RgthreeBaseNode.title) {
super(title);
this.isVirtualNode = true;
if (title == '__NEED_NAME__') {
throw new Error('RgthreeBaseNode needs overrides.');
}
this.properties = this.properties || {};
}
onModeChange() {
}
set mode(mode) {
if (this.mode_ != mode) {
this.mode_ = mode;
this.onModeChange();
}
}
get mode() {
return this.mode_;
}
}
RgthreeBaseNode.title = "__NEED_NAME__";
RgthreeBaseNode.category = 'rgthree';
RgthreeBaseNode._category = 'rgthree';
+3 -10
View File
@@ -1,13 +1,9 @@
import { app } from "../../scripts/app.js";
export class BaseCollectorNode extends LGraphNode {
constructor(title = BaseCollectorNode.title) {
import { RgthreeBaseNode } from "./base_node.js";
export class BaseCollectorNode extends RgthreeBaseNode {
constructor(title) {
super(title);
this.isVirtualNode = true;
if (title == '__NEED_NAME__') {
throw new Error('BaseCollectorNode needs overrides.');
}
this.properties = this.properties || {};
this.connections = [];
this.addInput("", "*");
this.addOutput("Output", "*");
}
@@ -55,6 +51,3 @@ export class BaseCollectorNode extends LGraphNode {
}
}
}
BaseCollectorNode.title = "__NEED_NAME__";
BaseCollectorNode.category = 'rgthree';
BaseCollectorNode._category = 'rgthree';
+17 -20
View File
@@ -1,38 +1,37 @@
import { app } from "../../scripts/app.js";
import { addConnectionLayoutSupport, addMenuItem, doChainLookup, wait } from "./utils.js";
export class BaseNodeModeChanger extends LGraphNode {
constructor(title = BaseNodeModeChanger.title) {
import { RgthreeBaseNode } from "./base_node.js";
import { addConnectionLayoutSupport, addMenuItem, getConnectedInputNodes, wait } from "./utils.js";
export class BaseNodeModeChanger extends RgthreeBaseNode {
constructor(title) {
super(title);
this.isVirtualNode = true;
this.debouncer = 0;
this.schedulePromise = null;
this.isVirtualNode = true;
this.modeOn = -1;
this.modeOff = -1;
if (title == '__NEED_NAME__') {
throw new Error('BaseNodeModeChanger needs overrides.');
}
wait(10).then(() => {
if (this.modeOn < 0 || this.modeOff < 0) {
throw new Error('modeOn and modeOff must be overridden.');
}
});
this.properties = this.properties || {};
this.connections = [];
this.addInput("", "*");
}
scheduleRefreshWidgets() {
scheduleStabilizeWidgets() {
if (!this.schedulePromise) {
this.schedulePromise = new Promise((resolve) => {
setTimeout(() => {
resolve(this.refreshWidgets());
resolve(this.stabilizeWidgets());
this.schedulePromise = null;
}, 100);
});
}
return this.schedulePromise;
}
refreshWidgets() {
const linkedNodes = doChainLookup(app, this, this);
stabilizeWidgets() {
if (!this.graph) {
return;
}
const linkedNodes = getConnectedInputNodes(app, this);
this.stabilizeInputsOutputs();
for (const [index, node] of linkedNodes.entries()) {
let widget = this.widgets && this.widgets[index];
@@ -47,6 +46,7 @@ export class BaseNodeModeChanger extends LGraphNode {
this.widgets.length = linkedNodes.length;
}
app.graph.setDirtyCanvas(true, true);
setTimeout(() => { this.stabilizeWidgets(); }, 500);
}
setWidget(widget, linkedNode) {
const off = linkedNode.mode === this.modeOff;
@@ -60,10 +60,10 @@ export class BaseNodeModeChanger extends LGraphNode {
};
}
onConnectionsChainChange() {
this.scheduleRefreshWidgets();
this.scheduleStabilizeWidgets();
}
onConnectionsChange(_type, _index, _connected, _linkInfo, _ioSlot) {
this.scheduleRefreshWidgets();
this.scheduleStabilizeWidgets();
}
removeInput(slot) {
this._tempWidth = this.size[0];
@@ -107,7 +107,7 @@ export class BaseNodeModeChanger extends LGraphNode {
static setUp(clazz) {
addMenuItem(clazz, app, {
name: 'Refresh',
callback: (node) => { node.scheduleRefreshWidgets(); }
callback: (node) => { node.scheduleStabilizeWidgets(); }
});
addMenuItem(clazz, app, {
name: (node) => { var _a; return (`${((_a = node.properties) === null || _a === void 0 ? void 0 : _a['collapse_connections']) ? 'Show' : 'Collapse'} Connections`); },
@@ -116,11 +116,8 @@ export class BaseNodeModeChanger extends LGraphNode {
callback: (_node) => { app.graph.setDirtyCanvas(true, true); }
});
addConnectionLayoutSupport(clazz, app, [['Left'], ['Right']]);
LiteGraph.registerNodeType(clazz.title, clazz);
LiteGraph.registerNodeType(clazz.type, clazz);
clazz.category = clazz._category;
}
}
BaseNodeModeChanger.title = "__NEED_NAME__";
BaseNodeModeChanger.category = 'rgthree';
BaseNodeModeChanger._category = 'rgthree';
BaseNodeModeChanger.collapsible = false;
+3 -1
View File
@@ -1,5 +1,6 @@
import { app } from "../../scripts/app.js";
import { BaseNodeModeChanger } from "./base_node_mode_changer.js";
import { NodeTypesString } from "./constants.js";
const MODE_BYPASS = 4;
const MODE_ALWAYS = 0;
class BypasserNode extends BaseNodeModeChanger {
@@ -9,7 +10,8 @@ class BypasserNode extends BaseNodeModeChanger {
this.modeOff = MODE_BYPASS;
}
}
BypasserNode.title = "Fast Bypasser (rgthree)";
BypasserNode.type = NodeTypesString.FAST_BYPASSER;
BypasserNode.title = NodeTypesString.FAST_BYPASSER;
app.registerExtension({
name: "rgthree.Bypasser",
registerCustomNodes() {
+12
View File
@@ -0,0 +1,12 @@
export function addRgthree(str) {
return str + ' (rgthree)';
}
export function stripRgthree(str) {
return str.replace(/\s*\(rgthree\)$/, '');
}
export const NodeTypesString = {
NODE_MODE_RELAY: addRgthree('Mute / Bypass Relay'),
NODE_MODE_REPEATER: addRgthree('Mute / Bypass Repeater'),
FAST_MUTER: addRgthree('Fast Muter'),
FAST_BYPASSER: addRgthree('Fast Bypasser'),
};
+3 -1
View File
@@ -1,5 +1,6 @@
import { app } from "../../scripts/app.js";
import { BaseNodeModeChanger } from "./base_node_mode_changer.js";
import { NodeTypesString } from "./constants.js";
const MODE_MUTE = 2;
const MODE_ALWAYS = 0;
class MuterNode extends BaseNodeModeChanger {
@@ -9,7 +10,8 @@ class MuterNode extends BaseNodeModeChanger {
this.modeOff = MODE_MUTE;
}
}
MuterNode.title = "Fast Muter (rgthree)";
MuterNode.type = NodeTypesString.FAST_MUTER;
MuterNode.title = NodeTypesString.FAST_MUTER;
app.registerExtension({
name: "rgthree.Muter",
registerCustomNodes() {
+1 -1
View File
@@ -43,7 +43,7 @@ async function updateCombinerToCollector(node) {
}
newNode.pos = [...node.pos];
newNode.size = [...node.size];
newNode.properties = Object.assign({}, node.properties);
newNode.properties = { ...node.properties };
const links = [];
for (const [index, output] of node.outputs.entries()) {
for (const linkId of (output.links || [])) {
+86
View File
@@ -0,0 +1,86 @@
import { app } from "../../scripts/app.js";
import { addConnectionLayoutSupport, addHelp, getConnectedInputNodes, getConnectedOutputNodes, wait } from "./utils.js";
import { BaseCollectorNode } from './base_node_collector.js';
import { NodeTypesString, stripRgthree } from "./constants.js";
const MODE_ALWAYS = 0;
const MODE_MUTE = 2;
const MODE_BYPASS = 4;
const MODE_REPEATS = [MODE_MUTE, MODE_BYPASS];
class NodeModeRelay extends BaseCollectorNode {
constructor(title) {
super(title);
setTimeout(() => { this.stabilize(); }, 500);
this.removeOutput(0);
this.addOutput('REPEATER', '_NODE_REPEATER_', {
color_on: '#Fc0',
color_off: '#a80',
shape: LiteGraph.ARROW_SHAPE,
});
}
onConnectOutput(outputIndex, inputType, inputSlot, inputNode, inputIndex) {
var _a, _b;
let canConnect = true;
if (super.onConnectOutput) {
canConnect = (_a = super.onConnectOutput) === null || _a === void 0 ? void 0 : _a.call(this, outputIndex, inputType, inputSlot, inputNode, inputIndex);
}
let nextNode = (_b = getConnectedOutputNodes(app, this, inputNode)[0]) !== null && _b !== void 0 ? _b : inputNode;
return canConnect && nextNode.type === NodeTypesString.NODE_MODE_REPEATER;
}
onConnectionsChange(type, slotIndex, isConnected, link_info, ioSlot) {
super.onConnectionsChange(type, slotIndex, isConnected, link_info, ioSlot);
setTimeout(() => { this.stabilize(); }, 500);
}
stabilize() {
var _a;
if (!this.graph || !this.isAnyOutputConnected() || !this.isInputConnected(0)) {
return;
}
const inputNodes = getConnectedInputNodes(app, this);
let mode = undefined;
for (const inputNode of inputNodes) {
if (mode === undefined) {
mode = inputNode.mode;
}
else if (mode === inputNode.mode && MODE_REPEATS.includes(mode)) {
continue;
}
else if (inputNode.mode === MODE_ALWAYS || mode === MODE_ALWAYS) {
mode = MODE_ALWAYS;
}
else {
mode = null;
}
}
if (mode != null) {
if ((_a = this.outputs) === null || _a === void 0 ? void 0 : _a.length) {
const outputNodes = getConnectedOutputNodes(app, this);
for (const outputNode of outputNodes) {
outputNode.mode = mode;
wait(16).then(() => {
outputNode.setDirtyCanvas(true, true);
});
}
}
}
setTimeout(() => { this.stabilize(); }, 500);
}
}
NodeModeRelay.type = NodeTypesString.NODE_MODE_RELAY;
NodeModeRelay.title = NodeTypesString.NODE_MODE_RELAY;
NodeModeRelay.help = [
`This node will relay its input nodes' modes (Mute, Bypass, or Active) to a connected`,
`${stripRgthree(NodeTypesString.NODE_MODE_REPEATER)} (which would then repeat that mode change to all of its inputs).`,
`\n`,
`\n- When all connected input nodes are muted, the relay will set a connected repeater to mute.`,
`\n- When all connected input nodes are bypassed, the relay will set a connected repeater to bypass.`,
`\n- When any connected input nodes are active, the relay will set a connected repeater to active.`,
].join(' ');
app.registerExtension({
name: "rgthree.NodeModeRepeaterHelper",
registerCustomNodes() {
addHelp(NodeModeRelay, app);
addConnectionLayoutSupport(NodeModeRelay, app, [['Left', 'Right'], ['Right', 'Left']]);
LiteGraph.registerNodeType(NodeModeRelay.type, NodeModeRelay);
NodeModeRelay.category = NodeModeRelay._category;
},
});
+49 -18
View File
@@ -1,30 +1,61 @@
import { app } from "../../scripts/app.js";
import { addConnectionLayoutSupport, addHelp, doChainLookup } from "./utils.js";
import { BaseCollectorNode } from './base_node_collector.js';
import { NodeTypesString, stripRgthree } from "./constants.js";
import { addConnectionLayoutSupport, addHelp, getConnectedInputNodes, getConnectedOutputNodes } from "./utils.js";
class NodeModeRepeater extends BaseCollectorNode {
constructor(title) {
super(title);
this.removeOutput(0);
this.addOutput('FAST_TOGGLER', '_FAST_TOGGLER_', {
color_on: '#Fc0',
color_off: '#a80',
shape: LiteGraph.ARROW_SHAPE,
});
}
onConnectOutput(outputIndex, inputType, inputSlot, inputNode, inputIndex) {
var _a, _b;
let canConnect = true;
if (super.onConnectOutput) {
canConnect = (_a = super.onConnectOutput) === null || _a === void 0 ? void 0 : _a.call(this, outputIndex, inputType, inputSlot, inputNode, inputIndex);
}
let nextNode = (_b = getConnectedOutputNodes(app, this, inputNode)[0]) !== null && _b !== void 0 ? _b : inputNode;
return canConnect && (nextNode.type === NodeTypesString.FAST_MUTER || nextNode.type === NodeTypesString.FAST_BYPASSER);
}
onConnectionsChange(type, slotIndex, isConnected, linkInfo, ioSlot) {
super.onConnectionsChange(type, slotIndex, isConnected, linkInfo, ioSlot);
if (type === LiteGraph.INPUT && isConnected) {
const connectedNode = this.getInputNode(slotIndex);
if ((connectedNode === null || connectedNode === void 0 ? void 0 : connectedNode.type) === NodeTypesString.NODE_MODE_RELAY) {
const input = this.inputs[slotIndex];
if (input) {
input.color_on = '#FC0';
input.color_off = '#a80';
}
}
}
}
onModeChange() {
const linkedNodes = doChainLookup(app, this, this);
super.onModeChange();
const linkedNodes = getConnectedInputNodes(app, this);
for (const node of linkedNodes) {
node.mode = this.mode;
if (node.type !== NodeTypesString.NODE_MODE_RELAY) {
node.mode = this.mode;
}
}
}
set mode(mode) {
if (this.mode_ != mode) {
this.mode_ = mode;
this.onModeChange();
}
}
get mode() {
return this.mode_;
}
}
NodeModeRepeater.type = "Node Mode Repeater (rgthree)";
NodeModeRepeater.title = "Node Mode Repeater (rgthree)";
NodeModeRepeater.type = NodeTypesString.NODE_MODE_REPEATER;
NodeModeRepeater.title = NodeTypesString.NODE_MODE_REPEATER;
NodeModeRepeater.help = [
`Connect other nodes\' outputs to this Node Mode Repeater and all connected nodes`,
`will update their mode (mute/bypass/active) when this node's mode changes.`,
`\n\nOptionally, connect this mode's output to a Fast Muter or Fast Bypasser for a single toggle`,
`to then quickly mute or bypass this node and all its connected nodes.`
`When this node's mode (Mute, Bypass, Active) changes, it will "repeat" that mode to all`,
`connected input nodes.`,
`\n`,
`\n- Optionally, connect this mode's output to a ${stripRgthree(NodeTypesString.FAST_MUTER)}`,
`or ${stripRgthree(NodeTypesString.FAST_BYPASSER)} for a single toggle to quickly`,
`mute/bypass all its connected nodes.`,
`\n- Optionally, connect a ${stripRgthree(NodeTypesString.NODE_MODE_RELAY)} to this nodes'`,
`inputs to have it automatically toggle its mode. If connected, this will always take`,
`precedence`,
].join(' ');
app.registerExtension({
name: "rgthree.NodeModeRepeater",
+36 -9
View File
@@ -5,6 +5,11 @@ api.getNodeDefs = async function () {
this.dispatchEvent(new CustomEvent('fresh-node-defs', { detail: defs }));
return defs;
};
var IoDirection;
(function (IoDirection) {
IoDirection[IoDirection["INPUT"] = 0] = "INPUT";
IoDirection[IoDirection["OUTPUT"] = 1] = "OUTPUT";
})(IoDirection || (IoDirection = {}));
const PADDING = 0;
export const LAYOUT_LABEL_TO_DATA = {
'Left': [LiteGraph.LEFT, [0, 0.5], [PADDING, 0]],
@@ -226,7 +231,7 @@ export function addHelp(node, app) {
const help = node.help;
if (help) {
addMenuItem(node, app, {
name: 'Node Help',
name: '🛟 Node Help',
property: 'help',
callback: (_node) => { alert(help); }
});
@@ -239,34 +244,56 @@ export function isPassThroughType(node) {
|| (type === null || type === void 0 ? void 0 : type.includes('Node Combiner'))
|| (type === null || type === void 0 ? void 0 : type.includes('Node Collector'));
}
export function doChainLookup(app, startNode, currentNode) {
export function getConnectedInputNodes(app, startNode, currentNode) {
return getConnectedNodes(app, startNode, IoDirection.INPUT, currentNode);
}
export function getConnectedOutputNodes(app, startNode, currentNode) {
return getConnectedNodes(app, startNode, IoDirection.OUTPUT, currentNode);
}
function getConnectedNodes(app, startNode, dir = IoDirection.INPUT, currentNode) {
var _a, _b;
currentNode = currentNode || startNode;
let rootNodes = [];
const slotsToRemove = [];
if (startNode === currentNode || isPassThroughType(currentNode)) {
const removeDups = startNode === currentNode;
for (const input of currentNode.inputs) {
const linkId = input.link;
let linkIds;
if (dir == IoDirection.OUTPUT) {
linkIds = (_a = currentNode.outputs) === null || _a === void 0 ? void 0 : _a.flatMap(i => i.links);
}
else {
linkIds = (_b = currentNode.inputs) === null || _b === void 0 ? void 0 : _b.map(i => i.link);
}
let graph = app.graph;
for (const linkId of linkIds) {
if (!linkId) {
continue;
}
const link = app.graph.links[linkId];
const originNode = app.graph.getNodeById(link.origin_id);
const link = graph.links[linkId];
const connectedId = dir == IoDirection.OUTPUT ? link.target_id : link.origin_id;
const originNode = graph.getNodeById(connectedId);
if (isPassThroughType(originNode)) {
for (const foundNode of doChainLookup(app, startNode, originNode)) {
for (const foundNode of getConnectedNodes(app, startNode, dir, originNode)) {
if (!rootNodes.includes(foundNode)) {
rootNodes.push(foundNode);
}
}
}
else if (rootNodes.includes(originNode)) {
removeDups && (slotsToRemove.push(link.target_slot));
const connectedSlot = dir == IoDirection.OUTPUT ? link.origin_slot : link.target_slot;
removeDups && (slotsToRemove.push(connectedSlot));
}
else {
rootNodes.push(originNode);
}
}
for (const slot of slotsToRemove) {
startNode.disconnectInput(slot);
if (dir == IoDirection.OUTPUT) {
startNode.disconnectOutput(slot);
}
else {
startNode.disconnectInput(slot);
}
}
}
return rootNodes;
+48
View File
@@ -0,0 +1,48 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import {app} from "../../scripts/app.js";
import { NodeMode } from "./typings/comfy.js";
import type {LGraphNode as TLGraphNode, LiteGraph as TLiteGraph} from './typings/litegraph.js';
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
export class RgthreeBaseNode extends LGraphNode {
static override title = "__NEED_NAME__";
// `category` seems to get reset at register, so we'll
// re-reset it after the register call. ¯\_(ツ)_/¯
static category = 'rgthree';
static _category = 'rgthree';
isVirtualNode = true;
constructor(title = RgthreeBaseNode.title) {
super(title);
if (title == '__NEED_NAME__') {
throw new Error('RgthreeBaseNode needs overrides.');
}
this.properties = this.properties || {};
}
mode_: NodeMode;
/** When a mode change, we want all connected nodes to match. */
onModeChange() {
// Override
}
// @ts-ignore - Changing the property to an accessor here seems to work, but ts compiler complains.
override set mode(mode: NodeMode) {
if (this.mode_ != mode) {
this.mode_ = mode;
this.onModeChange();
}
}
override get mode() {
return this.mode_;
}
}
+6 -15
View File
@@ -1,29 +1,20 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import { app } from "../../scripts/app.js";
import type {LLink, LGraph, INodeInputSlot, INodeOutputSlot, LGraphNode as TLGraphNode} from './typings/litegraph.js';
// @ts-ignore
import { ComfyWidgets } from "../../scripts/widgets.js";
import type {LLink, LGraph, INodeInputSlot, INodeOutputSlot, LGraphNode as TLGraphNode} from './typings/litegraph.js';
import { RgthreeBaseNode } from "./base_node.js";
declare const LGraphNode: typeof TLGraphNode;
export class BaseCollectorNode extends LGraphNode {
export class BaseCollectorNode extends RgthreeBaseNode {
static override title = "__NEED_NAME__";
// `category` seems to get reset at register, so we'll
// re-reset it after the register call. ¯\_(ツ)_/¯
static category = 'rgthree';
static _category = 'rgthree';
override isVirtualNode = true;
isVirtualNode = true;
constructor(title = BaseCollectorNode.title) {
constructor(title?: string) {
super(title);
if (title == '__NEED_NAME__') {
throw new Error('BaseCollectorNode needs overrides.');
}
this.properties = this.properties || {};
this.connections = [];
this.addInput("", "*");
this.addOutput("Output", "*");
}
+20 -23
View File
@@ -1,47 +1,41 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import {app} from "../../scripts/app.js";
import { RgthreeBaseNode } from "./base_node.js";
import type {Vector2, LLink, INodeInputSlot, INodeOutputSlot, LGraphNode as TLGraphNode, LiteGraph as TLiteGraph, IWidget} from './typings/litegraph.js';
import { addConnectionLayoutSupport, addMenuItem, doChainLookup, wait } from "./utils.js";
import { addConnectionLayoutSupport, addMenuItem, getConnectedInputNodes, wait } from "./utils.js";
declare const LiteGraph: typeof TLiteGraph;
declare const LGraphNode: typeof TLGraphNode;
export class BaseNodeModeChanger extends LGraphNode {
export class BaseNodeModeChanger extends RgthreeBaseNode {
static override title = "__NEED_NAME__";
// `category` seems to get reset at register, so we'll
// re-reset it after the register call. ¯\_(ツ)_/¯
static category = 'rgthree';
static _category = 'rgthree';
static collapsible = false;
override isVirtualNode = true;
debouncer: number = 0;
schedulePromise: Promise<void> | null = null;
isVirtualNode = true;
// These Must be overriden
readonly modeOn: number = -1;
readonly modeOff: number = -1;
constructor(title = BaseNodeModeChanger.title) {
constructor(title?: string) {
super(title);
if (title == '__NEED_NAME__') {
throw new Error('BaseNodeModeChanger needs overrides.');
}
wait(10).then(() => {
if (this.modeOn < 0 || this.modeOff < 0) {
throw new Error('modeOn and modeOff must be overridden.');
}
});
this.properties = this.properties || {};
this.connections = [];
this.addInput("", "*");
}
scheduleRefreshWidgets() {
scheduleStabilizeWidgets() {
if (!this.schedulePromise) {
this.schedulePromise = new Promise((resolve) => {
setTimeout(() => {
resolve(this.refreshWidgets());
resolve(this.stabilizeWidgets());
this.schedulePromise = null;
}, 100);
});
@@ -49,8 +43,11 @@ export class BaseNodeModeChanger extends LGraphNode {
return this.schedulePromise;
}
refreshWidgets() {
const linkedNodes = doChainLookup(app, this, this);
stabilizeWidgets() {
if (!this.graph) {
return;
}
const linkedNodes = getConnectedInputNodes(app, this);
this.stabilizeInputsOutputs();
for (const [index, node] of linkedNodes.entries()) {
let widget = this.widgets && this.widgets[index];
@@ -69,6 +66,7 @@ export class BaseNodeModeChanger extends LGraphNode {
this.widgets.length = linkedNodes.length
}
app.graph.setDirtyCanvas(true, true);
setTimeout(() => { this.stabilizeWidgets(); }, 500);
}
setWidget(widget: IWidget, linkedNode: TLGraphNode) {
@@ -84,13 +82,12 @@ export class BaseNodeModeChanger extends LGraphNode {
}
onConnectionsChainChange() {
this.scheduleRefreshWidgets();
this.scheduleStabilizeWidgets();
}
override onConnectionsChange(_type: number, _index: number, _connected: boolean, _linkInfo: LLink, _ioSlot: (INodeOutputSlot | INodeInputSlot)) {
this.scheduleRefreshWidgets();
this.scheduleStabilizeWidgets();
}
override removeInput(slot: number) {
@@ -138,7 +135,7 @@ export class BaseNodeModeChanger extends LGraphNode {
// @ts-ignore: Fix incorrect litegraph typings.
addMenuItem(clazz, app, {
name: 'Refresh',
callback: (node) => {(node as T).scheduleRefreshWidgets()}
callback: (node) => {(node as T).scheduleStabilizeWidgets()}
});
// @ts-ignore: Fix incorrect litegraph typings.
@@ -152,7 +149,7 @@ export class BaseNodeModeChanger extends LGraphNode {
// @ts-ignore: Fix incorrect litegraph typings.
addConnectionLayoutSupport(clazz, app, [['Left'],['Right']]);
LiteGraph.registerNodeType((clazz as any).title, clazz);
LiteGraph.registerNodeType((clazz as any).type, clazz);
(clazz as any).category = (clazz as any)._category;
}
}
+3 -1
View File
@@ -2,6 +2,7 @@
// @ts-ignore
import {app} from "../../scripts/app.js";
import { BaseNodeModeChanger } from "./base_node_mode_changer.js";
import { NodeTypesString } from "./constants.js";
import type {LGraphNode} from './typings/litegraph.js';
const MODE_BYPASS = 4;
@@ -9,7 +10,8 @@ const MODE_ALWAYS = 0;
class BypasserNode extends BaseNodeModeChanger {
static override title = "Fast Bypasser (rgthree)";
static override type = NodeTypesString.FAST_BYPASSER;
static override title = NodeTypesString.FAST_BYPASSER;
override readonly modeOn = MODE_ALWAYS;
override readonly modeOff = MODE_BYPASS;
+15
View File
@@ -0,0 +1,15 @@
export function addRgthree(str: string) {
return str + ' (rgthree)';
}
export function stripRgthree(str: string) {
return str.replace(/\s*\(rgthree\)$/, '');
}
export const NodeTypesString = {
NODE_MODE_RELAY: addRgthree('Mute / Bypass Relay'),
NODE_MODE_REPEATER: addRgthree('Mute / Bypass Repeater'),
FAST_MUTER: addRgthree('Fast Muter'),
FAST_BYPASSER: addRgthree('Fast Bypasser'),
}
+3 -1
View File
@@ -2,6 +2,7 @@
// @ts-ignore
import {app} from "../../scripts/app.js";
import { BaseNodeModeChanger } from "./base_node_mode_changer.js";
import { NodeTypesString } from "./constants.js";
import type {LGraphNode} from './typings/litegraph.js';
const MODE_MUTE = 2;
@@ -9,7 +10,8 @@ const MODE_ALWAYS = 0;
class MuterNode extends BaseNodeModeChanger {
static override title = "Fast Muter (rgthree)";
static override type = NodeTypesString.FAST_MUTER;
static override title = NodeTypesString.FAST_MUTER;
override readonly modeOn = MODE_ALWAYS;
override readonly modeOff = MODE_MUTE;
+111
View File
@@ -0,0 +1,111 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import { app } from "../../scripts/app.js";
import type {INodeInputSlot, INodeOutputSlot, LGraphNode, LLink, LiteGraph as TLiteGraph,} from './typings/litegraph.js';
import { addConnectionLayoutSupport, addHelp, getConnectedInputNodes, getConnectedOutputNodes, wait} from "./utils.js";
// @ts-ignore
import { ComfyWidgets } from "../../scripts/widgets.js";
// @ts-ignore
import { BaseCollectorNode } from './base_node_collector.js';
import { NodeMode } from "./typings/comfy.js";
import { NodeTypesString, stripRgthree } from "./constants.js";
declare const LiteGraph: typeof TLiteGraph;
const MODE_ALWAYS = 0;
const MODE_MUTE = 2;
const MODE_BYPASS = 4;
const MODE_REPEATS = [MODE_MUTE, MODE_BYPASS];
class NodeModeRelay extends BaseCollectorNode {
static override type = NodeTypesString.NODE_MODE_RELAY;
static override title = NodeTypesString.NODE_MODE_RELAY;
static help = [
`This node will relay its input nodes' modes (Mute, Bypass, or Active) to a connected`,
`${stripRgthree(NodeTypesString.NODE_MODE_REPEATER)} (which would then repeat that mode change to all of its inputs).`,
`\n`,
`\n- When all connected input nodes are muted, the relay will set a connected repeater to mute.`,
`\n- When all connected input nodes are bypassed, the relay will set a connected repeater to bypass.`,
`\n- When any connected input nodes are active, the relay will set a connected repeater to active.`,
].join(' ');
constructor(title?: string) {
super(title);
setTimeout(() => { this.stabilize(); }, 500);
this.removeOutput(0);
this.addOutput('REPEATER', '_NODE_REPEATER_', {
color_on: '#Fc0',
color_off: '#a80',
shape: LiteGraph.ARROW_SHAPE,
});
}
override onConnectOutput(outputIndex: number, inputType: string | -1, inputSlot: INodeInputSlot, inputNode: LGraphNode, inputIndex: number): boolean {
let canConnect = true;
if (super.onConnectOutput) {
canConnect = super.onConnectOutput?.(outputIndex, inputType, inputSlot, inputNode, inputIndex);
}
let nextNode = getConnectedOutputNodes(app, this, inputNode)[0] ?? inputNode;
return canConnect && nextNode.type === NodeTypesString.NODE_MODE_REPEATER;
}
override onConnectionsChange(type: number, slotIndex: number, isConnected: boolean, link_info: LLink, ioSlot: INodeOutputSlot | INodeInputSlot): void {
super.onConnectionsChange(type, slotIndex, isConnected, link_info, ioSlot);
setTimeout(() => { this.stabilize(); }, 500);
}
stabilize() {
// If we aren't connected to a repeater, then theres no sense in checking. And if we are, but
// have no inputs, then we're also not ready.
if (!this.graph || !this.isAnyOutputConnected() || !this.isInputConnected(0)) {
return;
}
const inputNodes = getConnectedInputNodes(app, this);
let mode: NodeMode|null = undefined;
for (const inputNode of inputNodes) {
// If we haven't set our mode to be, then let's set it. Otherwise, mode will stick if it
// remains constant, otherwise, if we hit an ALWAYS, then we'll unmute all repeaters and
// if not then we won't do anything.
if (mode === undefined) {
mode = inputNode.mode;
} else if (mode === inputNode.mode && MODE_REPEATS.includes(mode)) {
continue;
} else if (inputNode.mode === MODE_ALWAYS || mode === MODE_ALWAYS) {
mode = MODE_ALWAYS;
} else {
mode = null;
}
}
if (mode != null) {
if (this.outputs?.length) {
const outputNodes = getConnectedOutputNodes(app, this);
for (const outputNode of outputNodes) {
outputNode.mode = mode
wait(16).then(() => {
outputNode.setDirtyCanvas(true, true);
});
}
}
}
setTimeout(() => { this.stabilize(); }, 500);
}
}
app.registerExtension({
name: "rgthree.NodeModeRepeaterHelper",
registerCustomNodes() {
addHelp(NodeModeRelay, app);
addConnectionLayoutSupport(NodeModeRelay, app, [['Left','Right'],['Right','Left']]);
LiteGraph.registerNodeType(NodeModeRelay.type, NodeModeRelay);
NodeModeRelay.category = NodeModeRelay._category;
},
});
+57 -27
View File
@@ -1,50 +1,80 @@
// / <reference path="../node_modules/litegraph.js/src/litegraph.d.ts" />
// @ts-ignore
import { app } from "../../scripts/app.js";
import type {LiteGraph as TLiteGraph,} from './typings/litegraph.js';
import { addConnectionLayoutSupport, addHelp, doChainLookup} from "./utils.js";
// @ts-ignore
import { ComfyWidgets } from "../../scripts/widgets.js";
// @ts-ignore
import { BaseCollectorNode } from './base_node_collector.js';
import { NodeMode } from "./typings/comfy.js";
import { NodeTypesString, stripRgthree } from "./constants.js";
import type {INodeInputSlot, INodeOutputSlot, LGraphNode, LLink, LiteGraph as TLiteGraph,} from './typings/litegraph.js';
import { addConnectionLayoutSupport, addHelp, getConnectedInputNodes, getConnectedOutputNodes} from "./utils.js";
declare const LiteGraph: typeof TLiteGraph;
/** Legacy "Combiner" */
class NodeModeRepeater extends BaseCollectorNode {
static override type = "Node Mode Repeater (rgthree)";
static override title = "Node Mode Repeater (rgthree)";
static override type = NodeTypesString.NODE_MODE_REPEATER;
static override title = NodeTypesString.NODE_MODE_REPEATER;
static help = [
`Connect other nodes\' outputs to this Node Mode Repeater and all connected nodes`,
`will update their mode (mute/bypass/active) when this node's mode changes.`,
`\n\nOptionally, connect this mode's output to a Fast Muter or Fast Bypasser for a single toggle`,
`to then quickly mute or bypass this node and all its connected nodes.`
`When this node's mode (Mute, Bypass, Active) changes, it will "repeat" that mode to all`,
`connected input nodes.`,
`\n`,
`\n- Optionally, connect this mode's output to a ${stripRgthree(NodeTypesString.FAST_MUTER)}`,
`or ${stripRgthree(NodeTypesString.FAST_BYPASSER)} for a single toggle to quickly`,
`mute/bypass all its connected nodes.`,
`\n- Optionally, connect a ${stripRgthree(NodeTypesString.NODE_MODE_RELAY)} to this nodes'`,
`inputs to have it automatically toggle its mode. If connected, this will always take`,
`precedence`,
].join(' ');
mode_: NodeMode;
constructor(title?: string) {
super(title);
this.removeOutput(0);
this.addOutput('FAST_TOGGLER', '_FAST_TOGGLER_', {
color_on: '#Fc0',
color_off: '#a80',
shape: LiteGraph.ARROW_SHAPE,
});
}
/** When a mode change, we want all connected nodes to match. */
onModeChange() {
const linkedNodes = doChainLookup(app, this, this);
override onConnectOutput(outputIndex: number, inputType: string | -1, inputSlot: INodeInputSlot, inputNode: LGraphNode, inputIndex: number): boolean {
let canConnect = true;
if (super.onConnectOutput) {
canConnect = super.onConnectOutput?.(outputIndex, inputType, inputSlot, inputNode, inputIndex);
}
// Output can only connect to a FAST MUTER or FAST BYPASSER
let nextNode = getConnectedOutputNodes(app, this, inputNode)[0] ?? inputNode;
return canConnect && (nextNode.type === NodeTypesString.FAST_MUTER || nextNode.type === NodeTypesString.FAST_BYPASSER);
}
override onConnectionsChange(type: number, slotIndex: number, isConnected: boolean, linkInfo: LLink, ioSlot: INodeOutputSlot | INodeInputSlot): void {
super.onConnectionsChange(type, slotIndex, isConnected, linkInfo, ioSlot);
// If we've added an input, let's see if it's a relay and change our shape and color.
if (type === LiteGraph.INPUT && isConnected) {
const connectedNode = this.getInputNode(slotIndex);
if (connectedNode?.type === NodeTypesString.NODE_MODE_RELAY) {
const input = this.inputs[slotIndex]
if (input) {
input.color_on = '#FC0';
input.color_off = '#a80';
}
}
}
}
/** When a mode change, we want all connected nodes to match except for connected relays. */
override onModeChange() {
super.onModeChange();
const linkedNodes = getConnectedInputNodes(app, this);
for (const node of linkedNodes) {
node.mode = this.mode;
if (node.type !== NodeTypesString.NODE_MODE_RELAY) {
node.mode = this.mode;
}
}
}
// @ts-ignore - Changing the property to an accessor here seems to work, but ts compiler complains.
override set mode(mode: NodeMode) {
if (this.mode_ != mode) {
this.mode_ = mode;
this.onModeChange();
}
}
override get mode() {
return this.mode_;
}
}
+37 -10
View File
@@ -18,6 +18,11 @@ api.getNodeDefs = async function() {
declare const LGraphNode: typeof TLGraphNode;
declare const LiteGraph: typeof TLiteGraph;
enum IoDirection {
INPUT,
OUTPUT,
}
const PADDING = 0;
type LiteGraphDir = typeof LiteGraph.LEFT | typeof LiteGraph.RIGHT | typeof LiteGraph.UP | typeof LiteGraph.DOWN;
@@ -279,7 +284,7 @@ export function addHelp(node: typeof LGraphNode, app: ComfyApp) {
const help = (node as any).help as string;
if (help) {
addMenuItem(node, app, {
name: 'Node Help',
name: '🛟 Node Help',
property: 'help',
callback: (_node) => { alert(help); }
});
@@ -302,33 +307,55 @@ export function isPassThroughType(node: TLGraphNode|null) {
* Looks through the immediate chain of a node to collect all connected nodes, passing through nodes
* like reroute, etc. Will also disconnect duplicate nodes from a provided node
*/
export function doChainLookup(app: ComfyApp, startNode: TLGraphNode, currentNode: TLGraphNode) {
export function getConnectedInputNodes(app: ComfyApp, startNode: TLGraphNode, currentNode?: TLGraphNode) {
return getConnectedNodes(app, startNode, IoDirection.INPUT, currentNode);
}
export function getConnectedOutputNodes(app: ComfyApp, startNode: TLGraphNode, currentNode?: TLGraphNode) {
return getConnectedNodes(app, startNode, IoDirection.OUTPUT, currentNode);
}
function getConnectedNodes(app: ComfyApp, startNode: TLGraphNode, dir = IoDirection.INPUT, currentNode?: TLGraphNode) {
currentNode = currentNode || startNode;
let rootNodes: TLGraphNode[] = [];
const slotsToRemove = [];
if (startNode === currentNode || isPassThroughType(currentNode)) {
const removeDups = startNode === currentNode;
for (const input of currentNode.inputs) {
const linkId: number | null = input!.link;
let linkIds: Array<number|null>;
if (dir == IoDirection.OUTPUT) {
linkIds = currentNode.outputs?.flatMap(i => i.links);
} else {
linkIds = currentNode.inputs?.map(i => i.link);
}
let graph = app.graph as LGraph;
for (const linkId of linkIds) {
if (!linkId) {
continue;
}
const link: LLink = (app.graph as LGraph).links[linkId]!;
const originNode: TLGraphNode = (app.graph as LGraph).getNodeById(link.origin_id)!;
const link: LLink = graph.links[linkId]!;
const connectedId = dir == IoDirection.OUTPUT ? link.target_id : link.origin_id;
const originNode: TLGraphNode = graph.getNodeById(connectedId)!;
if (isPassThroughType(originNode)) {
for (const foundNode of doChainLookup(app, startNode, originNode)) {
for (const foundNode of getConnectedNodes(app, startNode, dir, originNode)) {
if (!rootNodes.includes(foundNode)) {
rootNodes.push(foundNode);
}
}
} else if (rootNodes.includes(originNode)) {
removeDups && (slotsToRemove.push(link.target_slot))
const connectedSlot = dir == IoDirection.OUTPUT ? link.origin_slot : link.target_slot;
removeDups && (slotsToRemove.push(connectedSlot))
} else {
rootNodes.push(originNode);
}
}
for (const slot of slotsToRemove) {
startNode.disconnectInput(slot);
if (dir == IoDirection.OUTPUT) {
startNode.disconnectOutput(slot);
} else {
startNode.disconnectInput(slot);
}
}
}
return rootNodes;
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2017",
"target": "es2019",
"module": "ESNext",
"typeRoots": [
"./ts/typings",