mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-22 07:19:41 +00:00
Support associated socket for widgets (#3326)
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user