140 lines
5.5 KiB
JavaScript
140 lines
5.5 KiB
JavaScript
// APNext H3 reference image autogrow
|
|
//
|
|
// The H3 reference writers declare nine optional image sockets (image_1..image_9)
|
|
// on the back end, mirroring ComfyUI's "MiniMax H3 Reference to Video" node. Nine
|
|
// empty sockets make the node tall and hard to read, so this keeps only the
|
|
// connected ones plus one spare: connect image_1 and image_2 appears, and so on.
|
|
// The matching pass-through outputs grow and shrink with them.
|
|
//
|
|
// Nothing here changes what the back end sees. Every socket is a declared
|
|
// optional input, so a workflow keeps working with this file removed.
|
|
|
|
import { app } from "../../../scripts/app.js";
|
|
|
|
const NODE_CLASSES = new Set([
|
|
"H3ClaudeCodeRefWriter",
|
|
"H3RefPromptWriter",
|
|
"H3ClaudeCodeCrossoverWriter",
|
|
"H3ClaudeCodeMusicVideoWriter",
|
|
"H3ClaudeCodePresentationWriter",
|
|
"H3ClaudeCodeShortFilmWriter",
|
|
"H3MusicVideoMinimal",
|
|
"H3PromptPreview", // inputs only (thumbnails), no pass-through outputs
|
|
]);
|
|
const MAX_IMAGES = 9;
|
|
|
|
const slotName = (i) => `image_${i}`;
|
|
|
|
function findInput(node, i) {
|
|
return (node.inputs || []).findIndex((input) => input && input.name === slotName(i));
|
|
}
|
|
|
|
function findOutput(node, i) {
|
|
return (node.outputs || []).findIndex((output) => output && output.name === slotName(i));
|
|
}
|
|
|
|
function outputHasLinks(output) {
|
|
return Array.isArray(output.links) && output.links.length > 0;
|
|
}
|
|
|
|
function imageOutputsAreTail(node) {
|
|
// The image outputs may only be grown/trimmed while they are the LAST
|
|
// outputs of the node. The server addresses outputs by slot index, so
|
|
// removing an image output that sits before a later non-image output
|
|
// (e.g. the Music Video Writer's `clip_starts`) would shift that output's
|
|
// index and silently re-point its links at an IMAGE slot.
|
|
const outs = node.outputs || [];
|
|
let sawImage = false;
|
|
for (const o of outs) {
|
|
if (/^image_\d+$/.test(o?.name || "")) sawImage = true;
|
|
else if (sawImage) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function syncImageSlots(node) {
|
|
if (!node || node._apnextSyncing) return;
|
|
node._apnextSyncing = true;
|
|
try {
|
|
let highest = 0;
|
|
for (let i = 1; i <= MAX_IMAGES; i++) {
|
|
const idx = findInput(node, i);
|
|
if (idx >= 0 && node.inputs[idx].link != null) highest = i;
|
|
}
|
|
// Any output that is wired downstream must keep its socket as well.
|
|
for (let i = 1; i <= MAX_IMAGES; i++) {
|
|
const idx = findOutput(node, i);
|
|
if (idx >= 0 && outputHasLinks(node.outputs[idx])) highest = Math.max(highest, i);
|
|
}
|
|
const wanted = Math.min(MAX_IMAGES, highest + 1);
|
|
const manageOutputs = node._apnextImageOutputs && imageOutputsAreTail(node);
|
|
|
|
// Sockets 1..wanted must exist. Adding appends at the tail; the image
|
|
// sockets are declared last among the sockets, so numeric order holds.
|
|
const rows = () => Math.max((node.inputs || []).length, (node.outputs || []).length);
|
|
const rowsBefore = rows();
|
|
let added = false;
|
|
let removed = false;
|
|
for (let i = 1; i <= wanted; i++) {
|
|
if (findInput(node, i) < 0) { node.addInput(slotName(i), "IMAGE"); added = true; }
|
|
if (manageOutputs && findOutput(node, i) < 0) { node.addOutput(slotName(i), "IMAGE"); added = true; }
|
|
}
|
|
|
|
// Trailing sockets past the spare go, but never one that carries a link.
|
|
for (let i = MAX_IMAGES; i > wanted; i--) {
|
|
const inIdx = findInput(node, i);
|
|
if (inIdx >= 0 && node.inputs[inIdx].link == null) { node.removeInput(inIdx); removed = true; }
|
|
if (manageOutputs) {
|
|
const outIdx = findOutput(node, i);
|
|
if (outIdx >= 0 && !outputHasLinks(node.outputs[outIdx])) { node.removeOutput(outIdx); removed = true; }
|
|
}
|
|
}
|
|
|
|
if (added || removed) {
|
|
// Adjust the height by exactly the rows that came or went, so a size the
|
|
// user chose survives; never drop below what the widgets need.
|
|
const slotHeight = (globalThis.LiteGraph && globalThis.LiteGraph.NODE_SLOT_HEIGHT) || 20;
|
|
const minimum = node.computeSize();
|
|
const height = Math.max(minimum[1], node.size[1] + (rows() - rowsBefore) * slotHeight);
|
|
node.setSize([Math.max(node.size[0], minimum[0]), height]);
|
|
node.setDirtyCanvas(true, true);
|
|
}
|
|
} finally {
|
|
node._apnextSyncing = false;
|
|
}
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "apnext.h3.referenceImages",
|
|
|
|
beforeRegisterNodeDef(nodeType, nodeData) {
|
|
if (!NODE_CLASSES.has(nodeData?.name)) return;
|
|
// Only writers pass the images through; the preview just shows them.
|
|
nodeType.prototype._apnextImageOutputs = (nodeData.output_name || []).includes("image_1");
|
|
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
const result = onNodeCreated?.apply(this, arguments);
|
|
// Let ComfyUI finish building widgets before trimming.
|
|
setTimeout(() => syncImageSlots(this), 0);
|
|
return result;
|
|
};
|
|
|
|
const onConfigure = nodeType.prototype.onConfigure;
|
|
nodeType.prototype.onConfigure = function () {
|
|
const result = onConfigure?.apply(this, arguments);
|
|
// Links are restored after configure; sync once they are in place.
|
|
setTimeout(() => syncImageSlots(this), 0);
|
|
return result;
|
|
};
|
|
|
|
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
|
|
nodeType.prototype.onConnectionsChange = function (type, index, connected, linkInfo, ioSlot) {
|
|
const result = onConnectionsChange?.apply(this, arguments);
|
|
const name = ioSlot?.name || "";
|
|
if (/^image_\d+$/.test(name)) setTimeout(() => syncImageSlots(this), 0);
|
|
return result;
|
|
};
|
|
},
|
|
});
|