Files

616 lines
19 KiB
JavaScript

// bv_pipe_shared.js
// Registered presentation exception: dynamic-pipe-slot-structure.
// See PRESENTATION_EXCEPTIONS in ui/src/regional/nodePresentation.ts.
import { app } from "../../scripts/app.js";
export const MAX_SLOTS = 100;
export const RESOLVE_RETRIES = 10;
export const RESOLVE_DELAY_MS = 40;
export function defer(fn) {
requestAnimationFrame(() => requestAnimationFrame(fn));
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
function getGraphFromNode(node) {
return node?.graph || app.graph;
}
/**
* Root graph capture:
* used for scanning container/subgraphs when no parent pointers exist.
*/
const ROOT_GRAPH =
window.__BV_PIPE_ROOT_GRAPH || (window.__BV_PIPE_ROOT_GRAPH = app.graph);
// Cache: innerGraph -> { parentNode, parentGraph }
const PARENT_CACHE = new WeakMap();
// GraphId cache (critical): graphs need stable unique IDs for visited keys
const GRAPH_ID = new WeakMap();
let GRAPH_ID_SEQ = 1;
function getGraphId(graph) {
if (!graph || typeof graph !== "object") return 0;
let id = GRAPH_ID.get(graph);
if (!id) {
id = GRAPH_ID_SEQ++;
GRAPH_ID.set(graph, id);
}
return id;
}
function getAllNodes(graph) {
return graph?._nodes || graph?.nodes || [];
}
// ---------- parsing ----------
export function parseNames(raw) {
return (raw || "")
.replaceAll(",", "\n")
.split("\n")
.map((s) => s.trim())
.filter(Boolean)
.slice(0, MAX_SLOTS);
}
export function getConfigFromConfigNode(cfgNode) {
const namesW = cfgNode.widgets?.find((w) => w.name === "names");
const namesRaw = namesW?.value ?? "";
const names = parseNames(namesRaw);
return { namesRaw, names, count: names.length };
}
// ---------- graph/subgraph utilities (exported global refresh uses these) ----------
export function isGraphLike(obj) {
if (!obj || typeof obj !== "object") return false;
const hasNodes = Array.isArray(obj._nodes) || Array.isArray(obj.nodes);
const hasLinks = obj.links && typeof obj.links === "object";
const hasGetNodeById = typeof obj.getNodeById === "function";
return hasNodes && hasLinks && hasGetNodeById;
}
function collectGraphsDeep(root, maxDepth = 3) {
const found = new Set();
const seen = new Set();
function walk(v, depth) {
if (!v || depth > maxDepth) return;
if (typeof v !== "object") return;
if (seen.has(v)) return;
seen.add(v);
if (isGraphLike(v)) found.add(v);
if (Array.isArray(v)) {
for (const x of v) walk(x, depth + 1);
return;
}
for (const k of Object.keys(v)) {
try {
walk(v[k], depth + 1);
} catch (e) {}
}
}
walk(root, 0);
return found;
}
function getInnerGraphFromNode(node) {
if (!node) return null;
// Skip BV nodes to avoid false positives from internal properties
if (node.comfyClass === "BV Pipe" || node.comfyClass === "BV Pipe Config") {
return null;
}
const direct =
node.subgraph ||
node._subgraph ||
node.inner_graph ||
node.graph_inside ||
node.subgraph_graph ||
node.subgraphGraph ||
(typeof node.getInnerGraph === "function" ? node.getInnerGraph() : null);
if (direct && isGraphLike(direct)) return direct;
// Check properties or widgets that might hold a graph reference
if (node.properties?.subgraph && isGraphLike(node.properties.subgraph)) return node.properties.subgraph;
// Only perform deep search for nodes that are likely to be containers
const possibleContainers = ["Subgraph", "Workflow", "Group", "GroupNode", "Subflow", "Recursive"];
const isLikelyContainer = possibleContainers.some(c => node.comfyClass?.includes(c)) || node.getInnerGraph;
if (isLikelyContainer) {
const graphs = collectGraphsDeep({ node, props: node?.properties }, 3);
for (const g of graphs) return g;
}
return null;
}
function findParentByScanning(rootGraph, targetSubGraph) {
const visitedGraphs = new Set();
const queue = [rootGraph];
while (queue.length) {
const g = queue.shift();
if (!g || visitedGraphs.has(g)) continue;
visitedGraphs.add(g);
for (const n of getAllNodes(g)) {
const inner = getInnerGraphFromNode(n);
if (inner) {
if (inner === targetSubGraph) {
return { parentNode: n, parentGraph: g };
}
queue.push(inner);
}
}
}
return null;
}
function getParentRefForSubGraph(subGraph) {
if (!subGraph) return null;
const cached = PARENT_CACHE.get(subGraph);
if (cached) return cached;
const candidates = [ROOT_GRAPH, app.graph].filter(Boolean);
for (const start of candidates) {
const found = findParentByScanning(start, subGraph);
if (found) {
PARENT_CACHE.set(subGraph, found);
return found;
}
}
return null;
}
/**
* Exported: refresh ALL BV Pipe nodes across the whole workflow,
* including nested subgraphs.
*/
export function refreshAllPipesEverywhere(startGraph) {
const root = (startGraph && isGraphLike(startGraph)) ? startGraph : ROOT_GRAPH || app.graph;
if (!root) return;
const visitedGraphs = new Set();
const queue = [root];
while (queue.length) {
const g = queue.shift();
if (!g || visitedGraphs.has(g)) continue;
visitedGraphs.add(g);
const nodes = getAllNodes(g);
// refresh BV Pipe in this graph
for (const n of nodes) {
if (n?.comfyClass === "BV Pipe") {
refreshPipeNodeFromUpstream(n);
}
}
// enqueue nested subgraphs
for (const n of nodes) {
const inner = getInnerGraphFromNode(n);
if (inner && !visitedGraphs.has(inner)) queue.push(inner);
}
}
}
// ---------- link helpers ----------
function getUpstreamByInput(node, inputIndex) {
const graph = getGraphFromNode(node);
const inp = node.inputs?.[inputIndex];
if (!inp?.link) return null;
const link = graph.links?.[inp.link];
if (!link) return null;
const originNode = graph.getNodeById(link.origin_id) || null;
return {
graph,
link,
originNode,
originSlot: Number.isFinite(link.origin_slot) ? link.origin_slot : 0,
};
}
function jumpFromSubgraphGraphInputToParentOrigin(subGraph, subInputIndex) {
const parentRef = getParentRefForSubGraph(subGraph);
if (!parentRef) return null;
const { parentNode, parentGraph } = parentRef;
// Search for the input that corresponds to the given index or is named "pipe"
const parentInp =
parentNode.inputs?.find((i, idx) => i?.name === "pipe" || i?.label === "pipe" || idx === subInputIndex) ||
parentNode.inputs?.[0];
if (!parentInp?.link) return null;
const parentLink = parentGraph.links?.[parentInp.link];
if (!parentLink) return null;
const origin = parentGraph.getNodeById(parentLink.origin_id);
if (!origin) return null;
return { node: origin, graph: parentGraph };
}
function jumpFromParentToSubgraphInput(parentNode, parentSlotIndex) {
const innerGraph = getInnerGraphFromNode(parentNode);
if (!innerGraph) return null;
const allNodes = getAllNodes(innerGraph);
const inputNodeClasses = [
"SubgraphInput", "PrimitiveInput", "InputNode", "GraphInput", "Input", "GroupInput",
"InternalInput", "ProxyInput", "CustomNodeInput", "WorkflowInput", "PipeInput", "SubflowInput"
];
const inputNodes = allNodes.filter((n) => inputNodeClasses.includes(n.comfyClass) || n.comfyClass?.toLowerCase().includes("input"));
// Try to find by property 'index' or widget value
let targetNode = inputNodes.find(
(n) => n.properties?.index === parentSlotIndex ||
n.widgets?.find((w) => (w.name === "index" || w.name === "input_index") && w.value === parentSlotIndex)
);
// Fallback: try by name/label match
if (!targetNode && parentNode.inputs?.[parentSlotIndex]) {
const parentInpName = parentNode.inputs[parentSlotIndex].name?.toLowerCase();
targetNode = inputNodes.find(n => n.title?.toLowerCase() === parentInpName || n.widgets?.find(w => w.name === "name" && w.value?.toLowerCase() === parentInpName));
}
if (!targetNode && parentSlotIndex === 0) {
targetNode = inputNodes[0];
}
// Fallback: if we found NO input node but there are BV Pipes inside, maybe it's a direct mapping?
if (!targetNode && inputNodes.length === 0) {
const bvPipesInside = allNodes.filter(n => n.comfyClass === "BV Pipe");
if (bvPipesInside.length === 1) {
targetNode = bvPipesInside[0];
}
}
return targetNode ? { node: targetNode, graph: innerGraph } : null;
}
function jumpFromSubgraphOutputToParent(subGraph, subOutputIndex, outputNode) {
const parentRef = getParentRefForSubGraph(subGraph);
if (!parentRef) return null;
const { parentNode, parentGraph } = parentRef;
let slotIdx = 0;
if (outputNode && outputNode.properties && typeof outputNode.properties.index === "number") {
slotIdx = outputNode.properties.index;
} else if (outputNode && outputNode.widgets) {
const indexW = outputNode.widgets.find(w => w.name === "index" || w.name === "output_index");
if (indexW && typeof indexW.value === "number") {
slotIdx = indexW.value;
}
} else {
const allNodes = getAllNodes(subGraph);
const outClasses = [
"SubgraphOutput", "OutputNode", "GraphOutput", "Output", "GroupOutput",
"InternalOutput", "ProxyOutput", "CustomNodeOutput", "WorkflowOutput", "PipeOutput", "SubflowOutput"
];
const outputNodes = allNodes.filter(n => outClasses.includes(n.comfyClass) || n.comfyClass?.toLowerCase().includes("output")).sort((a, b) => a.id - b.id);
const foundIdx = outputNodes.indexOf(outputNode);
if (foundIdx !== -1) {
slotIdx = foundIdx;
} else {
slotIdx = Number.isFinite(subOutputIndex) ? subOutputIndex : 0;
}
}
return { node: parentNode, graph: parentGraph, slot: slotIdx };
}
function jumpFromParentToSubgraphOutputOrigin(parentNode, parentSlotIndex) {
const innerGraph = getInnerGraphFromNode(parentNode);
if (!innerGraph) return null;
const allNodes = getAllNodes(innerGraph);
const outClasses = [
"SubgraphOutput", "OutputNode", "GraphOutput", "Output", "GroupOutput",
"InternalOutput", "ProxyOutput", "CustomNodeOutput", "WorkflowOutput", "PipeOutput", "SubflowOutput"
];
const outputNodes = allNodes.filter(n => outClasses.includes(n.comfyClass) || n.comfyClass?.toLowerCase().includes("output"));
let targetNode = outputNodes.find(
(n) => n.properties?.index === parentSlotIndex ||
n.widgets?.find((w) => (w.name === "index" || w.name === "output_index") && w.value === parentSlotIndex)
);
// Fallback: try by name/label match
if (!targetNode && parentNode.outputs?.[parentSlotIndex]) {
const parentOutName = parentNode.outputs[parentSlotIndex].name?.toLowerCase();
targetNode = outputNodes.find(n => n.title?.toLowerCase() === parentOutName || n.widgets?.find(w => w.name === "name" && w.value?.toLowerCase() === parentOutName));
}
if (!targetNode && parentSlotIndex === 0) {
targetNode = outputNodes[0];
}
if (targetNode) {
// We found the internal Output node, but we need to find what's connected to its INPUT (upstream)
const up = getUpstreamByInput(targetNode, 0);
if (up && up.originNode) {
return { node: up.originNode, graph: innerGraph };
}
}
return null;
}
function fallbackDirectMappedCfgFromInnerGraph(parentNode) {
const innerGraph = getInnerGraphFromNode(parentNode);
if (!innerGraph) return null;
const nodes = getAllNodes(innerGraph);
// Your observed case: subgraph contains ONLY the BV Pipe Config node
if (nodes.length === 1 && nodes[0]?.comfyClass === "BV Pipe Config") {
return { node: nodes[0], graph: innerGraph };
}
// Slightly more permissive fallback: if there is exactly one config node inside, use it
const cfgNodes = nodes.filter(n => n?.comfyClass === "BV Pipe Config");
if (cfgNodes.length === 1) {
return { node: cfgNodes[0], graph: innerGraph };
}
return null;
}
// ---------- config resolution ----------
export function resolveCfgByPipeLinksNoCache(node, visited = new Set()) {
if (!node) return null;
const g = getGraphFromNode(node);
const vkey = `${getGraphId(g)}:${node.id}`;
if (visited.has(vkey)) return null;
visited.add(vkey);
if (node.comfyClass === "BV Pipe Config") {
return getConfigFromConfigNode(node);
}
const up = getUpstreamByInput(node, 0);
if (!up) {
// Check if this node itself is inside a subgraph and might be a "direct mapped" input
const parentRef = getParentRefForSubGraph(g);
if (parentRef) {
// If we are at the "entry" of a subgraph but found no link, try to look at the parent
return resolveCfgByPipeLinksNoCache(parentRef.parentNode, visited);
}
return null;
}
if (up.originNode) {
// If originNode is a Subgraph/container, jump into it to find the source of the config
const inner = getInnerGraphFromNode(up.originNode);
if (inner) {
const jumped = jumpFromParentToSubgraphOutputOrigin(up.originNode, up.originSlot);
if (jumped) {
return resolveCfgByPipeLinksNoCache(jumped.node, visited);
}
// ✅ Fallback for "direct mapped" subgraphs (like your 1-node BV Pipe Config subgraph)
const direct = fallbackDirectMappedCfgFromInnerGraph(up.originNode);
if (direct) {
return resolveCfgByPipeLinksNoCache(direct.node, visited);
}
}
return resolveCfgByPipeLinksNoCache(up.originNode, visited);
}
// subgraph graph-input case (origin_id negative -> no originNode)
const jumped = jumpFromSubgraphGraphInputToParentOrigin(up.graph, up.originSlot);
if (!jumped) {
// Fallback: maybe the link just points outside the current graph scope?
const parentRef = getParentRefForSubGraph(up.graph);
if (parentRef) {
return resolveCfgByPipeLinksNoCache(parentRef.parentNode, visited);
}
return null;
}
return resolveCfgByPipeLinksNoCache(jumped.node, visited);
}
export function getCfgFromNodeWithFallback(node) {
return resolveCfgByPipeLinksNoCache(node) || node.__pipe_cfg || null;
}
// ---------- safe remove helpers ----------
function removeInputSafely(node, index) {
const graph = getGraphFromNode(node);
const inp = node.inputs?.[index];
if (!inp) return;
const linkId = inp.link;
if (linkId != null) {
try { node.disconnectInput(index); } catch (e) {}
if (graph.links?.[linkId]) {
try { graph.removeLink(linkId); } catch (e) {}
}
}
try { node.removeInput(index); } catch (e) {}
}
function removeOutputSafely(node, index) {
const graph = getGraphFromNode(node);
const out = node.outputs?.[index];
if (!out) return;
const links = out.links ? [...out.links] : [];
if (links.length) {
for (const linkId of links) {
if (graph.links?.[linkId]) {
try { graph.removeLink(linkId); } catch (e) {}
}
}
try { node.disconnectOutput(index); } catch (e) {}
}
try { node.removeOutput(index); } catch (e) {}
}
// ---------- slot update ----------
function ensureInputs(pipeNode, count, labels) {
const targetLen = 1 + count; // inputs[0] = pipe
while ((pipeNode.inputs?.length || 0) > targetLen) {
removeInputSafely(pipeNode, pipeNode.inputs.length - 1);
}
while ((pipeNode.inputs?.length || 0) < targetLen) {
const slot = pipeNode.inputs.length; // 1..count
const key = `v_${String(slot).padStart(3, "0")}`;
const label = labels[slot - 1] ?? key;
pipeNode.addInput(key, "*", { label, nameLocked: true });
}
for (let i = 1; i <= count; i++) {
const inp = pipeNode.inputs?.[i];
if (inp) inp.label = labels[i - 1] ?? inp.name;
}
}
function ensureOutputs(pipeNode, count, labels) {
const targetLen = 1 + count; // outputs[0] = pipe
while ((pipeNode.outputs?.length || 0) > targetLen) {
removeOutputSafely(pipeNode, pipeNode.outputs.length - 1);
}
while ((pipeNode.outputs?.length || 0) < targetLen) {
const slot = pipeNode.outputs.length; // 1..count
const key = `out_${String(slot).padStart(3, "0")}`;
const label = labels[slot - 1] ?? key;
pipeNode.addOutput(key, "*", { label, nameLocked: true });
}
for (let i = 1; i <= count; i++) {
const out = pipeNode.outputs?.[i];
if (out) out.label = labels[i - 1] ?? out.name;
}
}
export function updatePipeNodeUI(pipeNode, cfg) {
if (!cfg) return;
const count = Math.max(0, Math.min(MAX_SLOTS, Number(cfg.count || 0)));
const labels = (cfg.names || []).slice(0, count);
pipeNode.__pipe_cfg = { namesRaw: cfg.namesRaw ?? "", names: labels, count };
ensureInputs(pipeNode, count, labels);
ensureOutputs(pipeNode, count, labels);
const presentation = globalThis.__bvNodePresentationBridge;
if (presentation?.applyClassic) presentation.applyClassic(pipeNode, "BV Pipe");
else try {
if (pipeNode.computeSize) pipeNode.setSize(pipeNode.computeSize());
} catch (e) {}
pipeNode.setDirtyCanvas(true, true);
}
// ---------- propagation (same-graph only) ----------
export function propagateFrom(startNode) {
const visited = new Set();
const startCfg = getCfgFromNodeWithFallback(startNode);
const queue = [{ node: startNode, graph: getGraphFromNode(startNode), outputSlot: -1, cfg: startCfg }];
while (queue.length) {
const { node: cur, graph, outputSlot, cfg: curCfg } = queue.shift();
if (!cur) continue;
const vkey = `${getGraphId(graph)}:${cur.id}${outputSlot >= 0 ? ":" + outputSlot : ""}`;
if (visited.has(vkey)) continue;
visited.add(vkey);
const cfg = curCfg || getCfgFromNodeWithFallback(cur);
const slotsToProcess = outputSlot >= 0 ? [outputSlot] : [0];
for (const slotIdx of slotsToProcess) {
const out = cur.outputs?.[slotIdx];
const links = out?.links ? [...out.links] : [];
if (!links.length) continue;
for (const linkId of links) {
const link = graph.links?.[linkId];
if (!link) continue;
let target = graph.getNodeById(link.target_id);
if (!target || target.comfyClass === "SubgraphOutput" || link.target_id < 0) {
const jumped = jumpFromSubgraphOutputToParent(graph, link.origin_slot, target);
if (jumped) {
queue.push({ node: jumped.node, graph: jumped.graph, outputSlot: jumped.slot, cfg });
continue;
}
}
if (!target) continue;
// If target is a subgraph node, jump into it
const inner = getInnerGraphFromNode(target);
if (inner) {
const jumpedIn = jumpFromParentToSubgraphInput(target, link.target_slot);
if (jumpedIn) {
// CRITICAL FIX: Update the internal node immediately when jumping in
if (cfg && jumpedIn.node.comfyClass === "BV Pipe") {
updatePipeNodeUI(jumpedIn.node, cfg);
}
queue.push({ node: jumpedIn.node, graph: jumpedIn.graph, outputSlot: -1, cfg });
}
}
if (cfg && target.comfyClass === "BV Pipe") {
updatePipeNodeUI(target, cfg);
}
queue.push({ node: target, graph: graph, outputSlot: -1, cfg });
}
}
}
}
export async function refreshPipeNodeFromUpstream(pipeNode) {
for (let attempt = 0; attempt < RESOLVE_RETRIES; attempt++) {
const cfg = resolveCfgByPipeLinksNoCache(pipeNode);
if (cfg) {
updatePipeNodeUI(pipeNode, cfg);
propagateFrom(pipeNode);
return;
}
await sleep(RESOLVE_DELAY_MS);
}
}