Files
robertvoy-ComfyUI-Distributed/web/imageListStart.js
T
Robert Wojciechowski dcddf23db2 feat(image-list)!: replace fixed Start extras with growable value_N slots
Distributed Image List Start no longer has reference_image, text,
strength, seed or per-slot mode dropdowns. It now takes images, mask and
up to 10 generic value_N inputs (IMAGE, MASK, STRING, INT, FLOAT,
BOOLEAN) that grow via native V3 Autogrow, each with a matching output.

- One value is shared by every image; one per image pairs in order; any
  other count fails before dispatch. Only images is split into frames.
- Outputs are declared up front (image, mask, item_index, context,
  value_1..value_10). web/imageListStart.js hides unused value outputs,
  mirrors input types, and follows Autogrow compaction so downstream
  links keep their value.
- Image-list protocol bumped to v2; older workers fail preflight.

Breaking: saved workflows using the old Start inputs must recreate Start.
2026-10-13 09:18:50 +11:00

171 lines
7.3 KiB
JavaScript

import { app } from "/scripts/app.js";
// Distributed Image List Start declares value_1..value_10 outputs in Python so
// saved links and prompt output indexes stay valid. ComfyUI grows the matching
// `values.value_N` inputs natively (Autogrow) but has no growable outputs, so
// this extension only presents them: it shows outputs for connected inputs,
// copies each input's type onto its output, and moves output links when
// Autogrow compacts inputs after a middle input is disconnected.
// If this script fails, the node still runs; all outputs simply stay visible.
const START_CLASS = "DistributedImageListStart";
const FIXED_OUTPUTS = 4; // image, mask, item_index, context
const MAX_VALUES = 10;
const INPUT_PREFIX = "values.value_";
function valueOrdinal(name) {
if (typeof name !== "string" || !name.startsWith(INPUT_PREFIX)) return null;
const ordinal = Number.parseInt(name.slice(INPUT_PREFIX.length), 10);
return ordinal >= 1 && ordinal <= MAX_VALUES ? ordinal : null;
}
function getLink(graph, id) {
if (id == null || !graph) return undefined;
if (typeof graph.getLink === "function") return graph.getLink(id) ?? undefined;
const links = graph.links;
return (typeof links?.get === "function" ? links.get(id) : links?.[id]) ?? undefined;
}
function connectedValues(node) {
const result = new Map();
for (const input of node.inputs ?? []) {
const ordinal = valueOrdinal(input.name);
if (!ordinal) continue;
const link = getLink(node.graph, input.link);
if (!link) continue;
const origin = node.graph.getNodeById?.(link.origin_id);
const originType = origin?.outputs?.[link.origin_slot]?.type;
const type = originType && originType !== "*" ? originType : (link.type ?? "*");
result.set(ordinal, { type, key: `${link.origin_id}:${link.origin_slot}` });
}
return result;
}
function outputLinks(node, ordinal) {
return node.outputs?.[FIXED_OUTPUTS + ordinal - 1]?.links ?? [];
}
function typesCompatible(outputType, inputType) {
if (!outputType || !inputType || outputType === "*" || inputType === "*") return true;
const accepted = String(inputType).split(",");
return String(outputType).split(",").some(type => accepted.includes(type));
}
function moveOutputLinks(node, from, to) {
const graph = node.graph;
const targets = outputLinks(node, from)
.map(id => getLink(graph, id))
.filter(Boolean)
.map(link => [graph.getNodeById?.(link.target_id), link.target_slot])
.filter(([target]) => target);
// The moved links carry the same value, so give the destination output its
// type first; LiteGraph refuses connections to a mismatched output type.
node.outputs[FIXED_OUTPUTS + to - 1].type = node.outputs[FIXED_OUTPUTS + from - 1].type;
// Connecting a target input replaces its existing link from `from`.
for (const [target, slot] of targets) node.connect(FIXED_OUTPUTS + to - 1, target, slot);
}
function disconnectValueOutput(node, ordinal) {
if (outputLinks(node, ordinal).length) node.disconnectOutput(FIXED_OUTPUTS + ordinal - 1);
}
// Autogrow removes a disconnected middle input and shifts later input links
// down by one. Shift the matching outputs' links the same way, and drop the
// removed value's downstream links, so no consumer silently receives a
// different value.
function followCompactedInputs(node, current) {
const previous = node._distributedValueSources;
if (!previous) return;
const now = [];
for (const [ordinal, info] of current) now[ordinal - 1] = info.key;
const before = previous;
if (now.length !== before.length - 1) return;
let removed = 0;
while (removed < now.length && now[removed] === before[removed]) removed++;
// Removing the last value shifts nothing; keep its links so a temporary
// rewire does not silently drop downstream connections.
if (removed >= now.length) return;
for (let i = removed; i < now.length; i++) {
if (now[i] !== before[i + 1]) return; // Not a single Autogrow compaction.
}
disconnectValueOutput(node, removed + 1);
for (let i = removed + 1; i < before.length; i++) moveOutputLinks(node, i + 1, i);
}
function syncImageListStartOutputs(node) {
if (!node?.graph || !Array.isArray(node.outputs) || node.outputs.length < FIXED_OUTPUTS) return;
const current = connectedValues(node);
followCompactedInputs(node, current);
const sources = [];
for (const [ordinal, info] of current) sources[ordinal - 1] = info.key;
node._distributedValueSources = Array.from(sources);
let visible = Math.max(0, ...current.keys());
for (let ordinal = MAX_VALUES; ordinal > visible; ordinal--) {
// Never drop a wired output; its links keep it visible until removed.
if (outputLinks(node, ordinal).length) visible = ordinal;
}
const listShape = node.outputs[0]?.shape;
while (node.outputs.length < FIXED_OUTPUTS + visible) {
const ordinal = node.outputs.length - FIXED_OUTPUTS + 1;
node.addOutput(`value_${ordinal}`, "*", listShape === undefined ? undefined : { shape: listShape });
}
while (node.outputs.length > FIXED_OUTPUTS + visible) node.removeOutput(node.outputs.length - 1);
for (let ordinal = 1; ordinal <= visible; ordinal++) {
const output = node.outputs[FIXED_OUTPUTS + ordinal - 1];
const type = current.get(ordinal)?.type ?? "*";
output.name = `value_${ordinal}`;
if (output.type !== type) {
output.type = type;
for (const id of [...(output.links ?? [])]) {
const link = getLink(node.graph, id);
if (!link) continue;
const target = node.graph.getNodeById?.(link.target_id);
const targetType = target?.inputs?.[link.target_slot]?.type;
if (target && !typesCompatible(type, targetType)) target.disconnectInput(link.target_slot);
else link.type = type;
}
}
}
node.setDirtyCanvas?.(true, true);
}
function scheduleSync(node) {
if (node._distributedValueSyncPending) return;
node._distributedValueSyncPending = true;
const run = () => {
node._distributedValueSyncPending = false;
try {
syncImageListStartOutputs(node);
} catch (error) {
console.error("[Distributed] Image List Start output sync failed:", error);
}
};
// Run after Autogrow's own animation-frame input compaction.
if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => setTimeout(run, 0));
else setTimeout(run, 0);
}
app.registerExtension({
name: "Distributed.ImageListStartValues",
nodeCreated(node) {
if (node.comfyClass !== START_CLASS) return;
const onConnectionsChange = node.onConnectionsChange;
node.onConnectionsChange = function (...args) {
const result = onConnectionsChange?.apply(this, args);
scheduleSync(this);
return result;
};
const onConfigure = node.onConfigure;
node.onConfigure = function (...args) {
const result = onConfigure?.apply(this, args);
// A loaded graph is the source of truth for prior wiring.
this._distributedValueSources = undefined;
scheduleSync(this);
return result;
};
scheduleSync(node);
},
});