diff --git a/Rayko_Any_Switch.py b/Rayko_Any_Switch.py new file mode 100644 index 0000000..82aa01f --- /dev/null +++ b/Rayko_Any_Switch.py @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2025-2026 Raykosan (RaykoStudio) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +from typing import Any, Dict, Tuple + +class RSAnySwitch: + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + inputs = { + "required": {}, + "optional": { + "active_input": ("STRING", {"default": "input_1", "hidden": True}), + } + } + for i in range(1, 21): + inputs["optional"][f"input_{i}"] = ("*",) + return inputs + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("output",) + FUNCTION = "switch_input" + CATEGORY = "🦊 RaykoStudio" + + def switch_input(self, active_input: str, **kwargs) -> Tuple[Any]: + if active_input in kwargs and kwargs[active_input] is not None: + return (kwargs[active_input],) + for i in range(1, 21): + key = f"input_{i}" + if key in kwargs and kwargs[key] is not None: + return (kwargs[key],) + return (None,) + +NODE_CLASS_MAPPINGS = { + "RSAnySwitch": RSAnySwitch, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "RSAnySwitch": "🦊 RS Any Switch", +} \ No newline at end of file diff --git a/__init__.py b/__init__.py index 75e74d3..d618da6 100644 --- a/__init__.py +++ b/__init__.py @@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'} REQUIRED_MODULES: Set[str] = set() PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio" -__version__ = "0.22.0" +__version__ = "0.23.0" def load_modules(): print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}") diff --git a/package.json b/package.json index f2f7266..3f57366 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ComfyUI_RaykoStudio", - "version": "0.22.0", + "version": "0.23.0", "description": "Set of custom nodes for ComfyUI providing additional image processing capabilities", "author": "Raykosan", "license": "Apache License 2.0", diff --git a/pyproject.toml b/pyproject.toml index de206c5..40565af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ComfyUI_RaykoStudio" description = "Set of custom nodes for ComfyUI providing additional image processing capabilities" -version = "0.22.0" +version = "0.23.0" license = { file = "LICENSE" } dependencies = ["torch>=1.7.0", "numpy>=1.19.0", "Pillow>=8.0.0", "freetype-py>=2.5.1"] diff --git a/web/rs_any_switch.js b/web/rs_any_switch.js new file mode 100644 index 0000000..4e40aab --- /dev/null +++ b/web/rs_any_switch.js @@ -0,0 +1,310 @@ +import { app } from "../../../scripts/app.js"; + +app.registerExtension({ + name: "RaykoStudio.AnySwitch", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "RSAnySwitch") { + const rebuildFromConnections = function() { + const activeWidget = this.widgets.find(w => w.name === "active_input"); + const infoWidget = this.widgets.find(w => w.name === "info_display"); + if (!activeWidget) return; + + let maxConnectedIndex = 1; + const connectedInputs = []; + for (let i = 0; i < this.inputs.length; i++) { + const input = this.inputs[i]; + if (input.link !== null) { + const match = input.name.match(/input_(\d+)/); + if (match) { + const idx = parseInt(match[1], 10); + if (idx > maxConnectedIndex) maxConnectedIndex = idx; + connectedInputs.push({ index: i, name: input.name, idx }); + } + } + } + + for (let idx = 1; idx <= maxConnectedIndex; idx++) { + const existing = this.inputs.find(inp => inp.name === `input_${idx}`); + if (!existing) { + this.addInput(`input_${idx}`, "*"); + } + } + + for (let i = this.inputs.length - 1; i >= 0; i--) { + const match = this.inputs[i].name.match(/input_(\d+)/); + if (match && parseInt(match[1], 10) > maxConnectedIndex + 1) { + this.removeInput(i); + } + } + + const currentSlotWidgets = this.widgets.filter(w => w.slotName && w.slotName.startsWith("input_")); + for (let conn of connectedInputs) { + let widget = currentSlotWidgets.find(w => w.slotName === conn.name); + if (!widget) { + const targetNodeName = this.getConnectedNodeName(conn.index); + const prefix = `Input ${conn.idx}: `; + let displayName = prefix + targetNodeName; + if (displayName.length > 25) displayName = prefix + targetNodeName.substring(0, 22) + "..."; + + widget = this.addWidget("toggle", displayName, false, (value) => { + if (value) { + if (activeWidget) activeWidget.value = widget.slotName; + if (infoWidget) infoWidget.value = widget.slotName.replace("input_", "INPUT ").toUpperCase(); + for (let w of this.widgets) { + if (w !== widget && w.slotName && w.slotName.startsWith("input_")) { + w.value = false; + } + } + } else { + if (activeWidget) activeWidget.value = "none"; + if (infoWidget) infoWidget.value = "OFF"; + } + this.setDirtyCanvas(true, true); + }, {}); + widget.slotName = conn.name; + widget.draw = function(ctx, node, w, y, h) { + ctx.fillStyle = "#222"; + ctx.fillRect(0, y, w, h); + ctx.fillStyle = "#aaa"; + ctx.font = "14px Arial"; + ctx.textAlign = "left"; + ctx.fillText(this.name, 10, y + h / 2 + 1); + const toggleW = 30, toggleH = 14; + const toggleX = w - toggleW - 10; + const toggleY = y + (h - toggleH) / 2; + ctx.fillStyle = this.value ? "#4a4" : "#666"; + ctx.beginPath(); + ctx.arc(toggleX + toggleH / 2, toggleY + toggleH / 2, toggleH / 2, 0, Math.PI * 2); + ctx.fill(); + ctx.fillRect(toggleX + toggleH / 2, toggleY, toggleW - toggleH, toggleH); + ctx.beginPath(); + ctx.arc(toggleX + toggleW - toggleH / 2, toggleY + toggleH / 2, toggleH / 2, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = "#fff"; + ctx.beginPath(); + ctx.arc(this.value ? toggleX + toggleW - toggleH / 2 : toggleX + toggleH / 2, toggleY + toggleH / 2, toggleH / 2 - 2, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = this.value ? "#4a4" : "#a44"; + ctx.font = "bold 12px Arial"; + ctx.textAlign = "right"; + ctx.fillText(this.value ? "ON" : "OFF", toggleX - 5, y + h / 2 + 1); + }; + + const targetPos = conn.idx + 1; + const currentPos = this.widgets.indexOf(widget); + if (currentPos !== targetPos) { + this.widgets.splice(currentPos, 1); + this.widgets.splice(targetPos, 0, widget); + } + } else { + const targetNodeName = this.getConnectedNodeName(conn.index); + const prefix = `Input ${conn.idx}: `; + let displayName = prefix + targetNodeName; + if (displayName.length > 25) displayName = prefix + targetNodeName.substring(0, 22) + "..."; + widget.name = displayName; + } + widget.value = (activeWidget.value === conn.name); + } + + for (let w of currentSlotWidgets) { + if (!connectedInputs.some(conn => conn.name === w.slotName)) { + this.removeWidget(w); + } + } + + if (infoWidget) { + const activeName = activeWidget.value; + infoWidget.value = (!activeName || activeName === "none") ? "OFF" : activeName.replace("input_", "INPUT ").toUpperCase(); + } + + this.setSize(this.computeSize()); + this.size[0] += 50; + this.setSize(this.size); + this.setDirtyCanvas(true, true); + }; + + const getConnectedNodeName = function(inputIndex) { + const link = this.inputs[inputIndex]?.link; + if (!link) return "Unknown"; + const linkInfo = app.graph.links[link]; + if (!linkInfo) return "Unknown"; + const originNode = app.graph.getNodeById(linkInfo.origin_id); + return originNode ? (originNode.getTitle() || originNode.type) : "Unknown"; + }; + nodeType.prototype.getConnectedNodeName = getConnectedNodeName; + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function() { + if (onNodeCreated) onNodeCreated.apply(this, arguments); + + const activeWidget = this.widgets.find(w => w.name === "active_input"); + if (activeWidget) { + activeWidget.hidden = true; + activeWidget.computeSize = () => [0, 0]; + activeWidget.draw = () => {}; + } + + while (this.inputs.length > 1) this.removeInput(this.inputs.length - 1); + if (this.inputs.length === 0 || this.inputs[0].name !== "input_1") { + this.addInput("input_1", "*"); + } + + const infoWidget = this.addWidget("text", "info_display", "OFF", () => {}, { readOnly: true }); + infoWidget.computeSize = function(width) { return [width, 24]; }; + infoWidget.draw = function(ctx, node, w, y, h) { + ctx.strokeStyle = this.value === "OFF" ? "#f44336" : "#4caf50"; + ctx.lineWidth = 2; + ctx.beginPath(); + if (ctx.roundRect) ctx.roundRect(1, y + 1, w - 2, h - 2, 6); + else ctx.rect(1, y + 1, w - 2, h - 2); + ctx.stroke(); + ctx.fillStyle = "#fff"; + ctx.font = "bold 14px Arial"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(this.value, w / 2, y + h / 2); + }; + + if (activeWidget && activeWidget.value !== "none") { + infoWidget.value = activeWidget.value.replace("input_", "INPUT ").toUpperCase(); + } + + this.setSize(this.computeSize()); + this.size[0] += 50; + this.setSize(this.size); + this.setDirtyCanvas(true, true); + }; + + const onConnectionsChange = nodeType.prototype.onConnectionsChange; + nodeType.prototype.onConnectionsChange = function(type, index, connected, link_info) { + if (onConnectionsChange) onConnectionsChange.apply(this, arguments); + if (type !== 1) return; + + const activeWidget = this.widgets.find(w => w.name === "active_input"); + const infoWidget = this.widgets.find(w => w.name === "info_display"); + const inputName = this.inputs[index].name; + const inputIndex = index + 1; + + if (connected) { + let targetNodeName = "Unknown"; + if (link_info && link_info.origin_id) { + const originNode = app.graph.getNodeById(link_info.origin_id); + if (originNode) targetNodeName = originNode.getTitle() || originNode.type; + } + const prefix = `Input ${inputIndex}: `; + let displayName = prefix + targetNodeName; + if (displayName.length > 25) displayName = prefix + targetNodeName.substring(0, 22) + "..."; + + let widget = this.widgets.find(w => w.slotName === inputName); + if (!widget) { + const connectedCount = this.inputs.filter(inp => inp.link !== null).length; + const isFirstSlot = connectedCount === 1; + widget = this.addWidget("toggle", displayName, isFirstSlot, (value) => { + if (value) { + if (activeWidget) activeWidget.value = widget.slotName; + if (infoWidget) infoWidget.value = widget.slotName.replace("input_", "INPUT ").toUpperCase(); + for (let w of this.widgets) { + if (w !== widget && w.slotName && w.slotName.startsWith("input_")) { + w.value = false; + } + } + } else { + if (activeWidget) activeWidget.value = "none"; + if (infoWidget) infoWidget.value = "OFF"; + } + this.setDirtyCanvas(true, true); + }, {}); + widget.slotName = inputName; + widget.draw = function(ctx, node, w, y, h) { + ctx.fillStyle = "#222"; + ctx.fillRect(0, y, w, h); + ctx.fillStyle = "#aaa"; + ctx.font = "14px Arial"; + ctx.textAlign = "left"; + ctx.fillText(this.name, 10, y + h / 2 + 1); + const toggleW = 30, toggleH = 14; + const toggleX = w - toggleW - 10; + const toggleY = y + (h - toggleH) / 2; + ctx.fillStyle = this.value ? "#4a4" : "#666"; + ctx.beginPath(); + ctx.arc(toggleX + toggleH / 2, toggleY + toggleH / 2, toggleH / 2, 0, Math.PI * 2); + ctx.fill(); + ctx.fillRect(toggleX + toggleH / 2, toggleY, toggleW - toggleH, toggleH); + ctx.beginPath(); + ctx.arc(toggleX + toggleW - toggleH / 2, toggleY + toggleH / 2, toggleH / 2, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = "#fff"; + ctx.beginPath(); + ctx.arc(this.value ? toggleX + toggleW - toggleH / 2 : toggleX + toggleH / 2, toggleY + toggleH / 2, toggleH / 2 - 2, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = this.value ? "#4a4" : "#a44"; + ctx.font = "bold 12px Arial"; + ctx.textAlign = "right"; + ctx.fillText(this.value ? "ON" : "OFF", toggleX - 5, y + h / 2 + 1); + }; + + const targetPos = inputIndex + 1; + const currentPos = this.widgets.indexOf(widget); + if (currentPos !== targetPos) { + this.widgets.splice(currentPos, 1); + this.widgets.splice(targetPos, 0, widget); + } + + if (isFirstSlot && activeWidget) { + activeWidget.value = inputName; + if (infoWidget) infoWidget.value = inputName.replace("input_", "INPUT ").toUpperCase(); + } + } else { + widget.name = displayName; + } + + if (index === this.inputs.length - 1 && this.inputs.length < 20) { + this.addInput(`input_${this.inputs.length + 1}`, "*"); + } + } else { + const widget = this.widgets.find(w => w.slotName === inputName); + if (widget) { + const wasActive = widget.value; + this.removeWidget(widget); + if (wasActive && activeWidget) { + activeWidget.value = "none"; + if (infoWidget) infoWidget.value = "OFF"; + } + } + + if (this.inputs.length > 1) { + const hasConnected = this.inputs.some(inp => inp.link !== null); + if (!hasConnected) { + while (this.inputs.length > 1) { + this.removeInput(this.inputs.length - 1); + } + } else { + let lastConnectedIndex = -1; + for (let i = 0; i < this.inputs.length; i++) { + if (this.inputs[i].link !== null) { + lastConnectedIndex = i; + } + } + if (lastConnectedIndex < this.inputs.length - 2) { + while (this.inputs.length > lastConnectedIndex + 2) { + this.removeInput(this.inputs.length - 1); + } + } + } + } + } + + this.setSize(this.computeSize()); + this.size[0] += 50; + this.setSize(this.size); + this.setDirtyCanvas(true, true); + }; + + const onConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function(info) { + if (onConfigure) onConfigure.apply(this, arguments); + rebuildFromConnections.call(this); + }; + } + } +}); \ No newline at end of file