mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +00:00
Add support for dynamic widgets (#6661)
Adds support for "dynamic combo" widgets where selecting a value on a combo widget can cause other widgets or inputs to be created.  Includes a fairly large refactoring in litegraphService to remove `#private` methods and cleanup some duplication in constructors for subgraphNodes. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6661-Add-support-for-dynamic-widgets-2a96d73d3650817aa570c7babbaca2f3) by [Unito](https://www.unito.io) --------- Co-authored-by: Alexander Brown <drjkl@comfy.org>
This commit is contained in:
114
src/core/graph/widgets/dynamicWidgets.ts
Normal file
114
src/core/graph/widgets/dynamicWidgets.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
|
||||
import type { ComboInputSpec, InputSpec } from '@/schemas/nodeDefSchema'
|
||||
import { zDynamicComboInputSpec } from '@/schemas/nodeDefSchema'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
|
||||
function dynamicComboWidget(
|
||||
node: LGraphNode,
|
||||
inputName: string,
|
||||
untypedInputData: InputSpec,
|
||||
appArg: ComfyApp,
|
||||
widgetName?: string
|
||||
) {
|
||||
const { addNodeInput } = useLitegraphService()
|
||||
const parseResult = zDynamicComboInputSpec.safeParse(untypedInputData)
|
||||
if (!parseResult.success) throw new Error('invalid DynamicCombo spec')
|
||||
const inputData = parseResult.data
|
||||
const options = Object.fromEntries(
|
||||
inputData[1].options.map(({ key, inputs }) => [key, inputs])
|
||||
)
|
||||
const subSpec: ComboInputSpec = [Object.keys(options), {}]
|
||||
const { widget, minWidth, minHeight } = app.widgets['COMBO'](
|
||||
node,
|
||||
inputName,
|
||||
subSpec,
|
||||
appArg,
|
||||
widgetName
|
||||
)
|
||||
let currentDynamicNames: string[] = []
|
||||
const updateWidgets = (value?: string) => {
|
||||
if (!node.widgets) throw new Error('Not Reachable')
|
||||
const newSpec = value ? options[value] : undefined
|
||||
//TODO: Calculate intersection for widgets that persist across options
|
||||
//This would potentially allow links to be retained
|
||||
for (const name of currentDynamicNames) {
|
||||
const inputIndex = node.inputs.findIndex((input) => input.name === name)
|
||||
if (inputIndex !== -1) node.removeInput(inputIndex)
|
||||
const widgetIndex = node.widgets.findIndex(
|
||||
(widget) => widget.name === name
|
||||
)
|
||||
if (widgetIndex === -1) continue
|
||||
node.widgets[widgetIndex].value = undefined
|
||||
node.widgets.splice(widgetIndex, 1)
|
||||
}
|
||||
currentDynamicNames = []
|
||||
if (!newSpec) return
|
||||
|
||||
const insertionPoint = node.widgets.findIndex((w) => w === widget) + 1
|
||||
const startingLength = node.widgets.length
|
||||
const inputInsertionPoint =
|
||||
node.inputs.findIndex((i) => i.name === widget.name) + 1
|
||||
const startingInputLength = node.inputs.length
|
||||
if (insertionPoint === 0)
|
||||
throw new Error("Dynamic widget doesn't exist on node")
|
||||
const inputTypes: [Record<string, InputSpec> | undefined, boolean][] = [
|
||||
[newSpec.required, false],
|
||||
[newSpec.optional, true]
|
||||
]
|
||||
for (const [inputType, isOptional] of inputTypes)
|
||||
for (const name in inputType ?? {}) {
|
||||
addNodeInput(
|
||||
node,
|
||||
transformInputSpecV1ToV2(inputType![name], {
|
||||
name,
|
||||
isOptional
|
||||
})
|
||||
)
|
||||
currentDynamicNames.push(name)
|
||||
}
|
||||
|
||||
const addedWidgets = node.widgets.splice(startingLength)
|
||||
node.widgets.splice(insertionPoint, 0, ...addedWidgets)
|
||||
if (inputInsertionPoint === 0) {
|
||||
if (
|
||||
addedWidgets.length === 0 &&
|
||||
node.inputs.length !== startingInputLength
|
||||
)
|
||||
//input is inputOnly, but lacks an insertion point
|
||||
throw new Error('Failed to find input socket for ' + widget.name)
|
||||
return
|
||||
}
|
||||
const addedInputs = node
|
||||
.spliceInputs(startingInputLength)
|
||||
.map((addedInput) => {
|
||||
const existingInput = node.inputs.findIndex(
|
||||
(existingInput) => addedInput.name === existingInput.name
|
||||
)
|
||||
return existingInput === -1
|
||||
? addedInput
|
||||
: node.spliceInputs(existingInput, 1)[0]
|
||||
})
|
||||
//assume existing inputs are in correct order
|
||||
node.spliceInputs(inputInsertionPoint, 0, ...addedInputs)
|
||||
node.size[1] = node.computeSize([...node.size])[1]
|
||||
}
|
||||
//A little hacky, but onConfigure won't work.
|
||||
//It fires too late and is overly disruptive
|
||||
let widgetValue = widget.value
|
||||
Object.defineProperty(widget, 'value', {
|
||||
get() {
|
||||
return widgetValue
|
||||
},
|
||||
set(value) {
|
||||
widgetValue = value
|
||||
updateWidgets(value)
|
||||
}
|
||||
})
|
||||
widget.value = widgetValue
|
||||
return { widget, minWidth, minHeight }
|
||||
}
|
||||
|
||||
export const dynamicWidgets = { COMFY_DYNAMICCOMBO_V3: dynamicComboWidget }
|
||||
Reference in New Issue
Block a user