Support associated socket for widgets (#3326)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Chenlei Hu
2025-04-06 11:50:21 -04:00
committed by GitHub
parent 6eb2b76621
commit ac53296b2e
41 changed files with 212 additions and 534 deletions

View File

@@ -4,9 +4,14 @@ import {
LGraphEventMode,
LGraphNode,
LiteGraph,
RenderShape
RenderShape,
type Vector2
} from '@comfyorg/litegraph'
import { Vector2 } from '@comfyorg/litegraph'
import type {
ISerialisableNodeOutput,
ISerialisedNode
} from '@comfyorg/litegraph/dist/types/serialisation'
import _ from 'lodash'
import { useNodeAnimatedImage } from '@/composables/node/useNodeAnimatedImage'
import { useNodeCanvasImagePreview } from '@/composables/node/useNodeCanvasImagePreview'
@@ -32,6 +37,10 @@ import { isImageNode, isVideoNode } from '@/utils/litegraphUtil'
import { useExtensionService } from './extensionService'
const PRIMITIVE_TYPES = new Set(['INT', 'FLOAT', 'BOOLEAN', 'STRING', 'COMBO'])
export const CONFIG = Symbol()
export const GET_CONFIG = Symbol()
/**
* Service that augments litegraph with ComfyUI specific functionality.
*/
@@ -95,59 +104,79 @@ export const useLitegraphService = () => {
}
}
/**
* @internal Add input sockets to the node. (No widget)
*/
#addInputSocket(inputSpec: InputSpec) {
const inputName = inputSpec.name
const nameKey = `${this.#nodeKey}.inputs.${normalizeI18nKey(inputName)}.name`
const widgetConstructor = widgetStore.widgets.get(inputSpec.type)
if (widgetConstructor && !inputSpec.forceInput) return
this.addInput(inputName, inputSpec.type, {
shape: inputSpec.isOptional ? RenderShape.HollowCircle : undefined,
localized_name: st(nameKey, inputName)
})
}
/**
* @internal Add a widget to the node. For primitive types, an input socket is also added.
*/
#addInputWidget(inputSpec: InputSpec) {
const inputName = inputSpec.name
const nameKey = `${this.#nodeKey}.inputs.${normalizeI18nKey(inputName)}.name`
const widgetConstructor = widgetStore.widgets.get(inputSpec.type)
if (!widgetConstructor || inputSpec.forceInput) return
const {
widget,
minWidth = 1,
minHeight = 1
} = widgetConstructor(
this,
inputName,
transformInputSpecV2ToV1(inputSpec),
app
) ?? {}
if (widget) {
widget.label = st(nameKey, widget.label ?? inputName)
widget.options ??= {}
Object.assign(widget.options, {
inputIsOptional: inputSpec.isOptional,
forceInput: inputSpec.forceInput,
advanced: inputSpec.advanced,
hidden: inputSpec.hidden
})
}
if (PRIMITIVE_TYPES.has(inputSpec.type)) {
const inputSpecV1 = transformInputSpecV2ToV1(inputSpec)
this.addInput(inputName, inputSpec.type, {
shape: inputSpec.isOptional ? RenderShape.HollowCircle : undefined,
localized_name: st(nameKey, inputName),
widget: { name: inputName, [GET_CONFIG]: () => inputSpecV1 }
})
}
this.#initialMinSize.width = Math.max(
this.#initialMinSize.width,
minWidth
)
this.#initialMinSize.height = Math.max(
this.#initialMinSize.height,
minHeight
)
}
/**
* @internal Add inputs to the node.
*/
#addInputs(inputs: Record<string, InputSpec>) {
for (const [inputName, inputSpec] of Object.entries(inputs)) {
const inputType = inputSpec.type
const nameKey = `${this.#nodeKey}.inputs.${normalizeI18nKey(inputName)}.name`
const widgetConstructor = widgetStore.widgets[inputType]
if (widgetConstructor) {
const {
widget,
minWidth = 1,
minHeight = 1
} = widgetConstructor(
this,
inputName,
transformInputSpecV2ToV1(inputSpec),
app
) ?? {}
if (widget) {
widget.label = st(nameKey, widget.label ?? inputName)
widget.options ??= {}
Object.assign(widget.options, {
inputIsOptional: inputSpec.isOptional,
forceInput: inputSpec.forceInput,
defaultInput: inputSpec.defaultInput,
advanced: inputSpec.advanced,
hidden: inputSpec.hidden
})
}
this.#initialMinSize.width = Math.max(
this.#initialMinSize.width,
minWidth
)
this.#initialMinSize.height = Math.max(
this.#initialMinSize.height,
minHeight
)
} else {
// Node connection inputs
const shapeOptions = inputSpec.isOptional
? { shape: RenderShape.HollowCircle }
: {}
this.addInput(inputName, inputType, {
...shapeOptions,
localized_name: st(nameKey, inputName)
})
}
}
for (const inputSpec of Object.values(inputs))
this.#addInputSocket(inputSpec)
for (const inputSpec of Object.values(inputs))
this.#addInputWidget(inputSpec)
}
/**
@@ -182,33 +211,57 @@ export const useLitegraphService = () => {
this.setSize(s)
}
configure(data: any) {
// Keep 'name', 'type', 'shape', and 'localized_name' information from the original node definition.
const merge = (
current: Record<string, any>,
incoming: Record<string, any>
) => {
const result = { ...incoming }
if (current.widget === undefined && incoming.widget !== undefined) {
// Field must be input as only inputs can be converted
this.inputs.push(current as INodeInputSlot)
return incoming
/**
* Configure the node from a serialised node. Keep 'name', 'type', 'shape',
* and 'localized_name' information from the original node definition.
*/
override configure(data: ISerialisedNode): void {
const RESERVED_KEYS = ['name', 'type', 'shape', 'localized_name']
// Note: input name is unique in a node definition, so we can lookup
// input by name.
const inputByName = new Map<string, INodeInputSlot>(
data.inputs?.map((input) => [input.name, input]) ?? []
)
// Inputs defined by the node definition.
const definedInputNames = new Set(
this.inputs.map((input) => input.name)
)
const definedInputs = this.inputs.map((input) => {
const inputData = inputByName.get(input.name)
return inputData
? {
...inputData,
// Whether the input has associated widget follows the
// original node definition.
..._.pick(input, RESERVED_KEYS.concat('widget'))
}
: input
})
// Extra inputs that potentially dynamically added by custom js logic.
const extraInputs = data.inputs?.filter(
(input) => !definedInputNames.has(input.name)
)
data.inputs = [...definedInputs, ...(extraInputs ?? [])]
// Note: output name is not unique, so we cannot lookup output by name.
// Use index instead.
data.outputs = _.zip(this.outputs, data.outputs).map(
([output, outputData]) => {
// If there are extra outputs in the serialised node, use them directly.
// There are currently custom nodes that dynamically add outputs via
// js logic.
if (!output) return outputData as ISerialisableNodeOutput
return outputData
? {
...outputData,
..._.pick(output, RESERVED_KEYS)
}
: output
}
for (const key of ['name', 'type', 'shape', 'localized_name']) {
if (current[key] !== undefined) {
result[key] = current[key]
}
}
return result
}
for (const field of ['inputs', 'outputs']) {
const slots = data[field] ?? []
// @ts-expect-error fixme ts strict error
data[field] = slots.map((slot, i) =>
// @ts-expect-error fixme ts strict error
merge(this[field][i] ?? {}, slot)
)
}
)
super.configure(data)
}
}