Files

181 lines
6.9 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { isSubgraphHost, isSubgraphProjectionStale, projectSubgraphUIPresentation, resolveSubgraphWidgetPresentations, subgraphFor, synchronizeDynamicComboHost } from "./bv_subgraph_ui_projection.js";
import { projectSubgraphNodes2Presentation } from "./bv_subgraph_nodes2_projection.js";
import { projectSeedControls } from "./bv_seed.js";
const UI_CLASSES = new Set(["BV Subgraph Heading", "BV Subgraph Spacer", "BV Subgraph Divider", "BV Dynamic Combo"]);
let reconcileFrame = 0;
let domObserver;
const MAX_PROXY_STABILIZATION_FRAMES = 12;
function id() {
return globalThis.crypto?.randomUUID?.() ?? `bv-layout-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
function ensureIdentity(node) {
node.properties ??= {};
const nodes = node.graph?._nodes || node.graph?.nodes || [];
const duplicate = nodes.some((candidate) => candidate !== node && candidate.properties?.bvLayoutId === node.properties.bvLayoutId);
if (!node.properties.bvLayoutId || duplicate) node.properties.bvLayoutId = id();
if (!Number.isFinite(node.properties.bvLayoutOrder)) {
const orders = nodes.map((candidate) => candidate.properties?.bvLayoutOrder).filter(Number.isFinite);
node.properties.bvLayoutOrder = orders.length ? Math.max(...orders) + 1 : 0;
}
}
function refreshHost(host) {
const project = () => {
projectSubgraphUIPresentation(host);
projectSeedControls(host);
projectSubgraphNodes2Presentation(host, document, app.canvas);
};
queueMicrotask(() => {
let previousTargets = [];
let stableFrames = 0;
let remaining = MAX_PROXY_STABILIZATION_FRAMES;
const stabilize = () => {
project();
const presentations = resolveSubgraphWidgetPresentations(host);
const targets = presentations.map(({ target }) => target).filter(Boolean);
const sameTargets = targets.length > 0
&& targets.length === previousTargets.length
&& targets.every((target, index) => target === previousTargets[index]);
const projected = presentations.length > 0 && presentations.every(({ source, target, presentationType }) =>
target?.__bvPresentationType === presentationType && target?.__bvProjectedFrom === source);
stableFrames = sameTargets && projected ? stableFrames + 1 : 0;
previousTargets = targets;
remaining--;
if (stableFrames < 2 && remaining > 0) requestAnimationFrame(stabilize);
};
requestAnimationFrame(stabilize);
});
}
function attachHost(host) {
if (!isSubgraphHost(host)) return;
if (host.__bvSubgraphUIPresentationAttached) {
refreshHost(host);
return;
}
host.__bvSubgraphUIPresentationAttached = true;
const refresh = () => refreshHost(host);
const originalDrawForeground = host.onDrawForeground;
host.onDrawForeground = function () {
const result = originalDrawForeground?.apply(this, arguments);
if (!this.__bvSubgraphProjectionRefreshPending) {
if (isSubgraphProjectionStale(this)) {
this.__bvSubgraphProjectionRefreshPending = true;
queueMicrotask(() => {
try {
projectSubgraphUIPresentation(this);
} finally {
this.__bvSubgraphProjectionRefreshPending = false;
}
});
}
}
return result;
};
for (const eventName of ["widget-promoted", "widget-demoted", "input-added", "removing-input", "configured"]) {
subgraphFor(host)?.events?.addEventListener?.(eventName, refresh);
}
refreshHost(host);
}
function attachGraph(graph, visited = new Set()) {
if (!graph || visited.has(graph)) return;
visited.add(graph);
for (const node of graph._nodes || graph.nodes || []) {
if (!isSubgraphHost(node)) continue;
attachHost(node);
attachGraph(subgraphFor(node), visited);
}
}
function reconcileNodes2Graph(graph, visited = new Set()) {
if (!graph || visited.has(graph)) return;
visited.add(graph);
for (const node of graph._nodes || graph.nodes || []) {
if (!isSubgraphHost(node)) continue;
projectSubgraphNodes2Presentation(node, document, app.canvas);
reconcileNodes2Graph(subgraphFor(node), visited);
}
}
function reconcileVisibleHosts() {
const graph = app.canvas?.graph;
if (graph) reconcileNodes2Graph(graph);
}
function scheduleVisibleHostReconcile() {
if (reconcileFrame) return;
reconcileFrame = requestAnimationFrame(() => {
reconcileFrame = 0;
reconcileVisibleHosts();
});
}
function synchronizeDynamicComboGraph(graph, sourceNode, values, visited = new Set()) {
if (!graph || visited.has(graph)) return;
visited.add(graph);
for (const node of graph._nodes || graph.nodes || []) {
if (!isSubgraphHost(node)) continue;
synchronizeDynamicComboHost(node, sourceNode, values);
synchronizeDynamicComboGraph(subgraphFor(node), sourceNode, values, visited);
}
}
app.registerExtension({
name: "bv_nodepack.subgraph_layout_identity",
async nodeCreated(node) {
if (UI_CLASSES.has(node.comfyClass)) ensureIdentity(node);
if (isSubgraphHost(node)) attachHost(node);
},
setup() {
// setup() runs before app.graph is guaranteed to exist. Accessing the
// throwing getter here aborts the extension and prevents every listener
// below from being installed.
app.canvas?.addEventListener?.("litegraph:set-graph", (event) => attachGraph((event.detail || event).newGraph));
app.canvas?.addEventListener?.("subgraph-opened", (event) => {
const detail = event.detail || event;
attachHost(detail.fromNode);
attachGraph(detail.subgraph);
});
window.addEventListener("bv-dynamic-combo-options-changed", (event) => {
const { sourceNode, values } = event.detail || {};
const rootGraph = sourceNode?.graph?.rootGraph;
if (sourceNode && Array.isArray(values)) synchronizeDynamicComboGraph(rootGraph, sourceNode, values);
});
if (!domObserver && document.body) {
domObserver = new MutationObserver(scheduleVisibleHostReconcile);
domObserver.observe(document.body, { childList: true, subtree: true });
}
// Workflow tabs can replace the active graph without configuring it again.
// Keep a small fallback around the official canvas method as well as the
// event above so already-loaded tab graphs are projected immediately.
const canvas = app.canvas;
if (canvas?.setGraph && !canvas.__bvSubgraphSetGraphWrapped) {
canvas.__bvSubgraphSetGraphWrapped = true;
const originalSetGraph = canvas.setGraph;
canvas.setGraph = function (graph) {
const result = originalSetGraph.apply(this, arguments);
attachGraph(graph);
return result;
};
}
},
afterConfigureGraph() {
attachGraph(app.graph);
},
async beforeRegisterNodeDef(nodeType, nodeData) {
if (!UI_CLASSES.has(nodeData.name)) return;
const originalConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
const result = originalConfigure?.apply(this, arguments);
ensureIdentity(this);
return result;
};
},
});