152 lines
4.6 KiB
JavaScript
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;
|
|
}
|