-
Notifications
You must be signed in to change notification settings - Fork 411
Add support for dynamic widgets #6661
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7cc7a0c
728376f
5c9689f
135e9aa
e1c03ec
9ab7852
d07e4ab
b9cd1bd
58eab62
61018bb
1e47ddc
d58b8fe
b5f3df0
f185a98
5a28c64
699cec6
0d8338f
61c5bfa
756e391
6d0712a
6499cfb
6c0712e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The semantics of Maybe what the |
||
| 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) | ||
| } | ||
| }) | ||
|
Comment on lines
+101
to
+109
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep. Callback also isn't triggered after loading widgets_values, which means dynamic widgets wouldn't get created when loading a saved workflow. |
||
| widget.value = widgetValue | ||
| return { widget, minWidth, minHeight } | ||
| } | ||
|
|
||
| export const dynamicWidgets = { COMFY_DYNAMICCOMBO_V3: dynamicComboWidget } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we could communicate more about the data structure and semantics of the system via variable names, helper functions, code flow, etc.