Files
ethanfel-ComfyUI-LoRA-Optim…/js/lora_compatibility_analyzer.js
T
EthanfelandClaude Opus 4.6 3342ca2264 fix: space created nodes by type to prevent overlap
Use running Y offset with per-type spacing: 150px for LoraLoader
nodes, 300px for LoRAStackDynamic nodes. Prevents solo loader
nodes from overlapping each other.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 22:00:53 +01:00

151 lines
6.2 KiB
JavaScript

import { app } from "/scripts/app.js";
function findWidget(node, name) {
return node.widgets ? node.widgets.find((w) => w.name === name) : null;
}
app.registerExtension({
name: "LoRAOptimizer.CompatibilityAnalyzer",
nodeCreated(node) {
if (node.comfyClass !== "LoRACompatibilityAnalyzer") return;
if (!node.properties) node.properties = {};
if (!node.properties.createdGroupIds) node.properties.createdGroupIds = [];
// Clean up child nodes when analyzer is deleted
const origOnRemoved = node.onRemoved;
node.onRemoved = function () {
const tracked = this.properties?.createdGroupIds || [];
for (const id of tracked) {
if (id != null) {
const child = app.graph.getNodeById(id);
if (child && (child.comfyClass === "LoRAStackDynamic" || child.comfyClass === "LoraLoader" || child.comfyClass === "LoraLoaderModelOnly")) {
app.graph.remove(child);
}
}
}
if (origOnRemoved) origOnRemoved.call(this);
};
const origOnExecuted = node.onExecuted;
node.onExecuted = function (message) {
// Let ComfyUI handle ui.text and ui.images display first
if (origOnExecuted) origOnExecuted.call(this, message);
const groups = message?.groups;
if (!groups) return;
const hasClip = message.has_clip?.[0] !== false;
const tracked = node.properties.createdGroupIds || [];
const newTracked = [];
let mergeGroupNum = 0;
let yOffset = 0;
const loaderClass = hasClip ? "LoraLoader" : "LoraLoaderModelOnly";
// Update or create nodes for each group
for (let i = 0; i < groups.length; i++) {
const group = groups[i];
const isLoader = group.node_type === "loader";
const expectedClass = isLoader ? loaderClass : "LoRAStackDynamic";
let childNode = null;
// Try to reuse existing tracked node
if (i < tracked.length && tracked[i] != null) {
childNode = app.graph.getNodeById(tracked[i]);
// Remove if wrong type
if (childNode && childNode.comfyClass !== expectedClass) {
app.graph.remove(childNode);
childNode = null;
}
}
if (childNode && childNode.comfyClass === expectedClass) {
// Update existing node
if (isLoader) {
populateLoaderNode(childNode, group);
childNode.title = "Solo (Analyzer)";
} else {
mergeGroupNum++;
populateStackNode(childNode, group, i);
childNode.title = `Group ${mergeGroupNum} (Analyzer)`;
}
newTracked.push(childNode.id);
} else {
// Create new node
const created = LiteGraph.createNode(expectedClass);
if (!created) {
console.warn(`[CompatibilityAnalyzer] ${expectedClass} node type not found`);
continue;
}
app.graph.add(created);
created.pos = [
node.pos[0] + node.size[0] + 50,
node.pos[1] + yOffset,
];
if (isLoader) {
created.title = "Solo (Analyzer)";
populateLoaderNode(created, group);
} else {
mergeGroupNum++;
created.title = `Group ${mergeGroupNum} (Analyzer)`;
// Delay population to let widgets initialize (must exceed
// the 100ms initial-visibility timeout in lora_stack_dynamic.js)
setTimeout(() => populateStackNode(created, group, i), 200);
}
newTracked.push(created.id);
}
// Space nodes based on type: stacks are taller than loaders
yOffset += isLoader ? 150 : 300;
}
// Remove orphaned nodes (groups shrank)
for (let i = groups.length; i < tracked.length; i++) {
if (tracked[i] != null) {
const orphan = app.graph.getNodeById(tracked[i]);
if (orphan && (orphan.comfyClass === "LoRAStackDynamic" || orphan.comfyClass === "LoraLoader" || orphan.comfyClass === "LoraLoaderModelOnly")) {
app.graph.remove(orphan);
}
}
}
node.properties.createdGroupIds = newTracked;
app.canvas?.setDirty?.(true, true);
};
},
});
function populateLoaderNode(loaderNode, group) {
const nameWidget = findWidget(loaderNode, "lora_name");
if (nameWidget) nameWidget.value = group.lora_name;
const modelStr = findWidget(loaderNode, "strength_model");
if (modelStr) modelStr.value = group.strength;
const clipStr = findWidget(loaderNode, "strength_clip");
if (clipStr) clipStr.value = group.strength;
}
function populateStackNode(stackNode, group, groupIndex) {
const modeWidget = findWidget(stackNode, "input_mode");
if (modeWidget) modeWidget.value = "text";
const countWidget = findWidget(stackNode, "lora_count");
if (countWidget) countWidget.value = group.loras.length;
// Populate active slots and clear any stale surplus slots
const MAX_SLOTS = 10;
for (let i = 0; i < MAX_SLOTS; i++) {
const textWidget = findWidget(stackNode, `lora_name_text_${i + 1}`);
const strengthWidget = findWidget(stackNode, `strength_${i + 1}`);
if (i < group.loras.length) {
if (textWidget) textWidget.value = group.loras[i].name;
if (strengthWidget) strengthWidget.value = group.loras[i].strength;
} else {
if (textWidget) textWidget.value = "None";
if (strengthWidget) strengthWidget.value = 1.0;
}
}
}