mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +00:00
Nesting support for autogrow (#7275)
- Modifies autogrow inputs to be named by key - Allows autogrow inputs to be added after initialization. - Such as when added by another dynamic combo - Groups dynamic input information under a single comfyDynamic property which is opaque to Litegraph ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7275-Nesting-support-for-autogrow-2c46d73d36508171893ec43275f5b644) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
|
import { remove } from 'es-toolkit'
|
||||||
|
|
||||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||||
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
|
|
||||||
import type {
|
import type {
|
||||||
ISlotType,
|
ISlotType,
|
||||||
INodeInputSlot,
|
INodeInputSlot,
|
||||||
@@ -23,22 +24,41 @@ import type { ComfyApp } from '@/scripts/app'
|
|||||||
const INLINE_INPUTS = false
|
const INLINE_INPUTS = false
|
||||||
|
|
||||||
type MatchTypeNode = LGraphNode &
|
type MatchTypeNode = LGraphNode &
|
||||||
Pick<Required<LGraphNode>, 'comfyMatchType' | 'onConnectionsChange'>
|
Pick<Required<LGraphNode>, 'onConnectionsChange'> & {
|
||||||
|
comfyDynamic: { matchType: Record<string, Record<string, string>> }
|
||||||
|
}
|
||||||
|
type AutogrowNode = LGraphNode &
|
||||||
|
Pick<Required<LGraphNode>, 'onConnectionsChange' | 'widgets'> & {
|
||||||
|
comfyDynamic: {
|
||||||
|
autogrow: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
min: number
|
||||||
|
max: number
|
||||||
|
inputSpecs: InputSpecV2[]
|
||||||
|
prefix?: string
|
||||||
|
names?: string[]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ensureWidgetForInput(node: LGraphNode, input: INodeInputSlot) {
|
function ensureWidgetForInput(node: LGraphNode, input: INodeInputSlot) {
|
||||||
if (input.widget?.name) return
|
|
||||||
node.widgets ??= []
|
node.widgets ??= []
|
||||||
|
const { widget } = input
|
||||||
|
if (widget && node.widgets.some((w) => w.name === widget.name)) return
|
||||||
node.widgets.push({
|
node.widgets.push({
|
||||||
name: input.name,
|
|
||||||
y: 0,
|
|
||||||
type: 'shim',
|
|
||||||
options: {},
|
|
||||||
draw(ctx, _n, _w, y) {
|
draw(ctx, _n, _w, y) {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR
|
ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR
|
||||||
ctx.fillText(input.label ?? input.name, 20, y + 15)
|
ctx.fillText(input.label ?? input.name, 20, y + 15)
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
},
|
||||||
|
name: input.name,
|
||||||
|
options: {},
|
||||||
|
serialize: false,
|
||||||
|
type: 'shim',
|
||||||
|
y: 0
|
||||||
})
|
})
|
||||||
input.alwaysVisible = true
|
input.alwaysVisible = true
|
||||||
input.widget = { name: input.name }
|
input.widget = { name: input.name }
|
||||||
@@ -66,72 +86,47 @@ function dynamicComboWidget(
|
|||||||
appArg,
|
appArg,
|
||||||
widgetName
|
widgetName
|
||||||
)
|
)
|
||||||
let currentDynamicNames: string[] = []
|
function isInGroup(e: { name: string }): boolean {
|
||||||
|
return e.name.startsWith(inputName + '.')
|
||||||
|
}
|
||||||
const updateWidgets = (value?: string) => {
|
const updateWidgets = (value?: string) => {
|
||||||
if (!node.widgets) throw new Error('Not Reachable')
|
if (!node.widgets) throw new Error('Not Reachable')
|
||||||
const newSpec = value ? options[value] : undefined
|
const newSpec = value ? options[value] : undefined
|
||||||
const inputsToRemove: Record<string, INodeInputSlot> = {}
|
|
||||||
for (const name of currentDynamicNames) {
|
const removedInputs = remove(node.inputs, isInGroup)
|
||||||
const input = node.inputs.find((input) => input.name === name)
|
remove(node.widgets, isInGroup)
|
||||||
if (input) inputsToRemove[input.name] = input
|
|
||||||
const widgetIndex = node.widgets.findIndex(
|
if (!newSpec) return
|
||||||
(widget) => widget.name === name
|
|
||||||
)
|
|
||||||
if (widgetIndex === -1) continue
|
|
||||||
node.widgets[widgetIndex].value = undefined
|
|
||||||
node.widgets.splice(widgetIndex, 1)
|
|
||||||
}
|
|
||||||
currentDynamicNames = []
|
|
||||||
if (!newSpec) {
|
|
||||||
for (const input of Object.values(inputsToRemove)) {
|
|
||||||
const inputIndex = node.inputs.findIndex((inp) => inp === input)
|
|
||||||
if (inputIndex === -1) continue
|
|
||||||
node.removeInput(inputIndex)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertionPoint = node.widgets.findIndex((w) => w === widget) + 1
|
const insertionPoint = node.widgets.findIndex((w) => w === widget) + 1
|
||||||
const startingLength = node.widgets.length
|
const startingLength = node.widgets.length
|
||||||
const initialInputIndex =
|
const startingInputLength = node.inputs.length
|
||||||
node.inputs.findIndex((i) => i.name === widget.name) + 1
|
|
||||||
let startingInputLength = node.inputs.length
|
|
||||||
if (insertionPoint === 0)
|
if (insertionPoint === 0)
|
||||||
throw new Error("Dynamic widget doesn't exist on node")
|
throw new Error("Dynamic widget doesn't exist on node")
|
||||||
const inputTypes: [Record<string, InputSpec> | undefined, boolean][] = [
|
const inputTypes: (Record<string, InputSpec> | undefined)[] = [
|
||||||
[newSpec.required, false],
|
newSpec.required,
|
||||||
[newSpec.optional, true]
|
newSpec.optional
|
||||||
]
|
]
|
||||||
for (const [inputType, isOptional] of inputTypes)
|
inputTypes.forEach((inputType, idx) => {
|
||||||
for (const key in inputType ?? {}) {
|
for (const key in inputType ?? {}) {
|
||||||
const name = `${widget.name}.${key}`
|
const name = `${widget.name}.${key}`
|
||||||
const specToAdd = transformInputSpecV1ToV2(inputType![key], {
|
const specToAdd = transformInputSpecV1ToV2(inputType![key], {
|
||||||
name,
|
name,
|
||||||
isOptional
|
isOptional: idx !== 0
|
||||||
})
|
})
|
||||||
specToAdd.display_name = key
|
specToAdd.display_name = key
|
||||||
addNodeInput(node, specToAdd)
|
addNodeInput(node, specToAdd)
|
||||||
currentDynamicNames.push(name)
|
const newInputs = node.inputs
|
||||||
if (INLINE_INPUTS) ensureWidgetForInput(node, node.inputs.at(-1)!)
|
.slice(startingInputLength)
|
||||||
if (
|
.filter((inp) => inp.name.startsWith(name))
|
||||||
!inputsToRemove[name] ||
|
for (const newInput of newInputs) {
|
||||||
Array.isArray(inputType![key][0]) ||
|
if (INLINE_INPUTS && !newInput.widget)
|
||||||
!LiteGraph.isValidConnection(
|
ensureWidgetForInput(node, newInput)
|
||||||
inputsToRemove[name].type,
|
}
|
||||||
inputType![key][0]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
node.inputs.at(-1)!.link = inputsToRemove[name].link
|
|
||||||
inputsToRemove[name].link = null
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
for (const input of Object.values(inputsToRemove)) {
|
|
||||||
const inputIndex = node.inputs.findIndex((inp) => inp === input)
|
|
||||||
if (inputIndex === -1) continue
|
|
||||||
if (inputIndex < initialInputIndex) startingInputLength--
|
|
||||||
node.removeInput(inputIndex)
|
|
||||||
}
|
|
||||||
const inputInsertionPoint =
|
const inputInsertionPoint =
|
||||||
node.inputs.findIndex((i) => i.name === widget.name) + 1
|
node.inputs.findIndex((i) => i.name === widget.name) + 1
|
||||||
const addedWidgets = node.widgets.splice(startingLength)
|
const addedWidgets = node.widgets.splice(startingLength)
|
||||||
@@ -157,6 +152,28 @@ function dynamicComboWidget(
|
|||||||
)
|
)
|
||||||
//assume existing inputs are in correct order
|
//assume existing inputs are in correct order
|
||||||
spliceInputs(node, inputInsertionPoint, 0, ...addedInputs)
|
spliceInputs(node, inputInsertionPoint, 0, ...addedInputs)
|
||||||
|
|
||||||
|
for (const input of removedInputs) {
|
||||||
|
const inputIndex = node.inputs.findIndex((inp) => inp.name === input.name)
|
||||||
|
if (inputIndex === -1) {
|
||||||
|
node.inputs.push(input)
|
||||||
|
node.removeInput(node.inputs.length - 1)
|
||||||
|
} else {
|
||||||
|
node.inputs[inputIndex].link = input.link
|
||||||
|
if (!input.link) continue
|
||||||
|
const link = node.graph?.links?.[input.link]
|
||||||
|
if (!link) continue
|
||||||
|
link.target_slot = inputIndex
|
||||||
|
node.onConnectionsChange?.(
|
||||||
|
LiteGraph.INPUT,
|
||||||
|
inputIndex,
|
||||||
|
true,
|
||||||
|
link,
|
||||||
|
node.inputs[inputIndex]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
node.size[1] = node.computeSize([...node.size])[1]
|
node.size[1] = node.computeSize([...node.size])[1]
|
||||||
if (!node.graph) return
|
if (!node.graph) return
|
||||||
node._setConcreteSlots()
|
node._setConcreteSlots()
|
||||||
@@ -243,8 +260,9 @@ function changeOutputType(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function withComfyMatchType(node: LGraphNode): asserts node is MatchTypeNode {
|
function withComfyMatchType(node: LGraphNode): asserts node is MatchTypeNode {
|
||||||
if (node.comfyMatchType) return
|
if (node.comfyDynamic?.matchType) return
|
||||||
node.comfyMatchType = {}
|
node.comfyDynamic ??= {}
|
||||||
|
node.comfyDynamic.matchType = {}
|
||||||
|
|
||||||
const outputGroups = node.constructor.nodeData?.output_matchtypes
|
const outputGroups = node.constructor.nodeData?.output_matchtypes
|
||||||
node.onConnectionsChange = useChainCallback(
|
node.onConnectionsChange = useChainCallback(
|
||||||
@@ -258,9 +276,9 @@ function withComfyMatchType(node: LGraphNode): asserts node is MatchTypeNode {
|
|||||||
) {
|
) {
|
||||||
const input = this.inputs[slot]
|
const input = this.inputs[slot]
|
||||||
if (contype !== LiteGraph.INPUT || !this.graph || !input) return
|
if (contype !== LiteGraph.INPUT || !this.graph || !input) return
|
||||||
const [matchKey, matchGroup] = Object.entries(this.comfyMatchType).find(
|
const [matchKey, matchGroup] = Object.entries(
|
||||||
([, group]) => input.name in group
|
this.comfyDynamic.matchType
|
||||||
) ?? ['', undefined]
|
).find(([, group]) => input.name in group) ?? ['', undefined]
|
||||||
if (!matchGroup) return
|
if (!matchGroup) return
|
||||||
if (iscon && linf) {
|
if (iscon && linf) {
|
||||||
const { output, subgraphInput } = linf.resolve(this.graph)
|
const { output, subgraphInput } = linf.resolve(this.graph)
|
||||||
@@ -317,8 +335,8 @@ function applyMatchType(node: LGraphNode, inputSpec: InputSpecV2) {
|
|||||||
const typedSpec = { ...inputSpec, type: allowed_types }
|
const typedSpec = { ...inputSpec, type: allowed_types }
|
||||||
addNodeInput(node, typedSpec)
|
addNodeInput(node, typedSpec)
|
||||||
withComfyMatchType(node)
|
withComfyMatchType(node)
|
||||||
node.comfyMatchType[template_id] ??= {}
|
node.comfyDynamic.matchType[template_id] ??= {}
|
||||||
node.comfyMatchType[template_id][name] = allowed_types
|
node.comfyDynamic.matchType[template_id][name] = allowed_types
|
||||||
|
|
||||||
//TODO: instead apply on output add?
|
//TODO: instead apply on output add?
|
||||||
//ensure outputs get updated
|
//ensure outputs get updated
|
||||||
@@ -329,160 +347,215 @@ function applyMatchType(node: LGraphNode, inputSpec: InputSpecV2) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyAutogrow(node: LGraphNode, untypedInputSpec: InputSpecV2) {
|
function autogrowOrdinalToName(
|
||||||
|
ordinal: number,
|
||||||
|
key: string,
|
||||||
|
groupName: string,
|
||||||
|
node: AutogrowNode
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
names,
|
||||||
|
prefix = '',
|
||||||
|
inputSpecs
|
||||||
|
} = node.comfyDynamic.autogrow[groupName]
|
||||||
|
const baseName = names
|
||||||
|
? names[ordinal]
|
||||||
|
: (inputSpecs.length == 1 ? prefix : key) + ordinal
|
||||||
|
return { name: `${groupName}.${baseName}`, display_name: baseName }
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAutogrowGroup(
|
||||||
|
ordinal: number,
|
||||||
|
groupName: string,
|
||||||
|
node: AutogrowNode
|
||||||
|
) {
|
||||||
const { addNodeInput } = useLitegraphService()
|
const { addNodeInput } = useLitegraphService()
|
||||||
|
const { max, min, inputSpecs } = node.comfyDynamic.autogrow[groupName]
|
||||||
|
if (ordinal >= max) return
|
||||||
|
|
||||||
const parseResult = zAutogrowOptions.safeParse(untypedInputSpec)
|
const namedSpecs = inputSpecs.map((input) => ({
|
||||||
if (!parseResult.success) throw new Error('invalid Autogrow spec')
|
...input,
|
||||||
const inputSpec = parseResult.data
|
isOptional: ordinal >= (min ?? 0) || input.isOptional,
|
||||||
|
...autogrowOrdinalToName(ordinal, input.name, groupName, node)
|
||||||
|
}))
|
||||||
|
|
||||||
const { input, min, names, prefix, max } = inputSpec.template
|
const newInputs = namedSpecs
|
||||||
const inputTypes: [Record<string, InputSpec> | undefined, boolean][] = [
|
.filter(
|
||||||
[input.required, false],
|
(namedSpec) => !node.inputs.some((inp) => inp.name === namedSpec.name)
|
||||||
[input.optional, true]
|
|
||||||
]
|
|
||||||
const inputsV2 = inputTypes.flatMap(([inputType, isOptional]) =>
|
|
||||||
Object.entries(inputType ?? {}).map(([name, v]) =>
|
|
||||||
transformInputSpecV1ToV2(v, { name, isOptional })
|
|
||||||
)
|
)
|
||||||
|
.map((namedSpec) => {
|
||||||
|
addNodeInput(node, namedSpec)
|
||||||
|
const input = spliceInputs(node, node.inputs.length - 1, 1)[0]
|
||||||
|
if (inputSpecs.length !== 1 || (INLINE_INPUTS && !input.widget))
|
||||||
|
ensureWidgetForInput(node, input)
|
||||||
|
return input
|
||||||
|
})
|
||||||
|
|
||||||
|
const lastIndex = node.inputs.findLastIndex((inp) =>
|
||||||
|
inp.name.startsWith(groupName)
|
||||||
)
|
)
|
||||||
|
const insertionIndex = lastIndex === -1 ? node.inputs.length : lastIndex + 1
|
||||||
|
spliceInputs(node, insertionIndex, 0, ...newInputs)
|
||||||
|
app.canvas?.setDirty(true, true)
|
||||||
|
}
|
||||||
|
|
||||||
function nameToInputIndex(name: string) {
|
const ORDINAL_REGEX = /\d+$/
|
||||||
const index = node.inputs.findIndex((input) => input.name === name)
|
function resolveAutogrowOrdinal(
|
||||||
if (index === -1) throw new Error('Failed to find input')
|
inputName: string,
|
||||||
return index
|
groupName: string,
|
||||||
}
|
node: AutogrowNode
|
||||||
function nameToInput(name: string) {
|
): number | undefined {
|
||||||
return node.inputs[nameToInputIndex(name)]
|
//TODO preslice groupname?
|
||||||
|
const name = inputName.slice(groupName.length + 1)
|
||||||
|
const { names } = node.comfyDynamic.autogrow[groupName]
|
||||||
|
if (names) {
|
||||||
|
const ordinal = names.findIndex((s) => s === name)
|
||||||
|
return ordinal === -1 ? undefined : ordinal
|
||||||
}
|
}
|
||||||
|
const match = name.match(ORDINAL_REGEX)
|
||||||
|
if (!match) return undefined
|
||||||
|
const ordinal = parseInt(match[0])
|
||||||
|
return ordinal !== ordinal ? undefined : ordinal
|
||||||
|
}
|
||||||
|
function autogrowInputConnected(index: number, node: AutogrowNode) {
|
||||||
|
const input = node.inputs[index]
|
||||||
|
const groupName = input.name.slice(0, input.name.lastIndexOf('.'))
|
||||||
|
const lastInput = node.inputs.findLast((inp) =>
|
||||||
|
inp.name.startsWith(groupName)
|
||||||
|
)
|
||||||
|
const ordinal = resolveAutogrowOrdinal(input.name, groupName, node)
|
||||||
|
if (
|
||||||
|
!lastInput ||
|
||||||
|
ordinal == undefined ||
|
||||||
|
ordinal !== resolveAutogrowOrdinal(lastInput.name, groupName, node)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
addAutogrowGroup(ordinal + 1, groupName, node)
|
||||||
|
}
|
||||||
|
function autogrowInputDisconnected(index: number, node: AutogrowNode) {
|
||||||
|
const input = node.inputs[index]
|
||||||
|
if (!input) return
|
||||||
|
const groupName = input.name.slice(0, input.name.lastIndexOf('.'))
|
||||||
|
const { min = 1, inputSpecs } = node.comfyDynamic.autogrow[groupName]
|
||||||
|
const ordinal = resolveAutogrowOrdinal(input.name, groupName, node)
|
||||||
|
if (ordinal == undefined || ordinal + 1 < min) return
|
||||||
|
|
||||||
//In the distance, someone shouting YAGNI
|
//resolve all inputs in group
|
||||||
const trackedInputs: string[][] = []
|
const groupInputs = node.inputs.filter(
|
||||||
function addInputGroup(insertionIndex: number) {
|
(inp) =>
|
||||||
const ordinal = trackedInputs.length
|
inp.name.startsWith(groupName + '.') &&
|
||||||
const inputGroup = inputsV2.map((input) => ({
|
inp.name.lastIndexOf('.') === groupName.length
|
||||||
...input,
|
)
|
||||||
name: names
|
const stride = inputSpecs.length
|
||||||
? names[ordinal]
|
if (groupInputs.length % stride !== 0) {
|
||||||
: ((inputsV2.length == 1 ? prefix : input.name) ?? '') + ordinal,
|
console.error('Failed to group multi-input autogrow inputs')
|
||||||
isOptional: ordinal >= (min ?? 0) || input.isOptional
|
return
|
||||||
}))
|
|
||||||
const newInputs = inputGroup
|
|
||||||
.filter(
|
|
||||||
(namedSpec) => !node.inputs.some((inp) => inp.name === namedSpec.name)
|
|
||||||
)
|
|
||||||
.map((namedSpec) => {
|
|
||||||
addNodeInput(node, namedSpec)
|
|
||||||
const input = spliceInputs(node, node.inputs.length - 1, 1)[0]
|
|
||||||
if (inputsV2.length !== 1) ensureWidgetForInput(node, input)
|
|
||||||
return input
|
|
||||||
})
|
|
||||||
spliceInputs(node, insertionIndex, 0, ...newInputs)
|
|
||||||
trackedInputs.push(inputGroup.map((inp) => inp.name))
|
|
||||||
app.canvas?.setDirty(true, true)
|
|
||||||
}
|
}
|
||||||
for (let i = 0; i < (min || 1); i++) addInputGroup(node.inputs.length)
|
app.canvas?.setDirty(true, true)
|
||||||
function removeInputGroup(inputName: string) {
|
//groupBy would be nice here, but may not be supported
|
||||||
const groupIndex = trackedInputs.findIndex((ig) =>
|
for (let column = 0; column < stride; column++) {
|
||||||
ig.some((inpName) => inpName === inputName)
|
for (
|
||||||
)
|
let bubbleOrdinal = ordinal * stride + column;
|
||||||
if (groupIndex == -1) throw new Error('Failed to find group')
|
bubbleOrdinal + stride < groupInputs.length;
|
||||||
const group = trackedInputs[groupIndex]
|
bubbleOrdinal += stride
|
||||||
for (const nameToRemove of group) {
|
|
||||||
const inputIndex = nameToInputIndex(nameToRemove)
|
|
||||||
const input = spliceInputs(node, inputIndex, 1)[0]
|
|
||||||
if (!input.widget?.name) continue
|
|
||||||
const widget = node.widgets?.find((w) => w.name === input.widget!.name)
|
|
||||||
if (!widget) return
|
|
||||||
widget.value = undefined
|
|
||||||
node.removeWidget(widget)
|
|
||||||
}
|
|
||||||
trackedInputs.splice(groupIndex, 1)
|
|
||||||
node.size[1] = node.computeSize([...node.size])[1]
|
|
||||||
app.canvas?.setDirty(true, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
function inputConnected(index: number) {
|
|
||||||
const input = node.inputs[index]
|
|
||||||
const groupIndex = trackedInputs.findIndex((ig) =>
|
|
||||||
ig.some((inputName) => inputName === input.name)
|
|
||||||
)
|
|
||||||
if (groupIndex == -1) throw new Error('Failed to find group')
|
|
||||||
if (
|
|
||||||
groupIndex + 1 === trackedInputs.length &&
|
|
||||||
trackedInputs.length < (max ?? names?.length ?? 100)
|
|
||||||
) {
|
) {
|
||||||
const lastInput = trackedInputs[groupIndex].at(-1)
|
const curInput = groupInputs[bubbleOrdinal]
|
||||||
if (!lastInput) return
|
curInput.link = groupInputs[bubbleOrdinal + stride].link
|
||||||
const insertionIndex = nameToInputIndex(lastInput) + 1
|
if (!curInput.link) continue
|
||||||
if (insertionIndex === 0) throw new Error('Failed to find Input')
|
const link = node.graph?.links[curInput.link]
|
||||||
addInputGroup(insertionIndex)
|
if (!link) continue
|
||||||
|
const curIndex = node.inputs.findIndex((inp) => inp === curInput)
|
||||||
|
if (curIndex === -1) throw new Error('missing input')
|
||||||
|
link.target_slot = curIndex
|
||||||
}
|
}
|
||||||
|
const lastInput = groupInputs.at(column - stride)
|
||||||
|
if (!lastInput) continue
|
||||||
|
lastInput.link = null
|
||||||
}
|
}
|
||||||
function inputDisconnected(index: number) {
|
const removalChecks = groupInputs.slice((min - 1) * stride)
|
||||||
const input = node.inputs[index]
|
let i
|
||||||
if (trackedInputs.length === 1) return
|
for (i = removalChecks.length - stride; i >= 0; i -= stride) {
|
||||||
const groupIndex = trackedInputs.findIndex((ig) =>
|
if (removalChecks.slice(i, i + stride).some((inp) => inp.link)) break
|
||||||
ig.some((inputName) => inputName === input.name)
|
|
||||||
)
|
|
||||||
if (groupIndex == -1) throw new Error('Failed to find group')
|
|
||||||
if (
|
|
||||||
trackedInputs[groupIndex].some(
|
|
||||||
(inputName) => nameToInput(inputName).link != null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if (groupIndex + 1 < (min ?? 0)) return
|
|
||||||
//For each group from here to last group, bubble swap links
|
|
||||||
for (let column = 0; column < trackedInputs[0].length; column++) {
|
|
||||||
let prevInput = nameToInputIndex(trackedInputs[groupIndex][column])
|
|
||||||
for (let i = groupIndex + 1; i < trackedInputs.length; i++) {
|
|
||||||
const curInput = nameToInputIndex(trackedInputs[i][column])
|
|
||||||
const linkId = node.inputs[curInput].link
|
|
||||||
node.inputs[prevInput].link = linkId
|
|
||||||
const link = linkId && node.graph?.links?.[linkId]
|
|
||||||
if (link) link.target_slot = prevInput
|
|
||||||
prevInput = curInput
|
|
||||||
}
|
|
||||||
node.inputs[prevInput].link = null
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
trackedInputs.at(-2) &&
|
|
||||||
!trackedInputs.at(-2)?.some((name) => !!nameToInput(name).link)
|
|
||||||
)
|
|
||||||
removeInputGroup(trackedInputs.at(-1)![0])
|
|
||||||
}
|
}
|
||||||
|
const toRemove = removalChecks.slice(i + stride * 2)
|
||||||
|
remove(node.inputs, (inp) => toRemove.includes(inp))
|
||||||
|
for (const input of toRemove) {
|
||||||
|
const widgetName = input?.widget?.name
|
||||||
|
if (!widgetName) continue
|
||||||
|
remove(node.widgets, (w) => w.name === widgetName)
|
||||||
|
}
|
||||||
|
node.size[1] = node.computeSize([...node.size])[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
function withComfyAutogrow(node: LGraphNode): asserts node is AutogrowNode {
|
||||||
|
if (node.comfyDynamic?.autogrow) return
|
||||||
|
node.comfyDynamic ??= {}
|
||||||
|
node.comfyDynamic.autogrow = {}
|
||||||
|
|
||||||
let pendingConnection: number | undefined
|
let pendingConnection: number | undefined
|
||||||
let swappingConnection = false
|
let swappingConnection = false
|
||||||
|
|
||||||
const originalOnConnectInput = node.onConnectInput
|
const originalOnConnectInput = node.onConnectInput
|
||||||
node.onConnectInput = function (slot: number, ...args) {
|
node.onConnectInput = function (slot: number, ...args) {
|
||||||
pendingConnection = slot
|
pendingConnection = slot
|
||||||
requestAnimationFrame(() => (pendingConnection = undefined))
|
requestAnimationFrame(() => (pendingConnection = undefined))
|
||||||
return originalOnConnectInput?.apply(this, [slot, ...args]) ?? true
|
return originalOnConnectInput?.apply(this, [slot, ...args]) ?? true
|
||||||
}
|
}
|
||||||
|
|
||||||
node.onConnectionsChange = useChainCallback(
|
node.onConnectionsChange = useChainCallback(
|
||||||
node.onConnectionsChange,
|
node.onConnectionsChange,
|
||||||
(
|
function (
|
||||||
type: ISlotType,
|
this: AutogrowNode,
|
||||||
index: number,
|
contype: ISlotType,
|
||||||
|
slot: number,
|
||||||
iscon: boolean,
|
iscon: boolean,
|
||||||
linf: LLink | null | undefined
|
linf: LLink | null | undefined
|
||||||
) => {
|
) {
|
||||||
if (type !== NodeSlotType.INPUT) return
|
const input = this.inputs[slot]
|
||||||
const inputName = node.inputs[index].name
|
if (contype !== LiteGraph.INPUT || !input) return
|
||||||
if (!trackedInputs.flat().some((name) => name === inputName)) return
|
//Return if input isn't known autogrow
|
||||||
if (iscon) {
|
const key = input.name.slice(0, input.name.lastIndexOf('.'))
|
||||||
|
const autogrowGroup = this.comfyDynamic.autogrow[key]
|
||||||
|
if (!autogrowGroup) return
|
||||||
|
if (app.configuringGraph && input.widget)
|
||||||
|
ensureWidgetForInput(node, input)
|
||||||
|
if (iscon && linf) {
|
||||||
if (swappingConnection || !linf) return
|
if (swappingConnection || !linf) return
|
||||||
inputConnected(index)
|
autogrowInputConnected(slot, this)
|
||||||
} else {
|
} else {
|
||||||
if (pendingConnection === index) {
|
if (pendingConnection === slot) {
|
||||||
swappingConnection = true
|
swappingConnection = true
|
||||||
requestAnimationFrame(() => (swappingConnection = false))
|
requestAnimationFrame(() => (swappingConnection = false))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
requestAnimationFrame(() => inputDisconnected(index))
|
requestAnimationFrame(() => autogrowInputDisconnected(slot, this))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
function applyAutogrow(node: LGraphNode, inputSpecV2: InputSpecV2) {
|
||||||
|
withComfyAutogrow(node)
|
||||||
|
|
||||||
|
const parseResult = zAutogrowOptions.safeParse(inputSpecV2)
|
||||||
|
if (!parseResult.success) throw new Error('invalid Autogrow spec')
|
||||||
|
const inputSpec = parseResult.data
|
||||||
|
const { input, min = 1, names, prefix, max = 100 } = inputSpec.template
|
||||||
|
|
||||||
|
const inputTypes: (Record<string, InputSpec> | undefined)[] = [
|
||||||
|
input.required,
|
||||||
|
input.optional
|
||||||
|
]
|
||||||
|
const inputsV2 = inputTypes.flatMap((inputType, index) =>
|
||||||
|
Object.entries(inputType ?? {}).map(([name, v]) =>
|
||||||
|
transformInputSpecV1ToV2(v, { name, isOptional: index === 1 })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
node.comfyDynamic.autogrow[inputSpecV2.name] = {
|
||||||
|
names,
|
||||||
|
min,
|
||||||
|
max: names?.length ?? max,
|
||||||
|
prefix,
|
||||||
|
inputSpecs: inputsV2
|
||||||
|
}
|
||||||
|
for (let i = 0; i < min; i++) addAutogrowGroup(i, inputSpecV2.name, node)
|
||||||
|
}
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ export class LGraphNode
|
|||||||
selected?: boolean
|
selected?: boolean
|
||||||
showAdvanced?: boolean
|
showAdvanced?: boolean
|
||||||
|
|
||||||
declare comfyMatchType?: Record<string, Record<string, string>>
|
declare comfyDynamic?: Record<string, object>
|
||||||
declare comfyClass?: string
|
declare comfyClass?: string
|
||||||
declare isVirtualNode?: boolean
|
declare isVirtualNode?: boolean
|
||||||
applyToGraph?(extraLinks?: LLink[]): void
|
applyToGraph?(extraLinks?: LLink[]): void
|
||||||
|
|||||||
@@ -118,8 +118,8 @@ describe('Autogrow', () => {
|
|||||||
connectInput(node, 1, graph)
|
connectInput(node, 1, graph)
|
||||||
connectInput(node, 2, graph)
|
connectInput(node, 2, graph)
|
||||||
expect(node.inputs.length).toBe(4)
|
expect(node.inputs.length).toBe(4)
|
||||||
expect(node.inputs[0].name).toBe('test0')
|
expect(node.inputs[0].name).toBe('0.test0')
|
||||||
expect(node.inputs[2].name).toBe('test2')
|
expect(node.inputs[2].name).toBe('0.test2')
|
||||||
})
|
})
|
||||||
test('Can name by list of names', () => {
|
test('Can name by list of names', () => {
|
||||||
const graph = new LGraph()
|
const graph = new LGraph()
|
||||||
@@ -130,8 +130,8 @@ describe('Autogrow', () => {
|
|||||||
connectInput(node, 1, graph)
|
connectInput(node, 1, graph)
|
||||||
connectInput(node, 2, graph)
|
connectInput(node, 2, graph)
|
||||||
expect(node.inputs.length).toBe(3)
|
expect(node.inputs.length).toBe(3)
|
||||||
expect(node.inputs[0].name).toBe('a')
|
expect(node.inputs[0].name).toBe('0.a')
|
||||||
expect(node.inputs[2].name).toBe('c')
|
expect(node.inputs[2].name).toBe('0.c')
|
||||||
})
|
})
|
||||||
test('Can add autogrow with min input count', () => {
|
test('Can add autogrow with min input count', () => {
|
||||||
const node = testNode()
|
const node = testNode()
|
||||||
|
|||||||
Reference in New Issue
Block a user