Spaces:
Running
on
Zero
Running
on
Zero
| import { app } from "../../../scripts/app.js"; | |
| import { ComfyWidgets } from "../../../scripts/widgets.js"; | |
| const REROUTE_PRIMITIVE = "ReroutePrimitive|pysssss"; | |
| const MULTI_PRIMITIVE = "MultiPrimitive|pysssss"; | |
| const LAST_TYPE = Symbol("LastType"); | |
| app.registerExtension({ | |
| name: "pysssss.ReroutePrimitive", | |
| init() { | |
| // On graph configure, fire onGraphConfigured to create widgets | |
| const graphConfigure = LGraph.prototype.configure; | |
| LGraph.prototype.configure = function () { | |
| const r = graphConfigure.apply(this, arguments); | |
| for (const n of app.graph._nodes) { | |
| if (n.type === REROUTE_PRIMITIVE) { | |
| n.onGraphConfigured(); | |
| } | |
| } | |
| return r; | |
| }; | |
| // Hide this node as it is no longer supported | |
| const getNodeTypesCategories = LiteGraph.getNodeTypesCategories; | |
| LiteGraph.getNodeTypesCategories = function() { | |
| return getNodeTypesCategories.apply(this, arguments).filter(c => !c.startsWith("__hidden__")); | |
| } | |
| const graphToPrompt = app.graphToPrompt; | |
| app.graphToPrompt = async function () { | |
| const res = await graphToPrompt.apply(this, arguments); | |
| const multiOutputs = []; | |
| for (const nodeId in res.output) { | |
| const output = res.output[nodeId]; | |
| if (output.class_type === MULTI_PRIMITIVE) { | |
| multiOutputs.push({ id: nodeId, inputs: output.inputs }); | |
| } | |
| } | |
| function permute(outputs) { | |
| function generatePermutations(inputs, currentIndex, currentPermutation, result) { | |
| if (currentIndex === inputs.length) { | |
| result.push({ ...currentPermutation }); | |
| return; | |
| } | |
| const input = inputs[currentIndex]; | |
| for (const k in input) { | |
| currentPermutation[currentIndex] = input[k]; | |
| generatePermutations(inputs, currentIndex + 1, currentPermutation, result); | |
| } | |
| } | |
| const inputs = outputs.map((output) => output.inputs); | |
| const result = []; | |
| const current = new Array(inputs.length); | |
| generatePermutations(inputs, 0, current, result); | |
| return outputs.map((output, index) => ({ | |
| ...output, | |
| inputs: result.reduce((p, permutation) => { | |
| const count = Object.keys(p).length; | |
| p["value" + (count || "")] = permutation[index]; | |
| return p; | |
| }, {}), | |
| })); | |
| } | |
| const permutations = permute(multiOutputs); | |
| for (let i = 0; i < permutations.length; i++) { | |
| res.output[multiOutputs[i].id].inputs = permutations[i].inputs; | |
| } | |
| return res; | |
| }; | |
| }, | |
| async beforeRegisterNodeDef(nodeType, nodeData, app) { | |
| function addOutputHandler() { | |
| // Finds the first non reroute output node down the chain | |
| nodeType.prototype.getFirstReroutedOutput = function (slot) { | |
| if (nodeData.name === MULTI_PRIMITIVE) { | |
| slot = 0; | |
| } | |
| const links = this.outputs[slot].links; | |
| if (!links) return null; | |
| const search = []; | |
| for (const l of links) { | |
| const link = app.graph.links[l]; | |
| if (!link) continue; | |
| const node = app.graph.getNodeById(link.target_id); | |
| if (node.type !== REROUTE_PRIMITIVE && node.type !== MULTI_PRIMITIVE) { | |
| return { node, link }; | |
| } | |
| search.push({ node, link }); | |
| } | |
| for (const { link, node } of search) { | |
| const r = node.getFirstReroutedOutput(link.target_slot); | |
| if (r) { | |
| return r; | |
| } | |
| } | |
| }; | |
| } | |
| if (nodeData.name === REROUTE_PRIMITIVE) { | |
| const configure = nodeType.prototype.configure || LGraphNode.prototype.configure; | |
| const onConnectionsChange = nodeType.prototype.onConnectionsChange; | |
| const onAdded = nodeType.prototype.onAdded; | |
| nodeType.title_mode = LiteGraph.NO_TITLE; | |
| function hasAnyInput(node) { | |
| for (const input of node.inputs) { | |
| if (input.link) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| // Remove input text | |
| nodeType.prototype.onAdded = function () { | |
| onAdded?.apply(this, arguments); | |
| this.inputs[0].label = ""; | |
| this.outputs[0].label = "value"; | |
| this.setSize(this.computeSize()); | |
| }; | |
| // Restore any widgets | |
| nodeType.prototype.onGraphConfigured = function () { | |
| if (hasAnyInput(this)) return; | |
| const outputNode = this.getFirstReroutedOutput(0); | |
| if (outputNode) { | |
| this.checkPrimitiveWidget(outputNode); | |
| } | |
| }; | |
| // Check if we need to create (or remove) a widget on the node | |
| nodeType.prototype.checkPrimitiveWidget = function ({ node, link }) { | |
| let widgetType = link.type; | |
| let targetLabel = widgetType; | |
| const input = node.inputs[link.target_slot]; | |
| if (input.widget?.config?.[0] instanceof Array) { | |
| targetLabel = input.widget.name; | |
| widgetType = "COMBO"; | |
| } | |
| if (widgetType in ComfyWidgets) { | |
| if (!this.widgets?.length) { | |
| let v; | |
| if (this.widgets_values?.length) { | |
| v = this.widgets_values[0]; | |
| } | |
| let config = [link.type, {}]; | |
| if (input.widget?.config) { | |
| config = input.widget.config; | |
| } | |
| const { widget } = ComfyWidgets[widgetType](this, "value", config, app); | |
| if (v !== undefined && (!this[LAST_TYPE] || this[LAST_TYPE] === widgetType)) { | |
| widget.value = v; | |
| } | |
| this[LAST_TYPE] = widgetType; | |
| } | |
| } else if (this.widgets) { | |
| this.widgets.length = 0; | |
| } | |
| return targetLabel; | |
| }; | |
| // Finds all input nodes from the current reroute | |
| nodeType.prototype.getReroutedInputs = function (slot) { | |
| let nodes = [{ node: this }]; | |
| let node = this; | |
| while (node?.type === REROUTE_PRIMITIVE) { | |
| const input = node.inputs[slot]; | |
| if (input.link) { | |
| const link = app.graph.links[input.link]; | |
| node = app.graph.getNodeById(link.origin_id); | |
| slot = link.origin_slot; | |
| nodes.push({ | |
| node, | |
| link, | |
| }); | |
| } else { | |
| node = null; | |
| } | |
| } | |
| return nodes; | |
| }; | |
| addOutputHandler(); | |
| // Update the type of all reroutes in a chain | |
| nodeType.prototype.changeRerouteType = function (slot, type, label) { | |
| const color = LGraphCanvas.link_type_colors[type]; | |
| const output = this.outputs[slot]; | |
| this.inputs[slot].label = " "; | |
| output.label = label || (type === "*" ? "value" : type); | |
| output.type = type; | |
| // Process all linked outputs | |
| for (const linkId of output.links || []) { | |
| const link = app.graph.links[linkId]; | |
| if (!link) continue; | |
| link.color = color; | |
| const node = app.graph.getNodeById(link.target_id); | |
| if (node.changeRerouteType) { | |
| // Recursively update reroutes | |
| node.changeRerouteType(link.target_slot, type, label); | |
| } else { | |
| // Validate links to 'real' nodes | |
| const theirType = node.inputs[link.target_slot].type; | |
| if (theirType !== type && theirType !== "*") { | |
| node.disconnectInput(link.target_slot); | |
| } | |
| } | |
| } | |
| if (this.inputs[slot].link) { | |
| const link = app.graph.links[this.inputs[slot].link]; | |
| if (link) link.color = color; | |
| } | |
| }; | |
| // Override configure so we can flag that we are configuring to avoid link validation breaking | |
| let configuring = false; | |
| nodeType.prototype.configure = function () { | |
| configuring = true; | |
| const r = configure?.apply(this, arguments); | |
| configuring = false; | |
| return r; | |
| }; | |
| Object.defineProperty(nodeType, "title_mode", { | |
| get() { | |
| return app.canvas.current_node?.widgets?.length ? LiteGraph.NORMAL_TITLE : LiteGraph.NO_TITLE; | |
| }, | |
| }); | |
| nodeType.prototype.onConnectionsChange = function (type, _, connected, link_info) { | |
| // If configuring treat everything as OK as links may not be set by litegraph yet | |
| if (configuring) return; | |
| const isInput = type === LiteGraph.INPUT; | |
| const slot = isInput ? link_info.target_slot : link_info.origin_slot; | |
| let targetLabel = null; | |
| let targetNode = null; | |
| let targetType = "*"; | |
| let targetSlot = slot; | |
| const inputPath = this.getReroutedInputs(slot); | |
| const rootInput = inputPath[inputPath.length - 1]; | |
| const outputNode = this.getFirstReroutedOutput(slot); | |
| if (rootInput.node.type === REROUTE_PRIMITIVE) { | |
| // Our input node is a reroute, so see if we have an output | |
| if (outputNode) { | |
| targetType = outputNode.link.type; | |
| } else if (rootInput.node.widgets) { | |
| rootInput.node.widgets.length = 0; | |
| } | |
| targetNode = rootInput; | |
| targetSlot = rootInput.link?.target_slot ?? slot; | |
| } else { | |
| // We have a real input, so we want to use that type | |
| targetNode = inputPath[inputPath.length - 2]; | |
| targetType = rootInput.node.outputs[rootInput.link.origin_slot].type; | |
| targetSlot = rootInput.link.target_slot; | |
| } | |
| if (this.widgets && inputPath.length > 1) { | |
| // We have an input node so remove our widget | |
| this.widgets.length = 0; | |
| } | |
| if (outputNode && rootInput.node.checkPrimitiveWidget) { | |
| // We have an output, check if we need to create a widget | |
| targetLabel = rootInput.node.checkPrimitiveWidget(outputNode); | |
| } | |
| // Trigger an update of the type to all child nodes | |
| targetNode.node.changeRerouteType(targetSlot, targetType, targetLabel); | |
| return onConnectionsChange?.apply(this, arguments); | |
| }; | |
| // When collapsed fix the size to just the dot | |
| const computeSize = nodeType.prototype.computeSize || LGraphNode.prototype.computeSize; | |
| nodeType.prototype.computeSize = function () { | |
| const r = computeSize.apply(this, arguments); | |
| if (this.flags?.collapsed) { | |
| return [1, 25]; | |
| } else if (this.widgets?.length) { | |
| return r; | |
| } else { | |
| let w = 75; | |
| if (this.outputs?.[0]?.label) { | |
| const t = LiteGraph.NODE_TEXT_SIZE * this.outputs[0].label.length * 0.6 + 30; | |
| if (t > w) { | |
| w = t; | |
| } | |
| } | |
| return [w, r[1]]; | |
| } | |
| }; | |
| // On collapse shrink the node to just a dot | |
| const collapse = nodeType.prototype.collapse || LGraphNode.prototype.collapse; | |
| nodeType.prototype.collapse = function () { | |
| collapse.apply(this, arguments); | |
| this.setSize(this.computeSize()); | |
| requestAnimationFrame(() => { | |
| this.setDirtyCanvas(true, true); | |
| }); | |
| }; | |
| // Shift the bounding area up slightly as LiteGraph miscalculates it for collapsed nodes | |
| nodeType.prototype.onBounding = function (area) { | |
| if (this.flags?.collapsed) { | |
| area[1] -= 15; | |
| } | |
| }; | |
| } else if (nodeData.name === MULTI_PRIMITIVE) { | |
| addOutputHandler(); | |
| nodeType.prototype.onConnectionsChange = function (type, _, connected, link_info) { | |
| for (let i = 0; i < this.inputs.length - 1; i++) { | |
| if (!this.inputs[i].link) { | |
| this.removeInput(i--); | |
| } | |
| } | |
| if (this.inputs[this.inputs.length - 1].link) { | |
| this.addInput("v" + +new Date(), this.inputs[0].type).label = "value"; | |
| } | |
| }; | |
| } | |
| }, | |
| }); | |