A workflow that switches Mask by Touch off, for example to find a thing by its name instead, showed ComfyUI's red "Prompt has no outputs" message when a new picture was loaded. The node still asked for its own look at the picture, and ComfyUI leaves a bypassed or muted node out of every run, so that run had nothing left to do. - The node sends no run of its own while it is bypassed or muted, by hand, by a Workflow Switches card, or inside a box that is. - A look it skipped that way is made up when it is on again and the pointer comes to its picture. - A click while it is switched off says so on the node. isSwitchedOff in js/shared/graph_ids.mjs answers for the node and for every box on the way to it, with tests.
73 lines
3.1 KiB
JavaScript
73 lines
3.1 KiB
JavaScript
// Resolving ComfyUI execution ids to graph nodes.
|
|
//
|
|
// Kept free of /scripts/app.js imports so node:test can cover it
|
|
// (tests/graph_ids.test.mjs); callers pass the root graph in.
|
|
|
|
// ComfyUI identifies a node inside a subgraph by its execution id: the chain
|
|
// of subgraph node ids that reaches it, colon-joined ("12:3" is node 3 inside
|
|
// the subgraph node 12 holds). Only the last segment is a real graph node id,
|
|
// and ids are numbered independently inside each subgraph, so looking the
|
|
// bare tail up on the root graph does not merely miss - it can return a
|
|
// completely unrelated top-level node that happens to share the number.
|
|
// Walking the chain is the only way to answer correctly; an id that does not
|
|
// resolve returns null, because acting on the wrong node is worse than doing
|
|
// nothing at all.
|
|
export function nodeByExecutionId(rootGraph, executionId) {
|
|
const parts = String(executionId ?? "")
|
|
.split(":")
|
|
.filter((part) => part !== "");
|
|
if (!parts.length) return null;
|
|
let graph = rootGraph ?? null;
|
|
for (const part of parts.slice(0, -1)) {
|
|
const holder = graph?.getNodeById?.(Number(part)) ?? graph?.getNodeById?.(part);
|
|
graph = holder?.subgraph ?? null;
|
|
if (!graph) return null;
|
|
}
|
|
const tail = parts[parts.length - 1];
|
|
return graph?.getNodeById?.(Number(tail)) ?? graph?.getNodeById?.(tail) ?? null;
|
|
}
|
|
|
|
// The other way round: a node's execution id, found by walking from the root
|
|
// graph down through the boxes (subgraph nodes) that hold it. This is what a
|
|
// run of one node alone has to name. A box that is used twice gives its first
|
|
// use; a node that is on no graph gives null.
|
|
export function executionIdOf(rootGraph, node) {
|
|
if (!rootGraph || !node) return null;
|
|
const walk = (graph, prefix, depth) => {
|
|
const nodes = graph?.nodes ?? graph?._nodes ?? [];
|
|
if (nodes.includes(node)) return `${prefix}${node.id}`;
|
|
if (depth > 16) return null;
|
|
for (const holder of nodes) {
|
|
if (!holder?.subgraph) continue;
|
|
const found = walk(holder.subgraph, `${prefix}${holder.id}:`, depth + 1);
|
|
if (found) return found;
|
|
}
|
|
return null;
|
|
};
|
|
return walk(rootGraph, "", 0);
|
|
}
|
|
|
|
// Whether ComfyUI leaves a node out of every run: it is bypassed or muted
|
|
// (modes 4 and 2), or a box (subgraph node) on the way to it is. Such a node
|
|
// can not be run alone either: a prompt of it has nothing left to run, and
|
|
// ComfyUI refuses it with "Prompt has no outputs". A node that is on no graph
|
|
// counts only its own mode.
|
|
const OFF_MODES = new Set([2, 4]);
|
|
export function isSwitchedOff(rootGraph, node) {
|
|
if (!node) return false;
|
|
if (OFF_MODES.has(node.mode)) return true;
|
|
if (!rootGraph) return false;
|
|
const holdersOf = (graph, depth) => {
|
|
const nodes = graph?.nodes ?? graph?._nodes ?? [];
|
|
if (nodes.includes(node)) return [];
|
|
if (depth > 16) return null;
|
|
for (const holder of nodes) {
|
|
if (!holder?.subgraph) continue;
|
|
const found = holdersOf(holder.subgraph, depth + 1);
|
|
if (found) return [holder, ...found];
|
|
}
|
|
return null;
|
|
};
|
|
return (holdersOf(rootGraph, 0) ?? []).some((holder) => OFF_MODES.has(holder.mode));
|
|
}
|