import { app } from "scripts/app.js"; import type { INodeInputSlot, INodeOutputSlot, LGraphCanvas, LGraphNode, LLink, SerializedLGraphNode, Vector2, } from "typings/litegraph.js"; import type { NodeMode } from "typings/comfy.js"; import { PassThroughFollowing, addConnectionLayoutSupport, getConnectedInputNodesAndFilterPassThroughs, getConnectedOutputNodesAndFilterPassThroughs, } from "./utils.js"; import { wait } from "rgthree/common/shared_utils.js"; import { BaseCollectorNode } from "./base_node_collector.js"; import { NodeTypesString, stripRgthree } from "./constants.js"; import { fitString } from "./utils_canvas.js"; import { rgthree } from "./rgthree.js"; const MODE_ALWAYS = 0; const MODE_MUTE = 2; const MODE_BYPASS = 4; const MODE_REPEATS = [MODE_MUTE, MODE_BYPASS]; const MODE_NOTHING = -99; // MADE THIS UP. const MODE_TO_OPTION = new Map([ [MODE_ALWAYS, "ACTIVE"], [MODE_MUTE, "MUTE"], [MODE_BYPASS, "BYPASS"], [MODE_NOTHING, "NOTHING"], ]); const OPTION_TO_MODE = new Map([ ["ACTIVE", MODE_ALWAYS], ["MUTE", MODE_MUTE], ["BYPASS", MODE_BYPASS], ["NOTHING", MODE_NOTHING], ]); const MODE_TO_PROPERTY = new Map([ [MODE_MUTE, "on_muted_inputs"], [MODE_BYPASS, "on_bypassed_inputs"], [MODE_ALWAYS, "on_any_active_inputs"], ]); const logger = rgthree.newLogSession("[NodeModeRelay]"); /** * Like a BaseCollectorNode, this relay node connects to a Repeater node and _relays_ mode changes * changes to the repeater (so it can go on to modify its connections). */ class NodeModeRelay extends BaseCollectorNode { override readonly inputsPassThroughFollowing: PassThroughFollowing = PassThroughFollowing.ALL; static override type = NodeTypesString.NODE_MODE_RELAY; static override title = NodeTypesString.NODE_MODE_RELAY; override comfyClass = NodeTypesString.NODE_MODE_RELAY; static "@on_muted_inputs" = { type: "combo", values: ["MUTE", "ACTIVE", "BYPASS", "NOTHING"], }; static "@on_bypassed_inputs" = { type: "combo", values: ["BYPASS", "ACTIVE", "MUTE", "NOTHING"], }; static "@on_any_active_inputs" = { type: "combo", values: ["BYPASS", "ACTIVE", "MUTE", "NOTHING"], }; constructor(title?: string) { super(title); this.properties["on_muted_inputs"] = "MUTE"; this.properties["on_bypassed_inputs"] = "BYPASS"; this.properties["on_any_active_inputs"] = "ACTIVE"; this.onConstructed(); } override onConstructed() { this.addOutput("REPEATER", "_NODE_REPEATER_", { color_on: "#Fc0", color_off: "#a80", shape: LiteGraph.ARROW_SHAPE, }); setTimeout(() => { this.stabilize(); }, 500); return super.onConstructed(); } override onModeChange(from: NodeMode, to: NodeMode) { super.onModeChange(from, to); // If we aren't connected to anything, then we'll use our mode to relay when it changes. if (this.inputs.length <= 1 && !this.isInputConnected(0) && this.isAnyOutputConnected()) { const [n, v] = logger.infoParts(`Mode change without any inputs; relaying our mode.`); console[n]?.(...v); // Pass "to" since there may be other getters in the way to access this.mode directly. this.dispatchModeToRepeater(to); } } override configure(info: SerializedLGraphNode<LGraphNode>): void { // Patch a small issue (~14h) where multiple OPT_CONNECTIONS may have been created. // https://github.com/rgthree/rgthree-comfy/issues/206 // TODO: This can probably be removed within a few weeks. if (info.outputs?.length) { info.outputs.length = 1; } super.configure(info); } override onDrawForeground(ctx: CanvasRenderingContext2D, canvas: LGraphCanvas): void { if (this.flags?.collapsed) { return; } if ( this.properties["on_muted_inputs"] !== "MUTE" || this.properties["on_bypassed_inputs"] !== "BYPASS" || this.properties["on_any_active_inputs"] != "ACTIVE" ) { let margin = 15; ctx.textAlign = "left"; let label = `*(MUTE > ${this.properties["on_muted_inputs"]}, `; label += `BYPASS > ${this.properties["on_bypassed_inputs"]}, `; label += `ACTIVE > ${this.properties["on_any_active_inputs"]})`; ctx.fillStyle = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; const oldFont = ctx.font; ctx.font = "italic " + (LiteGraph.NODE_SUBTEXT_SIZE - 2) + "px Arial"; ctx.fillText(fitString(ctx, label, this.size[0] - 20), 15, this.size[1] - 6); ctx.font = oldFont; } } override computeSize(out: Vector2) { let size = super.computeSize(out); if ( this.properties["on_muted_inputs"] !== "MUTE" || this.properties["on_bypassed_inputs"] !== "BYPASS" || this.properties["on_any_active_inputs"] != "ACTIVE" ) { size[1] += 17; } return size; } override onConnectOutput( outputIndex: number, inputType: string | -1, inputSlot: INodeInputSlot, inputNode: LGraphNode, inputIndex: number, ): boolean { let canConnect = super.onConnectOutput?.( outputIndex, inputType, inputSlot, inputNode, inputIndex, ); let nextNode = getConnectedOutputNodesAndFilterPassThroughs(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 = getConnectedInputNodesAndFilterPassThroughs( this, this, -1, this.inputsPassThroughFollowing, ); let mode: NodeMode | -99 | 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; } } this.dispatchModeToRepeater(mode); setTimeout(() => { this.stabilize(); }, 500); } /** * Sends the mode to the repeater, checking to see if we're modifying our mode. */ private dispatchModeToRepeater(mode?: NodeMode | -99 | null) { if (mode != null) { const propertyVal = this.properties?.[MODE_TO_PROPERTY.get(mode) || ""]; const newMode = OPTION_TO_MODE.get(propertyVal); mode = (newMode !== null ? newMode : mode) as NodeMode | -99; if (mode !== null && mode !== MODE_NOTHING) { if (this.outputs?.length) { const outputNodes = getConnectedOutputNodesAndFilterPassThroughs(this); for (const outputNode of outputNodes) { outputNode.mode = mode; wait(16).then(() => { outputNode.setDirtyCanvas(true, true); }); } } } } } override getHelp() { return ` <p> 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). </p> <ul> <li><p> When all connected input nodes are muted, the relay will set a connected repeater to mute (by default). </p></li> <li><p> When all connected input nodes are bypassed, the relay will set a connected repeater to bypass (by default). </p></li> <li><p> When any connected input nodes are active, the relay will set a connected repeater to active (by default). </p></li> <li><p> If no inputs are connected, the relay will set a connected repeater to its mode <i>when its own mode is changed</i>. <b>Note</b>, if any inputs are connected, then the above will occur and the Relay's mode does not matter. </p></li> </ul> <p> Note, you can change which signals get sent on the above in the <code>Properties</code>. For instance, you could configure an inverse relay which will send a MUTE when any of its inputs are active (instead of sending an ACTIVE signal), and send an ACTIVE signal when all of its inputs are muted (instead of sending a MUTE signal), etc. </p> `; } } app.registerExtension({ name: "rgthree.NodeModeRepeaterHelper", registerCustomNodes() { addConnectionLayoutSupport(NodeModeRelay, app, [ ["Left", "Right"], ["Right", "Left"], ]); LiteGraph.registerNodeType(NodeModeRelay.type, NodeModeRelay); NodeModeRelay.category = NodeModeRelay._category; }, });