Files
FaberVS-MultiModel/js/active_model.js
T
2025-04-30 12:02:56 +03:00

624 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// --- 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`);