diff --git a/__init__.py b/__init__.py index 5764c46..db1fd8d 100644 --- a/__init__.py +++ b/__init__.py @@ -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__)) diff --git a/js/base_node.js b/js/base_node.js new file mode 100644 index 0000000..eb34ccd --- /dev/null +++ b/js/base_node.js @@ -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'; diff --git a/js/base_node_collector.js b/js/base_node_collector.js index 59b5486..0b30221 100644 --- a/js/base_node_collector.js +++ b/js/base_node_collector.js @@ -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'; diff --git a/js/base_node_mode_changer.js b/js/base_node_mode_changer.js index 3e24ea2..eedc49a 100644 --- a/js/base_node_mode_changer.js +++ b/js/base_node_mode_changer.js @@ -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; diff --git a/js/bypasser.js b/js/bypasser.js index d076af5..ddbafa1 100644 --- a/js/bypasser.js +++ b/js/bypasser.js @@ -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() { diff --git a/js/constants.js b/js/constants.js new file mode 100644 index 0000000..969c30b --- /dev/null +++ b/js/constants.js @@ -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'), +}; diff --git a/js/muter.js b/js/muter.js index 12da42b..ea42bf3 100644 --- a/js/muter.js +++ b/js/muter.js @@ -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() { diff --git a/js/node_collector.js b/js/node_collector.js index 299f348..f46d031 100644 --- a/js/node_collector.js +++ b/js/node_collector.js @@ -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 || [])) { diff --git a/js/node_mode_relay.js b/js/node_mode_relay.js new file mode 100644 index 0000000..ac7b6c6 --- /dev/null +++ b/js/node_mode_relay.js @@ -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; + }, +}); diff --git a/js/node_mode_repeater.js b/js/node_mode_repeater.js index 0940cc5..0e379c7 100644 --- a/js/node_mode_repeater.js +++ b/js/node_mode_repeater.js @@ -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", diff --git a/js/utils.js b/js/utils.js index 5f451f2..3fc2812 100644 --- a/js/utils.js +++ b/js/utils.js @@ -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; diff --git a/ts/base_node.ts b/ts/base_node.ts new file mode 100644 index 0000000..cd7f341 --- /dev/null +++ b/ts/base_node.ts @@ -0,0 +1,48 @@ +// / +// @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_; + } + +} \ No newline at end of file diff --git a/ts/base_node_collector.ts b/ts/base_node_collector.ts index e7f3d8e..899c467 100644 --- a/ts/base_node_collector.ts +++ b/ts/base_node_collector.ts @@ -1,29 +1,20 @@ // / // @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", "*"); } diff --git a/ts/base_node_mode_changer.ts b/ts/base_node_mode_changer.ts index 4694def..75b87a6 100644 --- a/ts/base_node_mode_changer.ts +++ b/ts/base_node_mode_changer.ts @@ -1,47 +1,41 @@ // / // @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 | 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; } } diff --git a/ts/bypasser.ts b/ts/bypasser.ts index 9cd3bc1..4223aea 100644 --- a/ts/bypasser.ts +++ b/ts/bypasser.ts @@ -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; diff --git a/ts/constants.ts b/ts/constants.ts new file mode 100644 index 0000000..adfa401 --- /dev/null +++ b/ts/constants.ts @@ -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'), +} \ No newline at end of file diff --git a/ts/muter.ts b/ts/muter.ts index edb7f52..955b4ba 100644 --- a/ts/muter.ts +++ b/ts/muter.ts @@ -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; diff --git a/ts/node_mode_relay.ts b/ts/node_mode_relay.ts new file mode 100644 index 0000000..a5fe3fc --- /dev/null +++ b/ts/node_mode_relay.ts @@ -0,0 +1,111 @@ +// / +// @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; + }, +}); \ No newline at end of file diff --git a/ts/node_mode_repeater.ts b/ts/node_mode_repeater.ts index 71afe65..eee3437 100644 --- a/ts/node_mode_repeater.ts +++ b/ts/node_mode_repeater.ts @@ -1,50 +1,80 @@ // / // @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_; - } } diff --git a/ts/utils.ts b/ts/utils.ts index 2fdc063..eae1ff2 100644 --- a/ts/utils.ts +++ b/ts/utils.ts @@ -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; + 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; -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index cd84881..5204a57 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es2017", + "target": "es2019", "module": "ESNext", "typeRoots": [ "./ts/typings",