|
import { app } from "../../scripts/app.js"; |
|
import { api } from "../../scripts/api.js"; |
|
import { NestedNode, serializeWorkflow } from "./nestedNode.js"; |
|
import { ComfirmDialog, showWidgetDialog } from "./dialog.js"; |
|
import { Queue } from "./queue.js"; |
|
|
|
export let nodeDefs = {}; |
|
|
|
export const ext = { |
|
name: "SS.NestedNodeBuilder", |
|
nestedDef: {}, |
|
nestedNodeDefs: {}, |
|
comfirmationDialog: new ComfirmDialog(), |
|
nestedPromptQueue: new Queue([null]), |
|
|
|
async setup(app) { |
|
|
|
const originalAppQueuePrompt = app.queuePrompt; |
|
app.queuePrompt = async function (number, batchsize) { |
|
|
|
const nestedWorkflow = structuredClone(app.graph.serialize()); |
|
|
|
|
|
const nestedNodesUnnested = {}; |
|
const nestedNodes = {}; |
|
const connectedInputNodes = {}; |
|
const nodes = app.graph._nodes; |
|
for (const i in nodes) { |
|
const node = nodes[i]; |
|
if (node.properties.nestedData) { |
|
node.beforeQueuePrompt(); |
|
nestedNodes[node.id] = node; |
|
connectedInputNodes[node.id] = node.getConnectedInputNodes(); |
|
} |
|
} |
|
|
|
for (const nestedNodeId in nestedNodes) { |
|
const node = nestedNodes[nestedNodeId]; |
|
const unnestedNodes = node.unnest(); |
|
nestedNodesUnnested[node.id] = unnestedNodes; |
|
} |
|
|
|
|
|
const originalApiQueuePrompt = api.queuePrompt; |
|
api.queuePrompt = async function (number, promptData) { |
|
const unnestedWorkflow = promptData.workflow; |
|
promptData.workflow = nestedWorkflow; |
|
const result = await originalApiQueuePrompt.apply(this, arguments); |
|
promptData.workflow = unnestedWorkflow; |
|
return result; |
|
} |
|
|
|
|
|
await originalAppQueuePrompt.call(this, number, batchsize); |
|
|
|
|
|
api.queuePrompt = originalApiQueuePrompt; |
|
|
|
|
|
for (const nestedId in nestedNodesUnnested) { |
|
const unnestedNodes = nestedNodesUnnested[nestedId]; |
|
const node = nestedNodes[nestedId]; |
|
|
|
|
|
app.graph.add(node); |
|
|
|
|
|
node.nestWorkflow(unnestedNodes); |
|
|
|
|
|
const inputNodes = connectedInputNodes[nestedId]; |
|
const currentConnectedInputNodes = node.getConnectedInputNodes(); |
|
let currentIdx = 0; |
|
for (const inputIdx in inputNodes) { |
|
const inputData = inputNodes[inputIdx]; |
|
const currentInputData = currentConnectedInputNodes[currentIdx]; |
|
if (inputData.node.id === currentInputData?.node.id) { |
|
|
|
currentIdx++; |
|
continue; |
|
} |
|
|
|
const srcSlot = inputData.srcSlot; |
|
const dstSlot = inputData.dstSlot; |
|
inputData.node.connect(srcSlot, node, dstSlot); |
|
} |
|
|
|
|
|
for (const widget of node.widgets ?? []) { |
|
if (widget.inputEl) { |
|
document.body.appendChild(widget.inputEl); |
|
} |
|
} |
|
|
|
|
|
node.setSize(node.size); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
const unnestedToNestedIds = {}; |
|
for (const nestedId in nestedNodesUnnested) { |
|
const unnestedNodes = nestedNodesUnnested[nestedId]; |
|
for (const unnestedNode of unnestedNodes) { |
|
unnestedToNestedIds[unnestedNode.id] = nestedId; |
|
} |
|
} |
|
console.log("[NestedNodeBuilder] unnestedToNestedIds:", unnestedToNestedIds) |
|
|
|
ext.nestedPromptQueue.enqueue(unnestedToNestedIds); |
|
} |
|
|
|
|
|
api.addEventListener("executing", ({ detail }) => { |
|
const unnestedToNestedIds = ext.nestedPromptQueue.peek(); |
|
if (unnestedToNestedIds?.[detail]) { |
|
app.runningNodeId = unnestedToNestedIds[detail]; |
|
} |
|
}); |
|
|
|
|
|
api.addEventListener("execution_start", ({ detail }) => { |
|
this.nestedPromptQueue.dequeue(); |
|
}); |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async addCustomNodeDefs(defs, app) { |
|
|
|
nodeDefs = defs; |
|
|
|
const resp = await api.fetchApi("/nested_node_builder/nested_defs") |
|
const nestedNodeDefs = await resp.json(); |
|
|
|
Object.assign(this.nestedNodeDefs, nestedNodeDefs); |
|
|
|
Object.assign(defs, this.nestedNodeDefs); |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) { |
|
|
|
if (!(nodeData.name in this.nestedNodeDefs)) { |
|
return; |
|
} |
|
|
|
|
|
Object.defineProperties(nodeType.prototype, Object.getOwnPropertyDescriptors(NestedNode.prototype)); |
|
|
|
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated; |
|
nodeType.prototype.onNodeCreated = function () { |
|
const result = onNodeCreated?.apply(this, arguments); |
|
this.addProperty("nestedData", nodeData.description, "object"); |
|
return result; |
|
} |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
loadedGraphNode(node, app) { |
|
|
|
if (!node.properties.nestedData) return; |
|
|
|
|
|
if (this.nestedNodeDefs[node.type]) return; |
|
|
|
|
|
const nestedDef = this.createNestedDef(node.properties.nestedData.nestedNodes, node.type); |
|
|
|
|
|
if (Object.values(this.nestedNodeDefs).some(def => def.name === nestedDef.name)) return; |
|
|
|
|
|
|
|
|
|
|
|
console.log("[NestedNodeBuilder] def for nested node not found, adding temporary def:", nestedDef); |
|
|
|
|
|
this.nestedNodeDefs[nestedDef.name] = nestedDef; |
|
|
|
LiteGraph.registered_node_types = {}; |
|
app.registerNodes().then(() => { |
|
|
|
app.loadGraphData(app.graph.serialize()); |
|
}, (error) => { |
|
console.log("Error registering nodes:", error); |
|
}); |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
nodeCreated(node, app) { |
|
|
|
const getBaseMenuOptions = node.getExtraMenuOptions; |
|
|
|
node.getExtraMenuOptions = function (_, options) { |
|
|
|
getBaseMenuOptions.call(this, _, options); |
|
|
|
|
|
options.push({ |
|
content: "Nest Selected Nodes", callback: () => { |
|
ext.onMenuNestSelectedNodes(); |
|
} |
|
}); |
|
|
|
|
|
const selectedNodes = app.canvas.selected_nodes; |
|
const serializedWorkflow = serializeWorkflow(selectedNodes); |
|
for (const defName in ext.nestedNodeDefs) { |
|
const def = ext.nestedNodeDefs[defName]; |
|
if (isStructurallyEqual(def.description.nestedNodes, serializedWorkflow)) { |
|
options.push({ |
|
content: `Convert selected to Nested Node: ${defName}`, callback: () => { |
|
ext.nestSelectedNodes(selectedNodes, defName); |
|
} |
|
}); |
|
} |
|
} |
|
|
|
|
|
if (this.properties.nestedData) { |
|
|
|
options.push({ |
|
content: "Unnest", callback: () => { |
|
this.unnest(); |
|
} |
|
}); |
|
} |
|
|
|
|
|
options.push(null); |
|
}; |
|
}, |
|
|
|
createNestedDef(serializedWorkflow, uniqueName) { |
|
|
|
const uniqueType = uniqueName.replace(/\s/g, "_"); |
|
let nestedDef = { |
|
name: uniqueType, |
|
display_name: uniqueName, |
|
category: "Nested Nodes", |
|
description: { |
|
nestedNodes: serializedWorkflow |
|
}, |
|
input: {}, |
|
output: [], |
|
output_is_list: [], |
|
output_name: [], |
|
}; |
|
|
|
const linksMapping = mapLinksToNodes(serializedWorkflow); |
|
|
|
for (const id in serializedWorkflow) { |
|
const node = serializedWorkflow[id]; |
|
const nodeDef = nodeDefs[node.type]; |
|
inheritInputs(node, nodeDef, nestedDef, linksMapping); |
|
inheritOutputs(node, nodeDef, nestedDef, linksMapping); |
|
} |
|
return nestedDef; |
|
}, |
|
|
|
createNestSelectedDialog(selectedNodes) { |
|
const pos = [window.innerWidth / 3, 2 * window.innerHeight / 3]; |
|
const enterName = (input) => { |
|
|
|
const name = input.value; |
|
if (name in this.nestedNodeDefs) { |
|
this.comfirmationDialog.show( |
|
`The name "${name}" is already used for a nested node. Do you want to overwrite it?`, |
|
() => { |
|
|
|
this.nestSelectedNodes(selectedNodes, name); |
|
}, |
|
() => { |
|
|
|
return; |
|
} |
|
); |
|
} else { |
|
|
|
this.nestSelectedNodes(selectedNodes, name); |
|
} |
|
} |
|
showWidgetDialog(pos, "Name for nested node:", enterName); |
|
}, |
|
|
|
onMenuNestSelectedNodes() { |
|
|
|
const selectedNodes = app.canvas.selected_nodes; |
|
|
|
|
|
this.createNestSelectedDialog(selectedNodes); |
|
}, |
|
|
|
nestSelectedNodes(selectedNodes, uniqueName) { |
|
|
|
const nestedDef = this.createNestedDef(serializeWorkflow(selectedNodes), uniqueName); |
|
|
|
|
|
this.nestedNodeDefs[nestedDef.name] = nestedDef; |
|
|
|
|
|
saveDef(nestedDef).then( |
|
(successful) => { |
|
|
|
LiteGraph.registered_node_types = {}; |
|
app.registerNodes().then(() => { |
|
|
|
const nestedNode = LiteGraph.createNode(nestedDef.name); |
|
app.graph.add(nestedNode); |
|
nestedNode.nestWorkflow(selectedNodes); |
|
|
|
|
|
app.canvas.selectNode(nestedNode, true); |
|
}, (error) => { |
|
console.log("Error registering nodes:", error); |
|
}); |
|
}, |
|
(error) => { |
|
app.ui.dialog.show(`Was unable to save the nested node. Check the console for more details.`); |
|
} |
|
); |
|
}, |
|
|
|
mapLinks(selectedNodes) { |
|
const serializedWorkflow = serializeWorkflow(selectedNodes); |
|
return mapLinksToNodes(serializedWorkflow); |
|
} |
|
|
|
}; |
|
|
|
app.registerExtension(ext); |
|
|
|
async function saveDef(nestedDef) { |
|
|
|
const request = { |
|
method: "POST", |
|
headers: { |
|
"Content-Type": "application/json", |
|
}, |
|
body: JSON.stringify(nestedDef) |
|
}; |
|
console.log("[NestedNodeBuilder] Saving nested node def:", nestedDef); |
|
const response = await api.fetchApi("/nested_node_builder/nested_defs", request); |
|
return response.status === 200; |
|
} |
|
|
|
export function mapLinksToNodes(serializedWorkflow) { |
|
|
|
const links = {}; |
|
|
|
for (const node of serializedWorkflow) { |
|
|
|
for (const inputIdx in node.inputs ?? []) { |
|
const input = node.inputs[inputIdx]; |
|
|
|
if (input.link === null) { |
|
continue; |
|
} |
|
|
|
if (links[input.link] === undefined) { |
|
links[input.link] = {}; |
|
} |
|
|
|
links[input.link].dstId = node.id; |
|
|
|
links[input.link].dstSlot = Number(inputIdx); |
|
} |
|
|
|
for (const outputIdx in node.outputs ?? []) { |
|
const output = node.outputs[outputIdx]; |
|
|
|
for (const link of output.links ?? []) { |
|
|
|
if (links[link] === undefined) { |
|
links[link] = {}; |
|
} |
|
|
|
links[link].srcId = node.id; |
|
|
|
links[link].srcSlot = Number(outputIdx); |
|
} |
|
} |
|
} |
|
return links; |
|
} |
|
|
|
function inheritInputs(node, nodeDef, nestedDef, linkMapping) { |
|
|
|
|
|
let linkInputIdx = 0; |
|
for (const inputType in (nodeDef?.input) ?? []) { |
|
|
|
const nestedInputType = inputType === "optional" ? "required" : inputType; |
|
if (!(nestedInputType in nestedDef.input)) { |
|
nestedDef.input[nestedInputType] = {}; |
|
} |
|
for (const inputName in nodeDef.input[inputType]) { |
|
|
|
let uniqueInputName = inputName; |
|
let i = 2; |
|
while (uniqueInputName in nestedDef.input[nestedInputType]) { |
|
uniqueInputName = inputName + "_" + i; |
|
i++; |
|
} |
|
const isRemainingWidgets = node.inputs === undefined || linkInputIdx >= node.inputs.length; |
|
if (isRemainingWidgets || inputName !== node.inputs[linkInputIdx].name) { |
|
|
|
nestedDef.input[nestedInputType][uniqueInputName] = nodeDef.input[inputType][inputName]; |
|
continue; |
|
} |
|
|
|
|
|
if (!isInputInternal(node, linkInputIdx, linkMapping)) { |
|
nestedDef.input[nestedInputType][uniqueInputName] = nodeDef.input[inputType][inputName]; |
|
} |
|
linkInputIdx++; |
|
} |
|
} |
|
} |
|
|
|
export function isInputInternal(node, inputIdx, linkMapping) { |
|
|
|
const link = node.inputs[inputIdx].link; |
|
if (link === null) { |
|
return false; |
|
} |
|
|
|
const entry = linkMapping[link]; |
|
if (entry.srcId === undefined) { |
|
|
|
return false; |
|
} |
|
return true; |
|
} |
|
|
|
export function isOutputInternal(node, outputIdx, linkMapping) { |
|
|
|
const links = node.outputs[outputIdx].links; |
|
if (links === null || links.length === 0) { |
|
return false; |
|
} |
|
|
|
for (const link of links) { |
|
const entry = linkMapping[link]; |
|
if (entry.dstId === undefined) { |
|
|
|
return false; |
|
} |
|
} |
|
return true; |
|
} |
|
|
|
function inheritOutputs(node, nodeDef, nestedDef, linksMapping) { |
|
|
|
|
|
|
|
|
|
for (const outputIdx in (nodeDef?.output) ?? []) { |
|
if (isOutputInternal(node, outputIdx, linksMapping)) { |
|
continue; |
|
} |
|
const defOutput = nodeDef.output[outputIdx]; |
|
const defOutputName = nodeDef.output_name[outputIdx]; |
|
const defOutputIsList = nodeDef.output_is_list[outputIdx]; |
|
nestedDef.output.push(defOutput); |
|
nestedDef.output_name.push(defOutputName); |
|
nestedDef.output_is_list.push(defOutputIsList); |
|
} |
|
} |
|
|
|
export function isStructurallyEqual(nestedWorkflow1, nestedWorkflow2) { |
|
|
|
if (nestedWorkflow1.length !== nestedWorkflow2.length) { |
|
return false; |
|
} |
|
|
|
|
|
|
|
|
|
const nodeTypeCount1 = {}; |
|
const nodeTypeCount2 = {}; |
|
for (const i in nestedWorkflow1) { |
|
const node1 = nestedWorkflow1[i]; |
|
if (nodeTypeCount1[node1.type] === undefined) { |
|
nodeTypeCount1[node1.type] = 0; |
|
} |
|
nodeTypeCount1[node1.type]++; |
|
|
|
const node2 = nestedWorkflow2[i]; |
|
if (nodeTypeCount2[node2.type] === undefined) { |
|
nodeTypeCount2[node2.type] = 0; |
|
} |
|
nodeTypeCount2[node2.type]++; |
|
} |
|
|
|
for (const type in nodeTypeCount1) { |
|
if (nodeTypeCount1[type] !== nodeTypeCount2[type]) { |
|
return false; |
|
} |
|
} |
|
|
|
|
|
const linksMapping1 = mapLinksToNodes(nestedWorkflow1); |
|
const linksMapping2 = mapLinksToNodes(nestedWorkflow2); |
|
|
|
|
|
for (const link in linksMapping1) { |
|
const entry = linksMapping1[link]; |
|
if (entry.srcId === undefined || entry.dstId === undefined) { |
|
delete linksMapping1[link]; |
|
} |
|
} |
|
for (const link in linksMapping2) { |
|
const entry = linksMapping2[link]; |
|
if (entry.srcId === undefined || entry.dstId === undefined) { |
|
delete linksMapping2[link]; |
|
} |
|
} |
|
|
|
|
|
const idToType1 = {}; |
|
const idToType2 = {}; |
|
for (const i in nestedWorkflow1) { |
|
const node1 = nestedWorkflow1[i]; |
|
idToType1[node1.id] = node1.type; |
|
const node2 = nestedWorkflow2[i]; |
|
idToType2[node2.id] = node2.type; |
|
} |
|
|
|
|
|
for (const link in linksMapping1) { |
|
const entry = linksMapping1[link]; |
|
entry.srcId = idToType1[entry.srcId]; |
|
entry.dstId = idToType1[entry.dstId]; |
|
} |
|
for (const link in linksMapping2) { |
|
const entry = linksMapping2[link]; |
|
entry.srcId = idToType2[entry.srcId]; |
|
entry.dstId = idToType2[entry.dstId]; |
|
} |
|
|
|
|
|
for (const link1 in linksMapping1) { |
|
|
|
let foundMatch = false; |
|
for (const link2 in linksMapping2) { |
|
const entry1 = linksMapping1[link1]; |
|
const entry2 = linksMapping2[link2]; |
|
if (entry1.srcId === entry2.srcId && entry1.dstId === entry2.dstId) { |
|
|
|
delete linksMapping2[link2]; |
|
foundMatch = true; |
|
break; |
|
} |
|
} |
|
if (!foundMatch) { |
|
return false; |
|
} |
|
} |
|
return true; |
|
} |