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>
151 lines
6.2 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
}
|