Files
chrmc40 8e3dceae99 ComfyUI-PromptChain
A prompt-authoring and image-iteration suite for ComfyUI.
2026-06-17 19:35:25 -07:00

152 lines
4.6 KiB
JavaScript

// Tree utilities — pure functions for building the PromptChain network tree.
// Extracted from fullscreen-editor.js for use by both imperative and Svelte code.
// These depend on graph state (app, slot-utils, label-utils) so they stay in js/lib/.
import { app } from "../../../scripts/app.js";
import { isPromptChain, getSourceNode, getConnectedInputs, hasCustomTitle, getLinkInfo } from "./slot-utils.js";
import { getSelfLabelOptions, getNodePromptContent } from "./label-utils.js";
export function findRoot(node) {
let current = node;
const visited = new Set();
while (current) {
if (visited.has(current.id)) break;
visited.add(current.id);
let parent = null;
for (const output of current.outputs || []) {
for (const linkId of output.links || []) {
const link = getLinkInfo(linkId);
if (!link) continue;
const target = app.graph?.getNodeById?.(link.target_id);
if (target && isPromptChain(target)) {
parent = target;
break;
}
}
if (parent) break;
}
if (!parent) break;
current = parent;
}
return current;
}
export function buildTree(node, ancestors = new Set()) {
if (!node || ancestors.has(node.id)) return null;
ancestors.add(node.id);
const children = [];
const inputs = getConnectedInputs(node);
inputs.sort((a, b) => {
const ai = parseInt(a.name.replace("inputs.", "").split("_")[1]);
const bi = parseInt(b.name.replace("inputs.", "").split("_")[1]);
return ai - bi;
});
for (const slot of inputs) {
const source = getSourceNode(node, slot);
if (source && isPromptChain(source)) {
const child = buildTree(source, ancestors);
if (child) children.push(child);
}
}
ancestors.delete(node.id);
const labels = children.length === 0 ? getSelfLabelOptions(node) : [];
const wildcards = [];
if (children.length === 0) {
const content = getNodePromptContent(node);
if (content) {
const wcRegex = /__([a-zA-Z0-9_/.-]+)__/g;
const wcModes = node.properties?.pcrWildcardModes || {};
const wcResults = node.properties?.pcrWildcardResults || {};
const seen = new Set();
let m;
while ((m = wcRegex.exec(content)) !== null) {
const name = m[1];
if (!seen.has(name)) {
seen.add(name);
const wm = wcModes[name] || {};
const wr = wcResults[name] || {};
wildcards.push({
name,
mode: wm.mode || "randomize",
index: wm.index || 0,
label: wm.label || "",
rolledLabel: wr.label || "",
});
}
}
}
}
const props = node.properties || {};
return {
node,
children,
labels,
wildcards,
title: hasCustomTitle(node) ? node.title : "PromptChain",
hasChildren: children.length > 0 || labels.length > 0 || wildcards.length > 0,
// snapshot for Svelte reactivity — tree objects are new each rebuild,
// but node is the same LGraphNode reference, so $derived(node.properties.*)
// won't detect changes. Reading from tree.* instead forces re-evaluation.
mode: props.pcrMode || "switch",
switchIndex: props.pcrSwitchIndex ?? 1,
locked: !!props.pcrLocked,
disabled: !!props.pcrDisabled,
collapsed: !!props.pcrTreeCollapsed,
};
}
export function flattenTree(tree, depth = 0, parentNode = null) {
if (!tree) return [];
const result = [{ ...tree, depth, parentNode }];
for (const child of tree.children) {
result.push(...flattenTree(child, depth + 1, tree.node));
}
return result;
}
export function findAllRoots() {
const seen = new Set();
const roots = [];
for (const node of app.graph?._nodes || []) {
if (!isPromptChain(node)) continue;
const root = findRoot(node);
if (!seen.has(root.id)) {
seen.add(root.id);
roots.push(root);
}
}
return roots;
}
export function stampInactive(tree) {
function walk(treeNode, inherited) {
treeNode._inactive = inherited;
const mode = treeNode.node.properties?.pcrMode || "switch";
const switchIndex = treeNode.node.properties?.pcrSwitchIndex ?? 1;
for (let i = 0; i < treeNode.children.length; i++) {
const childIndex = i + 1;
let childInactive = inherited;
if (!inherited && mode === "switch" && treeNode.children.length > 1) {
childInactive = switchIndex === 0 || childIndex !== switchIndex;
}
walk(treeNode.children[i], childInactive);
}
}
if (tree) walk(tree, false);
}
export function isDescendant(tree, targetId) {
if (tree.node.id === targetId) return true;
for (const child of tree.children) {
if (isDescendant(child, targetId)) return true;
}
return false;
}