commit 58d6e1e34f21730b30f55c90ad310e0b36b15098 Author: solidlime Date: Sun Aug 2 12:09:18 2026 +0900 Add Multi Bool Panel custom node diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d8a4716 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 solidlime + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d1dd6de --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# Multi Bool Panel + +## 概要 + +1ノードで複数のbooleanトグルを管理できるComfyUIカスタムノードです。 +最大20個の名前付きトグルを持ち、各トグルは独立したBOOLEAN出力ソケットを持ちます。 +`slot_count` でトグル数を1〜20の範囲で調整でき、`all_switch` で全トグルをONにできます。 + +## インストール + +**ComfyUI Manager の場合**: カスタムノードマネージャーで「Multi Bool Panel」を検索してインストール(ComfyUI-Managerレジストリ登録後)。 + +**git clone の場合**: +1. `ComfyUI/custom_nodes/` で `git clone https://github.com/solidlime/ComfyUI-MultiBoolPanel` を実行 +2. ComfyUIを再起動します +3. ノード追加メニューで「Multi Bool Panel」を検索して追加します(カテゴリ: `Logic`) + +**手動コピーの場合**: このフォルダ(`MultiBoolPanel`)を `ComfyUI/custom_nodes/` にコピー → ComfyUI再起動 →「Multi Bool Panel」を検索して追加 + +## 使い方(基本) + +- トグルスイッチのON/OFFがそのまま `Switch 1`〜`Switch 20` のBOOLEAN出力に出ます +- `slot_count` を超える番号の出力は常に `False` です +- `Labels` 出力には、ONかつラベル名が設定されているトグル名がカンマ区切りで出力されます +- トグル名は `label_N` で変更できます + +## 使い方(一括操作) + +`name_filter` にノード名(タイトルまたはクラス名)の部分文字列を指定します。 +`broadcast_mode` で適用方法を選択します: + +- `widgets`: 対象ノードのbooleanウィジェット(チェックボックス)全てをトグル値に書き換えます +- `bypass`: 対象ノードのbypass状態を切り替えます +- `mute`: 対象ノードのmute状態を切り替えます +- `off`: 一括操作しません(通常のトグルパネルとして使用) + +トグル番号Nは「フィルタに一致するN番目のノード(タイトル順ソート)」に対応します。 +`all_switch` がONの場合は全対象ノードがONになります。 + +適用タイミング: +- ノード上の「Apply to matching nodes」ボタン(手動) +- `auto_apply` がONの場合は、キュー実行時に自動適用されます + +## 制限・注意 + +- トグルは最大20個固定です +- `widgets` モードは対象ノードのBOOLEAN型ウィジェットのみ書き換えます(入力ソケット経由の接続には影響しません) +- 名前フィルタは部分一致で、タイトルとクラス名の両方を確認します diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..2dcfdb2 --- /dev/null +++ b/__init__.py @@ -0,0 +1,5 @@ +from .multi_bool_panel import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +WEB_DIRECTORY = "./web" +__version__ = "1.0.0" +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] diff --git a/multi_bool_panel.py b/multi_bool_panel.py new file mode 100644 index 0000000..120bb33 --- /dev/null +++ b/multi_bool_panel.py @@ -0,0 +1,49 @@ +""" +Multi Bool Panel — 1ノードに複数の名前付きbooleanトグルを持つComfyUIカスタムノード。 +各トグルは独立したBOOLEAN出力ソケットを持つ。slot_countでトグル数を調整できる。 +名前フィルタによる一括操作(他ノードのbooleanウィジェット / bypass / mute)は +フロントエンド拡張 (web/js/multi_bool_panel.js) が担当する。 +""" + +MAX_SLOTS = 20 + + +class SolidlimeMultiBoolPanel: + @classmethod + def INPUT_TYPES(cls): + required = { + "slot_count": ("INT", {"default": 4, "min": 1, "max": MAX_SLOTS, "step": 1}), + "name_filter": ("STRING", {"default": "", "multiline": False}), + "broadcast_mode": (["off", "widgets", "bypass", "mute"], {"default": "off"}), + "auto_apply": ("BOOLEAN", {"default": False}), + "all_switch": ("BOOLEAN", {"default": False}), + } + for i in range(1, MAX_SLOTS + 1): + required[f"label_{i}"] = ("STRING", {"default": "", "multiline": False}) + required[f"switch_{i}"] = ("BOOLEAN", {"default": False}) + return {"required": required} + + RETURN_TYPES = ("STRING",) + ("BOOLEAN",) * MAX_SLOTS + RETURN_NAMES = ("Labels",) + tuple(f"Switch {i}" for i in range(1, MAX_SLOTS + 1)) + FUNCTION = "execute" + CATEGORY = "Logic" + + def execute(self, **kwargs): + slot_count = max(1, min(MAX_SLOTS, int(kwargs.get("slot_count", 4)))) + all_switch = bool(kwargs.get("all_switch", False)) + labels_out = [] + out = [] + for i in range(1, MAX_SLOTS + 1): + if i > slot_count: + out.append(False) + continue + label = str(kwargs.get(f"label_{i}", "") or "") + val = bool(kwargs.get(f"switch_{i}", False)) or all_switch + out.append(val) + if val and label: + labels_out.append(label) + return (", ".join(labels_out),) + tuple(out) + + +NODE_CLASS_MAPPINGS = {"SolidlimeMultiBoolPanel": SolidlimeMultiBoolPanel} +NODE_DISPLAY_NAME_MAPPINGS = {"SolidlimeMultiBoolPanel": "Multi Bool Panel"} diff --git a/web/js/multi_bool_panel.js b/web/js/multi_bool_panel.js new file mode 100644 index 0000000..0de04e4 --- /dev/null +++ b/web/js/multi_bool_panel.js @@ -0,0 +1,55 @@ +import { app } from "../../scripts/app.js"; + +const PANEL_TYPE = "SolidlimeMultiBoolPanel"; + +function widget(node, name) { + return (node.widgets || []).find((w) => w.name === name); +} + +function applyBroadcast(node) { + const filter = (widget(node, "name_filter")?.value || "").trim(); + const mode = widget(node, "broadcast_mode")?.value; + if (!filter || !mode || mode === "off") return; + + const slotCount = Math.max(1, parseInt(widget(node, "slot_count")?.value ?? "4", 10) || 1); + const targets = (app.graph._nodes || []) + .filter((n) => n.id !== node.id && ( + (n.title && n.title.includes(filter)) || + (n.type && n.type.includes(filter)) + )) + .sort((a, b) => (a.title || a.type).localeCompare(b.title || b.type, "ja")); + + targets.forEach((t, idx) => { + const on = idx < slotCount ? !!widget(node, `switch_${idx + 1}`)?.value : false; + if (mode === "bypass") { + t.bypass = on; + } else if (mode === "mute") { + t.mute = on; + } else if (mode === "widgets") { + (t.widgets || []).forEach((wid) => { + if (wid.type === "boolean" || (wid.options && wid.options.type === "boolean")) { + wid.value = on; + } + }); + } + }); + app.graph.setDirtyCanvas(true, true); +} + +app.registerExtension({ + name: "MultiBoolPanel", + async setup() { + const origQueue = app.queuePrompt.bind(app); + app.queuePrompt = function (number, detail) { + (app.graph._nodes || []).forEach((n) => { + if (n.type === PANEL_TYPE && widget(n, "auto_apply")?.value) applyBroadcast(n); + }); + return origQueue(number, detail); + }; + }, + async nodeCreated(node) { + if (node.type === PANEL_TYPE || node.constructor.type === PANEL_TYPE) { + node.addWidget("button", "Apply to matching nodes", null, () => applyBroadcast(node)); + } + }, +});