From 4cbca53cda250a7fdf74a38a1f2a1ab276d755ac Mon Sep 17 00:00:00 2001 From: FaberVS <54268603+FaberVS@users.noreply.github.com> Date: Tue, 6 May 2025 17:27:06 +0300 Subject: [PATCH] Add files via upload --- js/ModelParamsNodeExt.js | 198 +--------------------- js/MultiModelSwitch.js | 347 +++++++++++++++++---------------------- js/active_model.js | 179 ++++++++++++++++++-- 3 files changed, 319 insertions(+), 405 deletions(-) diff --git a/js/ModelParamsNodeExt.js b/js/ModelParamsNodeExt.js index 8946106..08d24f3 100644 --- a/js/ModelParamsNodeExt.js +++ b/js/ModelParamsNodeExt.js @@ -1,197 +1,3 @@ // js/ModelParamsNodeExt.js -// Розширення для ModelParamsPipeNode, щоб він повідомляв про свої зміни - -import { app } from "/scripts/app.js"; - -const TARGET_NODE_TYPE = "ModelParamsPipe"; // Цільовий тип вузла, який ми розширюємо - -// --- Loading js/ModelParamsNodeExt.js --- v2-dynamic-inputs - -app.registerExtension({ - name: "MultiModel.ModelParamsPipeNode.Notifier", - - // Додатковий трекінг для діагностики - newNodesDetected: false, - - // Додаємо базову ініціалізацію та прямий пошук вузлів - async setup() { - // Перевіряємо наявність сервісу одразу - if (app.HubNodesService) { - } else { - console.error(`[${TARGET_NODE_TYPE} Notifier] HubNodesService NOT found during setup!`); - } - - // Зачекаємо, доки граф буде завантажено, і потім проскануємо його - const checkForGraph = () => { - if (app.graph) { - // Додаємо обробник зміни графу - if (app.graph.onNodeAdded && !app.graph._notifier_attached) { - const origNodeAdded = app.graph.onNodeAdded; - - app.graph.onNodeAdded = function(node) { - if (origNodeAdded) { - origNodeAdded.call(this, node); - } - - // Перевіряємо, чи це наш цільовий тип - if (node.type === TARGET_NODE_TYPE) { - // Викликаємо наш обробник - const extension = app.extensions.find(ext => ext.name === "MultiModel.ModelParamsPipeNode.Notifier"); - if (extension) { - extension.setupNodeMonitoring(node); - - if (app.HubNodesService) { - app.HubNodesService.notifyHubAdded(node); - - // Пробуємо різні методи оновлення інтерфейсу - if (typeof app.HubNodesService.forceUpdates === 'function') { - app.HubNodesService.forceUpdates(); - } else if (typeof app.HubNodesService.updateWidgets === 'function') { - app.HubNodesService.updateWidgets(); - } else if (app.HubNodesService.refreshUI && typeof app.HubNodesService.refreshUI === 'function') { - app.HubNodesService.refreshUI(); - } - } - } - } - }; - - app.graph._notifier_attached = true; - } - - // Зробимо сканування графу з невеликою затримкою для певності - setTimeout(() => { - if (!app.graph || !app.graph._nodes) { - return; - } - - const targetNodes = app.graph._nodes.filter(node => node.type === TARGET_NODE_TYPE); - - const extension = app.extensions.find(ext => ext.name === "MultiModel.ModelParamsPipeNode.Notifier"); - if (!extension) { - return; - } - - // Обробляємо знайдені вузли - for (const node of targetNodes) { - extension.setupNodeMonitoring(node); - - // Якщо знайшли хоча б один вузол, спробуємо оновити інтерфейс - if (app.HubNodesService) { - } - } - - // Якщо було знайдено вузли, форсуємо оновлення - if (targetNodes.length > 0 && app.HubNodesService) { - if (typeof app.HubNodesService.forceUpdates === 'function') { - app.HubNodesService.forceUpdates(); - } else if (typeof app.HubNodesService.updateWidgets === 'function') { - app.HubNodesService.updateWidgets(); - } else if (app.HubNodesService.refreshUI && typeof app.HubNodesService.refreshUI === 'function') { - app.HubNodesService.refreshUI(); - } - } - }, 500); - } else { - setTimeout(checkForGraph, 500); - } - }; - - // Запускаємо перевірку графу - checkForGraph(); - }, - - // Встановлення моніторингу title/видалення для вузла - setupNodeMonitoring(node, appInstance = app) { - if (!node) return false; - - // --- Встановлюємо моніторинг title --- - if (!node.__title_monitored) { - try { - const originalTitle = node.title; - - // Зберігаємо оригінальне значення - Object.defineProperty(node, '_monitored_title', { - value: originalTitle, - writable: true - }); - - // Перевизначаємо геттер/сеттер для title - Object.defineProperty(node, 'title', { - get: function() { - return this._monitored_title; - }, - set: function(newTitle) { - // Якщо значення не змінилось, не виконуємо додаткові дії - if (this._monitored_title === newTitle) { - return; - } - - // Зберігаємо нове значення - this._monitored_title = newTitle; - - // Повідомляємо сервіс про перейменування з невеликою затримкою - setTimeout(() => { - if (appInstance.HubNodesService) { - appInstance.HubNodesService.notifyHubRenamed(this); - } else { - console.error(` HubNodesService not found! Cannot notify rename.`); - } - }, 0); - }, - enumerable: true, - configurable: true - }); - - node.__title_monitored = true; - } catch (error) { - console.error(`[${TARGET_NODE_TYPE} Notifier] Error setting up title monitoring:`, error); - } - } - - // --- Встановлюємо перехоплення видалення --- - if (!node.onRemoved || !node.onRemoved.__isNotifierOverridden) { - try { - const originalOnRemoved = node.onRemoved; - - node.onRemoved = function() { - // Повідомляємо сервіс про видалення - if (appInstance.HubNodesService) { - appInstance.HubNodesService.notifyHubRemoved(this); - } else { - console.error(` HubNodesService not found! Cannot notify removal.`); - } - - // Викликаємо оригінальний метод, якщо він існує - if (originalOnRemoved) { - originalOnRemoved.apply(this, arguments); - } - }; - - node.onRemoved.__isNotifierOverridden = true; - } catch (error) { - console.error(`[${TARGET_NODE_TYPE} Notifier] Error overriding onRemoved:`, error); - } - } - - return true; - }, - - // Для логування проблемних випадків - async nodeCreated(node) { - }, - - // Для логування проблемних випадків - async onNodeAdded(node) { - }, - - // Хук для обробки вузлів, що вже існують при завантаженні - async loadedGraphNode(node, appInstance) { - if (!node) return; - - if (node.type === TARGET_NODE_TYPE) { - // Встановлюємо моніторинг для існуючого вузла - this.setupNodeMonitoring(node, appInstance); - } - } -}); \ No newline at end of file +// Заглушка. Вся логіка перенесена в active_model.js. +// Можна безпечно видалити цей файл або залишити як заглушку. \ No newline at end of file diff --git a/js/MultiModelSwitch.js b/js/MultiModelSwitch.js index 13e2db1..5d1778e 100644 --- a/js/MultiModelSwitch.js +++ b/js/MultiModelSwitch.js @@ -20,215 +20,172 @@ app.registerExtension({ async beforeRegisterNodeDef(nodeType, nodeData, app) { // Compare with the class name if (nodeData.name === nodeClassName) { - console.log(`[MultiModel] Patching ${nodeClassName}`); + console.log(`[ModelSwitch] Patching ${nodeClassName}`); - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function() { - onNodeCreated?.apply(this, arguments); - console.log(`[MultiModel] ${nodeClassName} created`); + // --- ФУНКЦІЇ --- + function handleModeSwitch(modeWidget, indexWidget, app, value) { + const updateVisibility = (modeValue) => { + if (indexWidget) { + const shouldBeHidden = (modeValue !== "index"); + indexWidget.hidden = shouldBeHidden; + if (app.graph) app.graph.setDirtyCanvas(true, false); + } + }; + updateVisibility(value ?? modeWidget.value); + // Перевизначаємо callback + const originalModeCallback = modeWidget.callback; + modeWidget.callback = (val) => { + originalModeCallback?.apply(modeWidget, [val]); + updateVisibility(val); + }; + } + function updateDynamicInputIndexes(node) { + console.log('[ModelSwitch] Оновлення індексів динамічних входів'); + const prefix = node._input_prefix || "input_"; + let dynamicInputs = node.inputs ? node.inputs.filter(inp => inp.name !== "mode" && inp.name !== "index") : []; + // Перейменовуємо динамічні входи по порядку + for (let i = 0; i < dynamicInputs.length; i++) { + const expectedName = `${prefix}${i + 1}`; + if (dynamicInputs[i].name !== expectedName) { + console.log(`[ModelSwitch] Перейменування входу ${dynamicInputs[i].name} -> ${expectedName}`); + dynamicInputs[i].name = expectedName; + } + } + let maxIdx = dynamicInputs.length > 0 ? dynamicInputs.length : 1; + node.last_connected = -1; + for (let i = 0; i < dynamicInputs.length; i++) { + const inp = dynamicInputs[i]; + if (inp.link != null) node.last_connected = node.inputs.indexOf(inp); + } + node._last_input_index = maxIdx; + // Останній порожній динамічний вхід + let lastEmptyIdx = -1; + for (let i = node.inputs.length - 1; i >= 0; i--) { + if (node.inputs[i].name !== "mode" && node.inputs[i].name !== "index" && node.inputs[i].name.startsWith(prefix) && node.inputs[i].link == null) { + lastEmptyIdx = i; + break; + } + } + node.last_empty = lastEmptyIdx; + } + + function updateDynamicInputsOnConnect(node, type, index, connected) { + if (node._updatingInputs) return; + node._updatingInputs = true; + const changedInput = node.inputs[index]; + const inputName = changedInput?.name; + const isDynamic = inputName && inputName !== "mode" && inputName !== "index"; + if (!isDynamic) { + node._updatingInputs = false; + return; + } + if (connected) { + console.log(`[ModelSwitch] Вхід "${inputName}" (${index}) підключено`); + if (index === node.last_empty) { + const prefix = node._input_prefix || "input_"; + let dynamicInputs = node.inputs.filter(inp => inp.name !== "mode" && inp.name !== "index"); + node.addInput(`${prefix}${dynamicInputs.length + 1}`, "*"); + console.log('[ModelSwitch] Додаємо ще один порожній динамічний вхід'); + updateDynamicInputIndexes(node); + } else { + console.log('[ModelSwitch] Підключено не до останнього вільного входу, новий не додаємо'); + } + } else { + console.log(`[ModelSwitch] Вхід "${inputName}" (${index}) відключено`); + // Якщо відключено від останнього підключеного, прибираємо цей вхід, але лише якщо порожніх динамічних входів більше одного + let dynamicInputs = node.inputs.filter(inp => inp.name !== "mode" && inp.name !== "index"); + let emptyInputs = dynamicInputs.filter(inp => inp.link == null); + if (index === node.last_connected && emptyInputs.length > 1) { + node.removeInput(index); + console.log('[ModelSwitch] Відключено від останнього підключеного — видаляємо цей вхід'); + updateDynamicInputIndexes(node); + } else if (index === node.last_connected) { + console.log('[ModelSwitch] Це останній порожній вхід — не видаляємо'); + } else { + console.log('[ModelSwitch] Відключено не від останнього підключеного — нічого не робимо'); + } + } + node._updatingInputs = false; + } + + function handleNodeCreation(node, app) { // --- Віджети --- - const modeWidget = this.widgets.find(w => w.name === "mode"); - const indexWidget = this.widgets.find(w => w.name === "index"); + const modeWidget = node.widgets.find(w => w.name === "mode"); + const indexWidget = node.widgets.find(w => w.name === "index"); if (!modeWidget) { - console.error(`[MultiModel] ${nodeClassName}: 'mode' widget not found! Cannot apply dynamic visibility.`); + console.error(`[ModelSwitch] ${nodeClassName}: 'mode' widget not found! Cannot apply dynamic visibility.`); return; } if (!indexWidget) { - console.error(`[MultiModel] ${nodeClassName}: 'index' widget not found! Cannot apply dynamic visibility.`); + console.error(`[ModelSwitch] ${nodeClassName}: 'index' widget not found! Cannot apply dynamic visibility.`); return; } - const updateVisibility = (modeValue) => { - if (indexWidget) { - const shouldBeHidden = (modeValue !== "index"); - indexWidget.hidden = shouldBeHidden; - app.graph.setDirtyCanvas(true, false); - } - }; - const originalModeCallback = modeWidget.callback; - modeWidget.callback = (value) => { - originalModeCallback?.apply(modeWidget, [value]); - updateVisibility(value); - }; - updateVisibility(modeWidget.value); + handleModeSwitch(modeWidget, indexWidget, app); // --- Динамічні входи (last_empty, last_connected) --- - this._input_prefix = "input_"; - const prefix = this._input_prefix; - this._last_input_index = 1; - this.last_empty = 0; - this.last_connected = null; - // Якщо входів немає або немає input_1 — додаємо перший вхід - let hasInput1 = false; - if (this.inputs && this.inputs.length > 0) { - for (let i = 0; i < this.inputs.length; i++) { - if (this.inputs[i].name === `${prefix}1`) { - hasInput1 = true; - break; - } - } + node._input_prefix = "input_"; + const prefix = node._input_prefix; + let dynamicInputs = node.inputs ? node.inputs.filter(inp => inp.name !== "mode" && inp.name !== "index") : []; + console.log('[ModelSwitch] dynamicInputs:', dynamicInputs.map(inp => ({name: inp.name, link: inp.link}))); + // --- Якщо входів немає, додаємо перший --- + if (dynamicInputs.length === 0) { + console.log('[ModelSwitch] Входів немає, додаємо перший'); + node.addInput(`${prefix}1`, "*"); + dynamicInputs = node.inputs.filter(inp => inp.name !== "mode" && inp.name !== "index"); } - if (!hasInput1) { - this.addInput(`${prefix}1`, "*"); - } - // Оновлюємо індекси для існуючих входів - if (this.inputs && this.inputs.length > 0) { - let maxIdx = 1; - for (let i = 0; i < this.inputs.length; i++) { - const inp = this.inputs[i]; - if (inp.name && inp.name.startsWith(prefix)) { - const idx = parseInt(inp.name.replace(prefix, ""), 10); - if (!isNaN(idx) && idx > maxIdx) maxIdx = idx; - if (inp.link != null) this.last_connected = i; - } - } - this._last_input_index = maxIdx; - this.last_empty = this.inputs.length - 1; - } - // --- Зберігаємо попередній стан links --- - this._prevLinks = []; - for (let i = 0; i < this.inputs.length; i++) { - this._prevLinks[i] = this.inputs[i].link; - } - app.graph.setDirtyCanvas(true, true); - // this.updateInputs(); // закоментовано, бо ця функція більше не використовується - // --- Динамічна логіка підключень (last_empty, last_connected) --- - const onConnectionsChange = nodeType.prototype.onConnectionsChange; - nodeType.prototype.onConnectionsChange = function(type, index, connected, link_info, slot) { - onConnectionsChange?.apply(this, arguments); - if (this._updatingInputs) return; - if (type !== 1) return; - const prefix = this._input_prefix || "input_"; - const wasLinked = this._prevLinks && this._prevLinks[index]; - const isLinked = this.inputs[index] && this.inputs[index].link; - // Додаємо новий вхід тільки якщо було null, а стало не null - if ( - connected && - index === this.last_empty && - wasLinked == null && - isLinked != null - ) { - this._updatingInputs = true; - // --- Коректна нумерація --- - let maxIdx = 0; - for (let i = 0; i < this.inputs.length; i++) { - const inp = this.inputs[i]; - if (inp.name && inp.name.startsWith(prefix)) { - const idx = parseInt(inp.name.replace(prefix, ""), 10); - if (!isNaN(idx) && idx > maxIdx) maxIdx = idx; + let connectedInputs = dynamicInputs.filter(inp => inp.link != null); + let emptyInputs = dynamicInputs.filter(inp => inp.link == null); + console.log('[ModelSwitch] connectedInputs:', connectedInputs); + console.log('[ModelSwitch] emptyInputs:', emptyInputs); + if (emptyInputs.length === 0) { + console.log('[ModelSwitch] Додаємо новий порожній вхід'); + node.addInput(`${prefix}${connectedInputs.length + 1}`, "*"); + dynamicInputs = node.inputs.filter(inp => inp.name !== "mode" && inp.name !== "index"); + } else if (emptyInputs.length > 1) { + console.log('[ModelSwitch] Видаляємо зайві порожні входи'); + for (let i = node.inputs.length - 1; i >= 0; i--) { + if (node.inputs[i].name !== "mode" && node.inputs[i].name !== "index" && node.inputs[i].link == null && emptyInputs.length > 1) { + node.removeInput(i); + emptyInputs.pop(); + } } - } - this._last_input_index = maxIdx + 1; - this.addInput(`${prefix}${this._last_input_index}`, "*"); - this.last_connected = this.last_empty; - this.last_empty = this.inputs.length - 1; - app.graph.setDirtyCanvas(true, true); - this._updatingInputs = false; - } - // Якщо відключили будь-який вхід - if (!connected) { - // Знайти новий last_connected - let new_last_connected = null; - for (let i = this.inputs.length - 1; i >= 0; i--) { - if (this.inputs[i].name.startsWith(prefix) && this.inputs[i].link != null) { - new_last_connected = i; - break; - } - } - // Видалити всі порожні входи після last_connected, але залишити один (last_empty) - let removed = false; - for (let i = this.inputs.length - 1; i > new_last_connected + 1; i--) { - if (this.inputs[i].name.startsWith(prefix) && this.inputs[i].link == null) { - this.removeInput(i); - removed = true; - } - } - this.last_connected = new_last_connected; - this.last_empty = this.inputs.length - 1; - // Якщо останній вхід зайнятий, додаємо ще один порожній - if ( - this.inputs.length === 0 || - (this.inputs[this.inputs.length - 1].name.startsWith(prefix) && this.inputs[this.inputs.length - 1].link != null) - ) { - // --- Коректна нумерація --- - let maxIdx = 0; - for (let i = 0; i < this.inputs.length; i++) { - const inp = this.inputs[i]; - if (inp.name && inp.name.startsWith(prefix)) { - const idx = parseInt(inp.name.replace(prefix, ""), 10); - if (!isNaN(idx) && idx > maxIdx) maxIdx = idx; - } - } - this._last_input_index = maxIdx + 1; - this.addInput(`${prefix}${this._last_input_index}`, "*"); - this.last_empty = this.inputs.length - 1; - removed = true; - } - if (removed) { - app.graph.setDirtyCanvas(true, true); - } - } - // --- Оновлюємо _prevLinks після всіх змін --- - this._prevLinks = []; - for (let i = 0; i < this.inputs.length; i++) { - this._prevLinks[i] = this.inputs[i].link; - } - }; - // --- Закоментовано стару логіку --- - /* - const onConnectionsChange = nodeType.prototype.onConnectionsChange; - nodeType.prototype.onConnectionsChange = function(type, index, connected, link_info, slot) { - onConnectionsChange?.apply(this, arguments); - this.scheduleUpdateInputs(); - }; - nodeType.prototype.scheduleUpdateInputs = debounce(function() { - this.updateInputs(); - }, 100); - nodeType.prototype.updateInputs = function() { - const prefix = "input_"; - const minInputs = 1; - // --- Гарантуємо перший слот --- - const dynamicInputs = this.inputs.filter(inp => inp.name && inp.name.startsWith(prefix)); - if (dynamicInputs.length === 0) { - const name = `${prefix}1`; - this.addInput(name, "*"); - app.graph.setDirtyCanvas(true, true); - return; - } - let lastConnectedIndex = -1; - for (let i = 0; i < this.inputs.length; i++) { - if (this.inputs[i].name.startsWith(prefix) && this.inputs[i].link != null) { - lastConnectedIndex = i; - } - } - let removalIndex = this.inputs.length - 1; - while (removalIndex > lastConnectedIndex && this.inputs.length > minInputs && removalIndex >= 0) { - if (this.inputs[removalIndex].name.startsWith(prefix) && this.inputs[removalIndex].link == null) { - if(this.inputs.length > minInputs) { - this.removeInput(removalIndex); - } else { - break; - } - } else { - break; - } - removalIndex--; - } - // Add a new input if the last one is connected OR if there are no inputs yet - if (dynamicInputs.length > 0 && this.inputs[this.inputs.length - 1].name.startsWith(prefix) && this.inputs[this.inputs.length - 1].link != null) { - // Знаходимо найбільший індекс серед динамічних входів - const dynamicIndexes = dynamicInputs - .map(inp => parseInt(inp.name.replace(prefix, ""), 10)) - .filter(num => !isNaN(num)); - const maxIndex = dynamicIndexes.length > 0 ? Math.max(...dynamicIndexes) : 0; - const nextIndex = maxIndex + 1; - const name = `${prefix}${nextIndex}`; - this.addInput(name, "*"); - } - app.graph.setDirtyCanvas(true, true); - }; - */ + dynamicInputs = node.inputs.filter(inp => inp.name !== "mode" && inp.name !== "index"); + } + console.log('[ModelSwitch] dynamicInputs після додавання/видалення:', dynamicInputs.map(inp => ({name: inp.name, link: inp.link}))); + updateDynamicInputIndexes(node); + // --- Зберігаємо попередній стан links --- + node._prevLinks = []; + for (let i = 0; i < node.inputs.length; i++) { + node._prevLinks[i] = node.inputs[i].link; + } + if (app.graph) app.graph.setDirtyCanvas(true, true); + } + + function handleConnectionsChange(node, type, index, connected, link_info, slot, app) { + updateDynamicInputsOnConnect(node, type, index, connected); + node._prevLinks = []; + for (let i = 0; i < node.inputs.length; i++) { + node._prevLinks[i] = node.inputs[i].link; + } + } + + // --- Патчимо onNodeCreated --- + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function() { + onNodeCreated?.apply(this, arguments); + console.log(`[ModelSwitch] ${nodeClassName} created`); + setTimeout(() => { + handleNodeCreation(this, app); + }, 500); + }; + + // --- Патчимо onConnectionsChange --- + const onConnectionsChange = nodeType.prototype.onConnectionsChange; + nodeType.prototype.onConnectionsChange = function(type, index, connected, link_info, slot) { + onConnectionsChange?.apply(this, arguments); + handleConnectionsChange(this, type, index, connected, link_info, slot, app); }; } }, diff --git a/js/active_model.js b/js/active_model.js index cfe680a..28f5582 100644 --- a/js/active_model.js +++ b/js/active_model.js @@ -245,6 +245,7 @@ app.registerExtension({ name: "MultiModel.ActiveModel.ServiceDriven", // Оновлено назву розширення setup(appInstance) { + console.log("[MultiModel] ✅ Ініціалізовано HubNodesService у app.HubNodesService"); // Реєструємо сервіс, щоб він був доступний іншим розширенням appInstance.HubNodesService = HubNodesService; HubNodesService.app = appInstance; @@ -367,24 +368,39 @@ app.registerExtension({ } 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; - } + // --- Виправлений callback: взаємовиключна логіка --- + existingWidget.callback = (value) => { + const hubNodes = HubNodesService.getHubNodes(); + const nodeToActivate = app.graph.getNodeById(targetNode.id); + if (value) { + // Вимикаємо всі інші, вмикаємо лише обраний + hubNodes.forEach(n => { + const nObj = app.graph.getNodeById(n.id); + if (nObj) nObj.mode = (n.id === targetNode.id) ? MODE_ALWAYS : MODE_BYPASS; + }); + } else { + // Якщо вимикаємо, просто переводимо цей вузол у BYPASS + if (nodeToActivate) nodeToActivate.mode = MODE_BYPASS; + } + // Оновлюємо всі віджети + if (this.refreshWidgets) this.refreshWidgets(); + }; + 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); + const hubNodes = HubNodesService.getHubNodes(); + const nodeToActivate = app.graph.getNodeById(targetNode.id); + if (value) { + hubNodes.forEach(n => { + const nObj = app.graph.getNodeById(n.id); + if (nObj) nObj.mode = (n.id === targetNode.id) ? MODE_ALWAYS : MODE_BYPASS; + }); + } else { + if (nodeToActivate) nodeToActivate.mode = MODE_BYPASS; } - else this.refreshWidgets(); + if (this.refreshWidgets) this.refreshWidgets(); }, { on: "ON", off: "OFF" } ); @@ -629,4 +645,139 @@ app.registerExtension({ } }); -console.log(`[${NODE_TITLE}] JS file loaded`); \ No newline at end of file +// --- ModelParamsPipe Notifier (інтегровано з ModelParamsNodeExt.js) --- +(function(){ + const TARGET_NODE_TYPE = "ModelParamsPipe"; + app.registerExtension({ + name: "MultiModel.ModelParamsPipeNode.Notifier", + newNodesDetected: false, + async setup() { + // Чекаємо на HubNodesService + const waitForHubNodesService = async (timeout = 5000) => { + const start = Date.now(); + while (!app.HubNodesService) { + if (Date.now() - start > timeout) { + throw new Error("HubNodesService not found after waiting"); + } + await new Promise(r => setTimeout(r, 100)); + } + }; + await waitForHubNodesService(); + // Додаємо базову ініціалізацію та прямий пошук вузлів + const checkForGraph = () => { + if (app.graph) { + if (app.graph.onNodeAdded && !app.graph._notifier_attached) { + const origNodeAdded = app.graph.onNodeAdded; + app.graph.onNodeAdded = function(node) { + if (origNodeAdded) origNodeAdded.call(this, node); + if (node.type === TARGET_NODE_TYPE) { + const extension = app.extensions.find(ext => ext.name === "MultiModel.ModelParamsPipeNode.Notifier"); + if (extension) { + extension.setupNodeMonitoring(node); + if (app.HubNodesService) { + app.HubNodesService.notifyHubAdded(node); + if (typeof app.HubNodesService.forceUpdates === 'function') { + app.HubNodesService.forceUpdates(); + } else if (typeof app.HubNodesService.updateWidgets === 'function') { + app.HubNodesService.updateWidgets(); + } else if (app.HubNodesService.refreshUI && typeof app.HubNodesService.refreshUI === 'function') { + app.HubNodesService.refreshUI(); + } + } + } + } + }; + app.graph._notifier_attached = true; + } + setTimeout(() => { + if (!app.graph || !app.graph._nodes) return; + const targetNodes = app.graph._nodes.filter(node => node.type === TARGET_NODE_TYPE); + const extension = app.extensions.find(ext => ext.name === "MultiModel.ModelParamsPipeNode.Notifier"); + if (!extension) return; + for (const node of targetNodes) { + extension.setupNodeMonitoring(node); + } + if (targetNodes.length > 0 && app.HubNodesService) { + if (typeof app.HubNodesService.forceUpdates === 'function') { + app.HubNodesService.forceUpdates(); + } else if (typeof app.HubNodesService.updateWidgets === 'function') { + app.HubNodesService.updateWidgets(); + } else if (app.HubNodesService.refreshUI && typeof app.HubNodesService.refreshUI === 'function') { + app.HubNodesService.refreshUI(); + } + } + }, 500); + } else { + setTimeout(checkForGraph, 500); + } + }; + checkForGraph(); + }, + setupNodeMonitoring(node, appInstance = app) { + if (!node) return false; + if (!node.__title_monitored) { + try { + const originalTitle = node.title; + if (!Object.getOwnPropertyDescriptor(node, '_monitored_title')) { + Object.defineProperty(node, '_monitored_title', { + value: originalTitle, + writable: true + }); + } + const titleDescriptor = Object.getOwnPropertyDescriptor(node, 'title'); + if (!titleDescriptor || titleDescriptor.configurable) { + Object.defineProperty(node, 'title', { + get: function() { return this._monitored_title; }, + set: function(newTitle) { + if (this._monitored_title === newTitle) return; + this._monitored_title = newTitle; + setTimeout(() => { + if (appInstance.HubNodesService) { + appInstance.HubNodesService.notifyHubRenamed(this); + } else { + console.error(` HubNodesService not found! Cannot notify rename.`); + } + }, 0); + }, + enumerable: true, + configurable: true + }); + } + node.__title_monitored = true; + } catch (error) { + console.error(`[${TARGET_NODE_TYPE} Notifier] Error setting up title monitoring:`, error); + } + } + if (!node.onRemoved || !node.onRemoved.__isNotifierOverridden) { + try { + const originalOnRemoved = node.onRemoved; + node.onRemoved = function() { + if (appInstance.HubNodesService) { + appInstance.HubNodesService.notifyHubRemoved(this); + } else { + console.error(` HubNodesService not found! Cannot notify removal.`); + } + if (originalOnRemoved) { + originalOnRemoved.apply(this, arguments); + } + }; + node.onRemoved.__isNotifierOverridden = true; + } catch (error) { + console.error(`[${TARGET_NODE_TYPE} Notifier] Error overriding onRemoved:`, error); + } + } + return true; + }, + async nodeCreated(node) {}, + async onNodeAdded(node) {}, + async loadedGraphNode(node, appInstance) { + if (!node) return; + if (node.type === TARGET_NODE_TYPE) { + this.setupNodeMonitoring(node, appInstance); + } + } + }); +})(); + +console.log(`[${NODE_TITLE}] JS file loaded`); +console.log("[MultiModel] ✅ Завантажено active_model.js, HubNodesService буде доступний після ініціалізації."); \ No newline at end of file