// --- Loading js/active_model.js --- v2-dynamic-inputs import { app } from "/scripts/app.js"; // Виправлено шлях на абсолютний // Цільовий тип вузла, який ми шукаємо const TARGET_NODE_TYPE = "ModelParamsPipe"; // Режими роботи вузлів ComfyUI/LiteGraph const MODE_ALWAYS = 0; // Завжди активний const MODE_BYPASS = 4; // Режим обходу (Bypass) const NODE_TITLE = "Active Model"; // Змінено заголовок const NODE_TYPE = "ActiveModel"; // І тип class ActiveModel { // Налаштування для LiteGraph constructor() { // Цей вузол є переважно контейнером для дій this.properties = {}; // Спробуємо знайти та вивести вузли одразу при створенні this.logTargetNodeNames(); } // Статичні властивості для визначення типу вузла static title = "Active Model"; static type = "ActiveModel"; // Унікальний тип для цього вузла // Функція для пошуку та логування імен цільових вузлів logTargetNodeNames() { const nodes = app.graph._nodes.filter(node => node.type === TARGET_NODE_TYPE); return nodes; // Повертаємо для можливого використання в onAction } // Метод для встановлення режиму для всіх знайдених цільових вузлів setHubsMode(mode) { const nodes = this.logTargetNodeNames(); if (nodes.length === 0) { return; // Виходимо, якщо немає цільових вузлів } const modeStr = mode === MODE_ALWAYS ? "ALWAYS" : (mode === MODE_BYPASS ? "BYPASS" : mode); let changed = false; for (const node of nodes) { if (node.mode !== mode) { node.mode = mode; changed = true; } } if (changed) { app.graph.setDirtyCanvas(true, true); } } // Спрощена обробка дій для тестування onAction(action) { // Просто логуємо знайдені вузли ще раз при виклику дії this.logTargetNodeNames(); } // Цей вузол не виконує обчислень, тому onExecute може бути порожнім onExecute() { // console.log(`[${ActiveModel.title}] Executed (likely no-op).`); } // Статичний метод для налаштування та реєстрації вузла static setUp(clazz) { // Явно встановлюємо статичні властивості, які потрібні LiteGraph clazz.title = ActiveModel.title; clazz.type = ActiveModel.type; clazz.title_mode = LiteGraph.NORMAL_TITLE; clazz.collapsable = true; clazz.size = [240, 50]; clazz.category = "MultiModel"; // Змінюємо категорію на основну try { // Реєструємо сам клас LiteGraph.registerNodeType(clazz.type, clazz); } catch (e) { console.error(`[${ActiveModel.title}] Node type registration via setUp FAILED:`, e); } } } // Визначаємо дії, доступні в контекстному меню ActiveModel.exposedActions = ["Log Hub Names", "Enable All Hubs", "Bypass All Hubs", "Toggle All Hubs"]; // Визначаємо статичні властивості title та type тут, щоб вони були доступні для setUp ActiveModel.title = "Active Model"; ActiveModel.type = "ActiveModel"; // --- Допоміжні функції --- // // Функція пошуку цільових вузлів (тепер отримує їх з сервісу) function findTargetNodes() { return HubNodesService.getHubNodes(); } // Функція зміни режиму ОДНОГО вузла function setHubMode(targetNode, mode) { if (!targetNode) return; const currentMode = targetNode.mode; const modeStr = mode === MODE_ALWAYS ? "ON" : "OFF"; // Якщо ми вмикаємо вузол (MODE_ALWAYS), і при цьому поточний режим НЕ MODE_ALWAYS // (тобто вузол був вимкнений, а тепер ми його вмикаємо) if (mode === MODE_ALWAYS && currentMode !== MODE_ALWAYS) { // Потрібно перевірити, чи є інші активні вузли, і вимкнути їх const allHubNodes = HubNodesService.getHubNodes(); const otherActiveNodes = allHubNodes.filter(n => n.id !== targetNode.id && n.mode === MODE_ALWAYS); if (otherActiveNodes.length > 0) { otherActiveNodes.forEach(node => { node.mode = MODE_BYPASS; }); } } // Змінюємо режим цільового вузла, незалежно від того, чи є інші активні вузли if (currentMode !== mode) { targetNode.mode = mode; app.graph.setDirtyCanvas(true, true); } } // Нова функція для оновлення віджетів у всіх байпасерах для конкретного вузла function updateAllBypasserWidgets(targetNode) { if (!targetNode) return; const bypassers = HubNodesService.bypasserNodes; bypassers.forEach(bypasser => { if (!bypasser || !bypasser.widgets) return; const widgetName = `hub_${targetNode.id}`; const widget = bypasser.widgets.find(w => w.name === widgetName); if (widget) { // Оновлюємо значення віджета const isBypassed = targetNode.mode === MODE_BYPASS; const newValue = !isBypassed; // Оновлюємо також заголовок, щоб він відповідав новому формату const newLabel = `${targetNode.title || '(Untitled)'}`; let changed = false; if (widget.value !== newValue) { widget.value = newValue; changed = true; } if (widget.label !== newLabel) { widget.label = newLabel; changed = true; } if (changed) { bypasser.graph.setDirtyCanvas(true, false); } } }); } // --- Сервіс для керування вузлами --- // const HubNodesService = { hubNodes: [], bypasserNodes: [], updateTimeout: null, updateDelay: 100, app: null, // Додано для доступу до app registerBypasserNode(node) { if (!this.bypasserNodes.find(n => n.id === node.id)) { this.bypasserNodes.push(node); this.scheduleUpdate(`registerBypasser_${node.id}`); } }, unregisterBypasserNode(node) { const index = this.bypasserNodes.findIndex(n => n.id === node.id); if (index !== -1) { this.bypasserNodes.splice(index, 1); // Оновлення не потрібне при видаленні самого байпасера } else { console.log(`[HubNodesService] Node ID ${node.id} not found for unregistration.`); } }, getHubNodes() { return [...this.hubNodes]; }, // --- Нові методи для сповіщень від Hub --- // notifyHubAdded(node) { if (!node) return; if (!this.hubNodes.find(n => n.id === node.id)) { this.hubNodes.push(node); this.hubNodes.sort((a, b) => a.id - b.id); // Підтримуємо сортування this.scheduleUpdate(`notifyHubAdded_${node.id}`); } else { console.log(`[HubNodesService] Hub node ID ${node.id} already exists.`); } }, notifyHubRemoved(node) { if (!node) return; const index = this.hubNodes.findIndex(n => n.id === node.id); if (index !== -1) { this.hubNodes.splice(index, 1); this.scheduleUpdate(`notifyHubRemoved_${node.id}`); } else { console.log(`[HubNodesService] Hub node ID ${node.id} not found for removal notification.`); } }, notifyHubRenamed(node) { if (!node) return; this.scheduleUpdate(`notifyHubRenamed_${node.id}`); }, // --- Кінець нових методів --- // scheduleUpdate(reason = "unknown") { if (this.updateTimeout) { clearTimeout(this.updateTimeout); } this.updateTimeout = setTimeout(() => { this.updateTimeout = null; this.bypasserNodes.forEach(node => { if (node.refreshWidgets) { node.refreshWidgets(); } else { console.warn(`[HubNodesService] Bypasser node ID: ${node.id} missing refreshWidgets method.`); } }); }, this.updateDelay); } }; app.registerExtension({ name: "MultiModel.ActiveModel.ServiceDriven", // Оновлено назву розширення setup(appInstance) { // Реєструємо сервіс, щоб він був доступний іншим розширенням appInstance.HubNodesService = HubNodesService; HubNodesService.app = appInstance; // Додаємо перехоплення onNodeAdded для відстеження додавання нашої ноди if (appInstance.graph && !appInstance.graph._activemodel_onNodeAdded_attached) { const origNodeAdded = appInstance.graph.onNodeAdded; appInstance.graph.onNodeAdded = function(node) { // Спочатку викликаємо оригінальний обробник if (origNodeAdded) { origNodeAdded.call(this, node); } // Перевіряємо, чи це наша нода ActiveModel if (node.type === NODE_TYPE) { // Викликаємо обробку ноди для додавання віджетів const extension = appInstance.extensions.find(ext => ext.name === "MultiModel.ActiveModel.ServiceDriven"); if (extension) { // Конфігуруємо ноду extension.loadedGraphNode(node, appInstance); // Оновлюємо віджети з невеликою затримкою setTimeout(() => { if (node.refreshWidgets) { node.refreshWidgets(); } }, 100); } } }; appInstance.graph._activemodel_onNodeAdded_attached = true; } // Початкове сканування потрібне, щоб знайти вузли, які вже існують при завантаженні setTimeout(() => { // Знаходимо існуючі Hub та Bypasser ноди і реєструємо їх let initialHubsFound = 0; let initialBypassersFound = 0; appInstance.graph._nodes.forEach(node => { if (node.type === TARGET_NODE_TYPE) { HubNodesService.notifyHubAdded(node); initialHubsFound++; } else if (node.type === NODE_TYPE) { // Реєстрація bypasser'а відбувається також у loadedGraphNode, // але продублюємо тут на випадок проблем з порядком HubNodesService.registerBypasserNode(node); initialBypassersFound++; // Додаємо додаткову ініціалізацію для вже існуючих Bypasser нод this.loadedGraphNode(node, appInstance); } }); // Додаємо окремий виклик для оновлення віджетів для всіх bypassers з більшою затримкою if (initialBypassersFound > 0) { setTimeout(() => { HubNodesService.bypasserNodes.forEach(node => { if (node && node.refreshWidgets) { node.refreshWidgets(); } }); }, 500); } }, 500); }, // loadedGraphNode тепер відповідає ТІЛЬКИ за конфігурацію САМОГО ActiveModel loadedGraphNode(node, appInstance) { if (node.type === NODE_TYPE) { node.app = appInstance; node.IS_BYPASSER_NODE = true; delete node.onAction; // Видаляємо, бо додамо getActions // Визначаємо refreshWidgets node.refreshWidgets = function() { const targetNodes = findTargetNodes(); // Отримуємо з сервісу // Додаємо детальну діагностику if (targetNodes.length === 0) { console.log(`[${NODE_TITLE} ID: ${this.id}] No target nodes found. HubNodesService state:`, {hubNodesCount: HubNodesService.hubNodes.length, bypassersCount: HubNodesService.bypasserNodes.length}); } else { console.log(`[${NODE_TITLE} ID: ${this.id}] Target nodes details:`, targetNodes.map(n => ({id: n.id, title: n.title, mode: n.mode}))); } // --- Подальша логіка залишається ТАКОЮ Ж САМОЮ, як і раніше --- // (Порівняння віджетів, додавання, видалення, оновлення) const currentWidgets = this.widgets ? [...this.widgets] : []; let widgetsChanged = false; const activeWidgetNames = new Set(); // Add/Update widgets targetNodes.forEach(targetNode => { if (!targetNode) { return; // Пропускаємо невалідні ноди } const widgetName = `hub_${targetNode.id}`; // Прибираємо ID ноди з заголовка const widgetLabel = `${targetNode.title || '(Untitled)'}`; const isBypassed = targetNode.mode === MODE_BYPASS; const widgetValue = !isBypassed; activeWidgetNames.add(widgetName); let existingWidget = this.widgets?.find(w => w.name === widgetName); if (existingWidget) { let labelChanged = false; if (existingWidget.label !== widgetLabel) { existingWidget.label = widgetLabel; labelChanged = true; } if (existingWidget.value !== widgetValue) { existingWidget.value = widgetValue; widgetsChanged = true; } if (labelChanged) widgetsChanged = true; if (!existingWidget.callback) { existingWidget.callback = (value) => { // Нова логіка: якщо перемикач вмикається, вимикаємо всі інші // якщо вимикається - просто вимикаємо const nodeToChange = app.graph.getNodeById(targetNode.id); if (nodeToChange) setHubMode(nodeToChange, value ? MODE_ALWAYS : MODE_BYPASS); }; widgetsChanged = true; } } else { this.addWidget( "toggle", widgetName, widgetValue, (value) => { // Нова логіка: дозволяємо будь-які переключення const nodeToChange = app.graph.getNodeById(targetNode.id); if (nodeToChange) setHubMode(nodeToChange, value ? MODE_ALWAYS : MODE_BYPASS); else this.refreshWidgets(); }, { on: "ON", off: "OFF" } ); this.widgets[this.widgets.length - 1].label = widgetLabel; widgetsChanged = true; } }); // Remove outdated widgets let removedWidgets = false; if (this.widgets) { for (let i = this.widgets.length - 1; i >= 0; i--) { const widget = this.widgets[i]; if (widget.name?.startsWith("hub_") && !activeWidgetNames.has(widget.name)) { this.widgets.splice(i, 1); widgetsChanged = true; removedWidgets = true; } } } // Resize node only if widgets were added/removed if (widgetsChanged) { // ... (логіка зміни розміру залишається такою ж) ... const baseHeight = LiteGraph.NODE_TITLE_HEIGHT + 5; const widgetHeight = LiteGraph.NODE_WIDGET_HEIGHT; const spacing = 4; const hubWidgets = this.widgets?.filter(w => w.name?.startsWith("hub_")) || []; const numWidgets = hubWidgets.length; const requiredHeight = baseHeight + numWidgets * (widgetHeight + spacing) + (numWidgets > 0 ? spacing : 0); const computedSize = this.computeSize(); const newHeight = Math.max(computedSize[1], requiredHeight); if (Math.abs(this.size[1] - newHeight) > 2 || removedWidgets) { this.size[1] = newHeight; this.graph.setDirtyCanvas(true, true); } else if (widgetsChanged) { this.graph.setDirtyCanvas(true, false); } } // Перевірка на кількість активних вузлів після оновлення віджетів setTimeout(() => { const activeNodes = targetNodes.filter(node => node.mode === MODE_ALWAYS); // Якщо більше одного активного вузла, залишаємо тільки перший // Але не активуємо автоматично вузли, якщо всі вимкнені if (activeNodes.length > 1) { // Залишаємо перший вузол активним, інші вимикаємо for (let i = 1; i < activeNodes.length; i++) { setHubMode(activeNodes[i], MODE_BYPASS); // Оновлюємо відповідний віджет const widgetName = `hub_${activeNodes[i].id}`; const widget = this.widgets?.find(w => w.name === widgetName); if (widget) { widget.value = false; } } this.graph.setDirtyCanvas(true, false); } }, 100); }; // Визначаємо дії для контекстного меню node.getActions = function() { const actions = []; if (this.widgets?.some(w => w.name.startsWith("hub_"))) { actions.push({ name: "Activate First Hub", callback: () => this.setAllHubsMode(MODE_ALWAYS) }); actions.push({ name: "Switch To Next Hub", callback: () => this.toggleAllHubsMode() }); } actions.push({ name: "Log Hub Names (Console)", callback: () => this.logTargetNodeNames() }); return actions; }; node.logTargetNodeNames = function() { // Додаємо метод до екземпляра const nodes = HubNodesService.getHubNodes(); }; // Оновлений метод для встановлення режиму всіх хабів node.setAllHubsMode = function(mode) { const hubNodes = HubNodesService.getHubNodes(); if (hubNodes.length === 0) { return; } if (mode === MODE_ALWAYS) { // Активуємо тільки перший вузол, інші вимикаємо hubNodes.forEach((node, index) => { const newMode = index === 0 ? MODE_ALWAYS : MODE_BYPASS; if (node.mode !== newMode) { node.mode = newMode; updateAllBypasserWidgets(node); } }); app.graph.setDirtyCanvas(true, true); } else if (mode === MODE_BYPASS) { // Якщо режим BYPASS, вимикаємо всі вузли hubNodes.forEach(node => { if (node.mode !== MODE_BYPASS) { node.mode = MODE_BYPASS; updateAllBypasserWidgets(node); } }); app.graph.setDirtyCanvas(true, true); } }; // Оновлений метод для перемикання вузлів node.toggleAllHubsMode = function() { const hubNodes = HubNodesService.getHubNodes(); if (hubNodes.length === 0) { return; } // Перевіряємо, чи є хоча б один активний вузол const activeNodes = hubNodes.filter(n => n.mode === MODE_ALWAYS); if (activeNodes.length === 0) { // Якщо немає активних вузлів, активуємо перший if (hubNodes.length > 0) { const node = hubNodes[0]; node.mode = MODE_ALWAYS; updateAllBypasserWidgets(node); app.graph.setDirtyCanvas(true, true); } } else { // Якщо є активний вузол, знаходимо його індекс і активуємо наступний const activeNode = activeNodes[0]; const currentIndex = hubNodes.findIndex(n => n.id === activeNode.id); // Вимикаємо поточний активний вузол activeNode.mode = MODE_BYPASS; updateAllBypasserWidgets(activeNode); // Знаходимо наступний вузол (або переходимо до першого, якщо це останній) const nextIndex = (currentIndex + 1) % hubNodes.length; const nextNode = hubNodes[nextIndex]; // Активуємо наступний вузол nextNode.mode = MODE_ALWAYS; updateAllBypasserWidgets(nextNode); app.graph.setDirtyCanvas(true, true); } }; // Реєстрація/видалення самого bypasser'а в сервісі node.onAdded = function() { if (appInstance.HubNodesService) { // Перевіряємо наявність сервісу appInstance.HubNodesService.registerBypasserNode(this); } else { console.error(`[${NODE_TITLE} ID: ${this.id}] Cannot register bypasser, HubNodesService not found!`); } }; node.onRemoved = function() { if (appInstance.HubNodesService) { appInstance.HubNodesService.unregisterBypasserNode(this); } else { console.error(`[${NODE_TITLE} ID: ${this.id}] Cannot unregister bypasser, HubNodesService not found!`); } }; // Початкове оновлення віджетів для цього конкретного вузла setTimeout(() => { if (node.refreshWidgets) node.refreshWidgets(); }, 50); } }, getExtraMenuOptions(_, options) { options.push({ content: "Add Active Model", callback: () => { const node = LiteGraph.createNode(NODE_TYPE); // Записуємо текст для коментаря, щоб показати функціональність node.properties.desc = "Controls the flow of Model Parameters Pipe Hubs"; app.graph.add(node); // Розміщення вузла в центрі видимої області const canvas = app.canvas; const visible_rect = canvas.visible_area; node.pos[0] = visible_rect[0] + visible_rect[2] * 0.5 - node.size[0] * 0.5; node.pos[1] = visible_rect[1] + visible_rect[3] * 0.5 - node.size[1] * 0.5; // Явно налаштуємо ноду після створення з меню if (app.HubNodesService) { // Отримуємо розширення const extension = app.extensions.find(ext => ext.name === "MultiModel.ActiveModel.ServiceDriven"); if (extension) { // Конфігуруємо ноду extension.loadedGraphNode(node, app); // Оновлюємо віджети з затримкою setTimeout(() => { if (node.refreshWidgets) { node.refreshWidgets(); } }, 100); } } } }); }, nodeRegistered(type, clazz) { if (type === NODE_TYPE) { try { ActiveModel.setUp(clazz); } catch (e) { console.error(`[${NODE_TITLE}] Error in additional node setup:`, e); } } }, // Додаємо обробники створення ноди для максимального покриття nodeCreated(node) { if (node && node.type === NODE_TYPE) { this.loadedGraphNode(node, app); // Запускаємо оновлення віджетів з затримкою setTimeout(() => { if (node.refreshWidgets) { node.refreshWidgets(); } }, 100); } }, onNodeAdded(node) { if (node && node.type === NODE_TYPE) { this.loadedGraphNode(node, app); // Запускаємо оновлення віджетів з затримкою setTimeout(() => { if (node.refreshWidgets) { node.refreshWidgets(); } }, 100); } } }); console.log(`[${NODE_TITLE}] JS file loaded`);