Support associated socket for widgets (#3326)
Co-authored-by: github-actions <github-actions@github.com>
@@ -81,7 +81,7 @@ export class NodeWidgetReference {
|
||||
if (!widget) throw new Error(`Widget ${index} not found.`)
|
||||
|
||||
const [x, y, w, h] = node.getBounding()
|
||||
return window['app'].canvas.ds.convertOffsetToCanvas([
|
||||
return window['app'].canvasPosToClientPos([
|
||||
x + w / 2,
|
||||
y + window['LiteGraph']['NODE_TITLE_HEIGHT'] + widget.last_y + 1
|
||||
])
|
||||
@@ -94,6 +94,36 @@ export class NodeWidgetReference {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The position of the widget's associated socket
|
||||
*/
|
||||
async getSocketPosition(): Promise<Position> {
|
||||
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
|
||||
([id, index]) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found.`)
|
||||
const widget = node.widgets[index]
|
||||
if (!widget) throw new Error(`Widget ${index} not found.`)
|
||||
|
||||
const slot = node.inputs.find(
|
||||
(slot) => slot.widget?.name === widget.name
|
||||
)
|
||||
if (!slot) throw new Error(`Socket ${widget.name} not found.`)
|
||||
|
||||
const [x, y] = node.getBounding()
|
||||
return window['app'].canvasPosToClientPos([
|
||||
x + slot.pos[0],
|
||||
y + slot.pos[1] + window['LiteGraph']['NODE_TITLE_HEIGHT']
|
||||
])
|
||||
},
|
||||
[this.node.id, this.index] as const
|
||||
)
|
||||
return {
|
||||
x: pos[0],
|
||||
y: pos[1]
|
||||
}
|
||||
}
|
||||
|
||||
async click() {
|
||||
await this.node.comfyPage.canvas.click({
|
||||
position: await this.getPosition()
|
||||
@@ -250,7 +280,7 @@ export class NodeReference {
|
||||
const targetWidget = await targetNode.getWidget(targetWidgetIndex)
|
||||
await this.comfyPage.dragAndDrop(
|
||||
await originSlot.getPosition(),
|
||||
await targetWidget.getPosition()
|
||||
await targetWidget.getSocketPosition()
|
||||
)
|
||||
return originSlot
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 65 KiB |
@@ -88,63 +88,6 @@ test.describe('Node Right Click Menu', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Widget conversion', () => {
|
||||
const convertibleWidgetTypes = ['text', 'string', 'number', 'toggle']
|
||||
|
||||
test('Can convert widget to input', async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickEmptyLatentNode()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
||||
await comfyPage.page.getByText('Convert Widget to Input').click()
|
||||
await comfyPage.nextFrame()
|
||||
// The submenu has an identical entry as the base menu - use last
|
||||
await comfyPage.page.getByText('Convert width to input').last().click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'right-click-node-widget-converted.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can convert widget without submenu', async ({ comfyPage }) => {
|
||||
// Right-click the width widget
|
||||
await comfyPage.rightClickEmptyLatentNode()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
||||
await comfyPage.page.getByText('Convert width to input').click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'right-click-node-widget-converted.png'
|
||||
)
|
||||
})
|
||||
|
||||
convertibleWidgetTypes.forEach((widgetType) => {
|
||||
test(`Can convert ${widgetType} widget to input`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const nodeType = 'KSampler'
|
||||
|
||||
// To avoid needing multiple clicks, disable nesting of conversion options
|
||||
await comfyPage.setSetting('Comfy.NodeInputConversionSubmenus', false)
|
||||
|
||||
// Add the widget using the node's `addWidget` method
|
||||
await comfyPage.page.evaluate(
|
||||
([nodeType, widgetType]) => {
|
||||
const node = window['app'].graph.nodes.find(
|
||||
(n) => n.type === nodeType
|
||||
)
|
||||
node.addWidget(widgetType, widgetType, 'defaultValue', () => {}, {})
|
||||
},
|
||||
[nodeType, widgetType]
|
||||
)
|
||||
|
||||
// Verify the context menu includes the conversion option
|
||||
const node = (await comfyPage.getNodeRefsByType(nodeType))[0]
|
||||
const menuOptions = await node.getContextMenuOptionNames()
|
||||
expect(menuOptions.includes(`Convert ${widgetType} to input`)).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('Can pin and unpin', async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickEmptyLatentNode()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
||||
|
||||
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
8
package-lock.json
generated
@@ -12,7 +12,7 @@
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.31",
|
||||
"@comfyorg/litegraph": "^0.12.0",
|
||||
"@comfyorg/litegraph": "^0.13.0-0",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -478,9 +478,9 @@
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.12.0.tgz",
|
||||
"integrity": "sha512-2LK1tNHIPAGmalloJxVtWXndG4vWNAEBX2RuQE7Fvtj2UuMFFpV/tWq+ofFJjj8sk2K/S5CfVsx+RFnREpV3RQ==",
|
||||
"version": "0.13.0-0",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.13.0-0.tgz",
|
||||
"integrity": "sha512-jkrk3d+riU7LpiV2CUXcVyropIa8W+FvTK48cSVqzJh+/BB7kHiYtYRfzwp3fgln5/LHsfUf1TWd6OqZWFTXrA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.31",
|
||||
"@comfyorg/litegraph": "^0.12.0",
|
||||
"@comfyorg/litegraph": "^0.13.0-0",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
|
||||
@@ -49,7 +49,9 @@ const style = computed<CSSProperties>(() => ({
|
||||
...positionStyle.value,
|
||||
...(enableDomClipping.value ? clippingStyle.value : {}),
|
||||
zIndex: widgetState.zIndex,
|
||||
pointerEvents: widgetState.readonly ? 'none' : 'auto'
|
||||
pointerEvents:
|
||||
widgetState.readonly || widget.computedDisabled ? 'none' : 'auto',
|
||||
opacity: widget.computedDisabled ? 0.5 : 1
|
||||
}))
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { LGraphNode, LiteGraph, RenderShape } from '@comfyorg/litegraph'
|
||||
import { LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import type {
|
||||
IFoundSlot,
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
ISlotType,
|
||||
@@ -14,13 +13,11 @@ import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import type { InputSpec } from '@/schemas/nodeDefSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { ComfyWidgets, addValueControlWidgets } from '@/scripts/widgets'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { CONFIG, GET_CONFIG } from '@/services/litegraphService'
|
||||
import { mergeInputSpec } from '@/utils/nodeDefUtil'
|
||||
import { applyTextReplacements } from '@/utils/searchAndReplace'
|
||||
import { isPrimitiveNode } from '@/utils/typeGuardUtil'
|
||||
|
||||
const CONVERTED_TYPE = 'converted-widget'
|
||||
const VALID_TYPES = [
|
||||
'STRING',
|
||||
'combo',
|
||||
@@ -30,8 +27,6 @@ const VALID_TYPES = [
|
||||
'text',
|
||||
'string'
|
||||
]
|
||||
const CONFIG = Symbol()
|
||||
const GET_CONFIG = Symbol()
|
||||
|
||||
const replacePropertyName = 'Run widget replace on values'
|
||||
export class PrimitiveNode extends LGraphNode {
|
||||
@@ -107,18 +102,14 @@ export class PrimitiveNode extends LGraphNode {
|
||||
|
||||
onAfterGraphConfigured() {
|
||||
if (this.outputs[0].links?.length && !this.widgets?.length) {
|
||||
// TODO: Review this check
|
||||
// @ts-expect-error
|
||||
if (!this.#onFirstConnection()) return
|
||||
this.#onFirstConnection()
|
||||
|
||||
// Populate widget values from config data
|
||||
if (this.widgets) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (this.widgets && this.widgets_values) {
|
||||
for (let i = 0; i < this.widgets_values.length; i++) {
|
||||
const w = this.widgets[i]
|
||||
if (w) {
|
||||
// @ts-expect-error change widget type from string to unknown
|
||||
w.value = this.widgets_values[i]
|
||||
w.value = this.widgets_values[i] as any
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -447,109 +438,21 @@ function isConvertibleWidget(widget: IWidget, config: InputSpec): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
function hideWidget(
|
||||
node: LGraphNode,
|
||||
widget: IWidget,
|
||||
options: { suffix?: string; holdSpace?: boolean } = {}
|
||||
) {
|
||||
const { suffix = '', holdSpace = true } = options
|
||||
|
||||
if (widget.type?.startsWith(CONVERTED_TYPE)) return
|
||||
widget.origType = widget.type
|
||||
widget.origComputeSize = widget.computeSize
|
||||
widget.origSerializeValue = widget.serializeValue
|
||||
// @ts-expect-error custom widget type
|
||||
widget.type = CONVERTED_TYPE + suffix
|
||||
if (holdSpace) {
|
||||
widget.computeSize = () => [0, LiteGraph.NODE_WIDGET_HEIGHT]
|
||||
} else {
|
||||
// -4 is due to the gap litegraph adds between widgets automatically
|
||||
widget.computeSize = () => [0, -4]
|
||||
}
|
||||
widget.serializeValue = (node: LGraphNode, index: number) => {
|
||||
// Prevent serializing the widget if we have no input linked
|
||||
if (!node.inputs) {
|
||||
return undefined
|
||||
}
|
||||
let node_input = node.inputs.find((i) => i.widget?.name === widget.name)
|
||||
|
||||
if (!node_input || !node_input.link) {
|
||||
return undefined
|
||||
}
|
||||
return widget.origSerializeValue
|
||||
? widget.origSerializeValue(node, index)
|
||||
: widget.value
|
||||
}
|
||||
|
||||
// Hide any linked widgets, e.g. seed+seedControl
|
||||
if (widget.linkedWidgets) {
|
||||
for (const w of widget.linkedWidgets) {
|
||||
hideWidget(node, w, { suffix: ':' + widget.name, holdSpace: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showWidget(widget: IWidget) {
|
||||
// @ts-expect-error custom widget type
|
||||
widget.type = widget.origType
|
||||
widget.computeSize = widget.origComputeSize
|
||||
widget.serializeValue = widget.origSerializeValue
|
||||
|
||||
delete widget.origType
|
||||
delete widget.origComputeSize
|
||||
delete widget.origSerializeValue
|
||||
|
||||
// Hide any linked widgets, e.g. seed+seedControl
|
||||
if (widget.linkedWidgets) {
|
||||
for (const w of widget.linkedWidgets) {
|
||||
showWidget(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a widget to an input slot.
|
||||
* @deprecated Widget to socket conversion is no longer necessary, as they co-exist now.
|
||||
* @param node The node to convert the widget to an input slot for.
|
||||
* @param widget The widget to convert to an input slot.
|
||||
* @returns The input slot that was converted from the widget or undefined if the widget is not found.
|
||||
*/
|
||||
export function convertToInput(
|
||||
node: LGraphNode,
|
||||
widget: IWidget,
|
||||
config: InputSpec
|
||||
): INodeInputSlot {
|
||||
hideWidget(node, widget)
|
||||
|
||||
const { type } = getWidgetType(config)
|
||||
|
||||
// Add input and store widget config for creating on primitive node
|
||||
const [oldWidth, oldHeight] = node.size
|
||||
const inputIsOptional = !!widget.options?.inputIsOptional
|
||||
const input = node.addInput(widget.name, type, {
|
||||
widget: { name: widget.name, [GET_CONFIG]: () => config },
|
||||
...(inputIsOptional ? { shape: RenderShape.HollowCircle } : {})
|
||||
})
|
||||
|
||||
for (const widget of node.widgets ?? []) {
|
||||
widget.last_y = (widget.last_y ?? 0) + LiteGraph.NODE_SLOT_HEIGHT
|
||||
}
|
||||
|
||||
// Restore original size but grow if needed
|
||||
node.setSize([
|
||||
Math.max(oldWidth, node.size[0]),
|
||||
Math.max(oldHeight, node.size[1])
|
||||
])
|
||||
return input
|
||||
}
|
||||
|
||||
function convertToWidget(node: LGraphNode, widget: IWidget) {
|
||||
showWidget(widget)
|
||||
const [oldWidth, oldHeight] = node.size
|
||||
node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name))
|
||||
|
||||
for (const widget of node.widgets ?? []) {
|
||||
widget.last_y = (widget.last_y ?? 0) - LiteGraph.NODE_SLOT_HEIGHT
|
||||
}
|
||||
|
||||
// Restore original size but grow if needed
|
||||
node.setSize([
|
||||
Math.max(oldWidth, node.size[0]),
|
||||
Math.max(oldHeight, node.size[1])
|
||||
])
|
||||
widget: IWidget
|
||||
): INodeInputSlot | undefined {
|
||||
console.warn(
|
||||
'Please remove call to convertToInput. Widget to socket conversion is no longer necessary, as they co-exist now.'
|
||||
)
|
||||
return node.inputs.find((slot) => slot.widget?.name === widget.name)
|
||||
}
|
||||
|
||||
function getWidgetType(config: InputSpec) {
|
||||
@@ -631,167 +534,13 @@ export function mergeIfValid(
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.WidgetInputs',
|
||||
settings: [
|
||||
{
|
||||
id: 'Comfy.NodeInputConversionSubmenus',
|
||||
name: 'In the node context menu, place the entries that convert between input/widget in sub-menus.',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
}
|
||||
],
|
||||
setup() {
|
||||
app.canvas.getWidgetLinkType = function (widget, node) {
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const nodeDef = nodeDefStore.nodeDefsByName[node.type]
|
||||
const input = nodeDef.inputs[widget.name]
|
||||
return input?.type
|
||||
}
|
||||
|
||||
app.canvas.linkConnector.events.addEventListener(
|
||||
'dropped-on-widget',
|
||||
(e) => {
|
||||
const { node, link, widget } = e.detail
|
||||
if (!node || !link || !widget) return
|
||||
|
||||
const nodeData = node.constructor.nodeData
|
||||
if (!nodeData) return
|
||||
const all = {
|
||||
...nodeData?.input?.required,
|
||||
...nodeData?.input?.optional
|
||||
}
|
||||
const inputSpec = all[widget.name]
|
||||
if (!inputSpec) return
|
||||
|
||||
const input = convertToInput(node, widget, inputSpec)
|
||||
link.node.connectSlots(link.fromSlot, node, input, link.fromReroute?.id)
|
||||
}
|
||||
)
|
||||
},
|
||||
async beforeRegisterNodeDef(nodeType, _nodeData, app) {
|
||||
// Add menu options to convert to/from widgets
|
||||
const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions
|
||||
// @ts-expect-error adding extra property
|
||||
nodeType.prototype.convertWidgetToInput = function (
|
||||
this: LGraphNode,
|
||||
widget: IWidget
|
||||
) {
|
||||
const config = getConfig.call(this, widget.name) ?? [
|
||||
widget.type,
|
||||
widget.options || {}
|
||||
]
|
||||
if (!isConvertibleWidget(widget, config)) return false
|
||||
if (widget.type?.startsWith(CONVERTED_TYPE)) return false
|
||||
convertToInput(this, widget, config)
|
||||
return true
|
||||
}
|
||||
|
||||
nodeType.prototype.getExtraSlotMenuOptions = function (
|
||||
this: LGraphNode,
|
||||
slot: IFoundSlot
|
||||
) {
|
||||
if (!slot.input || !slot.input.widget) return []
|
||||
|
||||
const widget = this.widgets?.find(
|
||||
(w) => w.name === slot.input?.widget?.name
|
||||
nodeType.prototype.convertWidgetToInput = function (this: LGraphNode) {
|
||||
console.warn(
|
||||
'Please remove call to convertWidgetToInput. Widget to socket conversion is no longer necessary, as they co-exist now.'
|
||||
)
|
||||
if (!widget) return []
|
||||
return [
|
||||
{
|
||||
content: `Convert to widget`,
|
||||
callback: () => convertToWidget(this, widget)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
nodeType.prototype.getExtraMenuOptions = function (
|
||||
this: LGraphNode,
|
||||
_,
|
||||
options
|
||||
) {
|
||||
const r = origGetExtraMenuOptions
|
||||
? // @ts-expect-error fixme ts strict error
|
||||
origGetExtraMenuOptions.apply(this, arguments)
|
||||
: undefined
|
||||
|
||||
const getPointerCanvasPos = () => {
|
||||
const pos = this.graph?.list_of_graphcanvas?.at(0)?.graph_mouse
|
||||
return pos ? { canvasX: pos[0], canvasY: pos[1] } : undefined
|
||||
}
|
||||
|
||||
if (this.widgets) {
|
||||
const { canvasX = 0, canvasY = 0 } = getPointerCanvasPos() ?? {}
|
||||
const widget = this.getWidgetOnPos(canvasX, canvasY)
|
||||
// @ts-expect-error custom widget type
|
||||
if (widget && widget.type !== CONVERTED_TYPE) {
|
||||
const config = getConfig.call(this, widget.name) ?? [
|
||||
widget.type,
|
||||
widget.options || {}
|
||||
]
|
||||
if (isConvertibleWidget(widget, config)) {
|
||||
options.push({
|
||||
content: `Convert ${widget.name} to input`,
|
||||
callback: () => convertToInput(this, widget, config) && false
|
||||
})
|
||||
}
|
||||
}
|
||||
let toInput = []
|
||||
let toWidget = []
|
||||
for (const w of this.widgets) {
|
||||
if (w.options?.forceInput) {
|
||||
continue
|
||||
}
|
||||
// @ts-expect-error custom widget type
|
||||
if (w.type === CONVERTED_TYPE) {
|
||||
toWidget.push({
|
||||
// @ts-expect-error never
|
||||
content: `Convert ${w.name} to widget`,
|
||||
callback: () => convertToWidget(this, w)
|
||||
})
|
||||
} else {
|
||||
const config = getConfig.call(this, w.name) ?? [
|
||||
w.type,
|
||||
w.options || {}
|
||||
]
|
||||
if (isConvertibleWidget(w, config)) {
|
||||
toInput.push({
|
||||
content: `Convert ${w.name} to input`,
|
||||
callback: () => convertToInput(this, w, config)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Convert.. main menu
|
||||
if (toInput.length) {
|
||||
if (useSettingStore().get('Comfy.NodeInputConversionSubmenus')) {
|
||||
options.push({
|
||||
content: 'Convert Widget to Input',
|
||||
submenu: {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
options: toInput
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
options.push(...toInput, null)
|
||||
}
|
||||
}
|
||||
if (toWidget.length) {
|
||||
if (useSettingStore().get('Comfy.NodeInputConversionSubmenus')) {
|
||||
options.push({
|
||||
content: 'Convert Input to Widget',
|
||||
submenu: {
|
||||
options: toWidget
|
||||
}
|
||||
})
|
||||
} else {
|
||||
options.push(...toWidget, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
return false
|
||||
}
|
||||
|
||||
nodeType.prototype.onGraphConfigured = useChainCallback(
|
||||
@@ -808,9 +557,7 @@ app.registerExtension({
|
||||
}
|
||||
|
||||
const w = this.widgets?.find((w) => w.name === name)
|
||||
if (w) {
|
||||
hideWidget(this, w)
|
||||
} else {
|
||||
if (!w) {
|
||||
this.removeInput(this.inputs.findIndex((i) => i === input))
|
||||
}
|
||||
}
|
||||
@@ -818,24 +565,6 @@ app.registerExtension({
|
||||
}
|
||||
)
|
||||
|
||||
nodeType.prototype.onNodeCreated = useChainCallback(
|
||||
nodeType.prototype.onNodeCreated,
|
||||
function (this: LGraphNode) {
|
||||
// When node is created, convert any force/default inputs
|
||||
if (!app.configuringGraph && this.widgets) {
|
||||
for (const w of this.widgets) {
|
||||
if (w?.options?.forceInput || w?.options?.defaultInput) {
|
||||
const config = getConfig.call(this, w.name) ?? [
|
||||
w.type,
|
||||
w.options || {}
|
||||
]
|
||||
convertToInput(this, w, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
nodeType.prototype.onConfigure = useChainCallback(
|
||||
nodeType.prototype.onConfigure,
|
||||
function (this: LGraphNode) {
|
||||
@@ -845,10 +574,6 @@ app.registerExtension({
|
||||
if (input.widget && !input.widget[GET_CONFIG]) {
|
||||
const name = input.widget.name
|
||||
input.widget[GET_CONFIG] = () => getConfig.call(this, name)
|
||||
const w = this.widgets?.find((w) => w.name === name)
|
||||
if (w) {
|
||||
hideWidget(this, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,8 +209,6 @@
|
||||
"Shapes": "Shapes",
|
||||
"Bypass": "Bypass",
|
||||
"Copy (Clipspace)": "Copy (Clipspace)",
|
||||
"Convert Widget to Input": "Convert Widget to Input",
|
||||
"Convert Input to Widget": "Convert Input to Widget",
|
||||
"Add Node": "Add Node",
|
||||
"Add Group": "Add Group",
|
||||
"Convert to Group Node": "Convert to Group Node",
|
||||
@@ -219,9 +217,6 @@
|
||||
"Save Selected as Template": "Save Selected as Template",
|
||||
"Node Templates": "Node Templates",
|
||||
"Manage": "Manage",
|
||||
"Convert ": "Convert ",
|
||||
" to input": " to input",
|
||||
" to widget": " to widget",
|
||||
"Search": "Search"
|
||||
},
|
||||
"icon": {
|
||||
@@ -695,7 +690,6 @@
|
||||
"BrushAdjustment": "Brush Adjustment",
|
||||
"NewEditor": "New Editor",
|
||||
"ModelLibrary": "Model Library",
|
||||
"NodeInputConversionSubmenus": "Node Input Conversion Submenus",
|
||||
"NodeLibrary": "Node Library",
|
||||
"Node Search Box": "Node Search Box",
|
||||
"Pointer": "Pointer",
|
||||
|
||||
@@ -189,9 +189,6 @@
|
||||
"Hide built-in": "Hide built-in"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeInputConversionSubmenus": {
|
||||
"name": "In the node context menu, place the entries that convert between input/widget in sub-menus."
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl": {
|
||||
"name": "Node search box implementation",
|
||||
"options": {
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
"yellow": "Amarillo"
|
||||
},
|
||||
"contextMenu": {
|
||||
" to input": " a entrada",
|
||||
" to widget": " a widget",
|
||||
"Add Group": "Agregar Grupo",
|
||||
"Add Group For Selected Nodes": "Agregar Grupo para Nodos Seleccionados",
|
||||
"Add Node": "Agregar Nodo",
|
||||
@@ -29,9 +27,6 @@
|
||||
"Clone": "Clonar",
|
||||
"Collapse": "Colapsar",
|
||||
"Colors": "Colores",
|
||||
"Convert ": "Convertir ",
|
||||
"Convert Input to Widget": "Convertir Entrada a Widget",
|
||||
"Convert Widget to Input": "Convertir Widget a Entrada",
|
||||
"Convert to Group Node": "Convertir en Nodo de Grupo",
|
||||
"Copy (Clipspace)": "Copiar (Espacio de Clip)",
|
||||
"Expand": "Expandir",
|
||||
@@ -836,7 +831,6 @@
|
||||
"Node": "Nodo",
|
||||
"Node Search Box": "Caja de Búsqueda de Nodo",
|
||||
"Node Widget": "Widget de Nodo",
|
||||
"NodeInputConversionSubmenus": "Submenús de Conversión de Entrada de Nodo",
|
||||
"NodeLibrary": "Biblioteca de Nodos",
|
||||
"Pointer": "Puntero",
|
||||
"Queue": "Cola",
|
||||
|
||||
@@ -157,9 +157,6 @@
|
||||
"Show all": "Mostrar todo"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeInputConversionSubmenus": {
|
||||
"name": "En el menú contextual del nodo, coloque las entradas que convierten entre entrada/widget en submenús."
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl": {
|
||||
"name": "Implementación de la caja de búsqueda de nodos",
|
||||
"options": {
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
"yellow": "Jaune"
|
||||
},
|
||||
"contextMenu": {
|
||||
" to input": " en entrée",
|
||||
" to widget": " en widget",
|
||||
"Add Group": "Ajouter un Groupe",
|
||||
"Add Group For Selected Nodes": "Ajouter un Groupe pour les Nœuds Sélectionnés",
|
||||
"Add Node": "Ajouter un Nœud",
|
||||
@@ -29,9 +27,6 @@
|
||||
"Clone": "Cloner",
|
||||
"Collapse": "Réduire",
|
||||
"Colors": "Couleurs",
|
||||
"Convert ": "Convertir ",
|
||||
"Convert Input to Widget": "Convertir l'Entrée en Widget",
|
||||
"Convert Widget to Input": "Convertir le Widget en Entrée",
|
||||
"Convert to Group Node": "Convertir en Nœud de Groupe",
|
||||
"Copy (Clipspace)": "Copier (Clipspace)",
|
||||
"Expand": "Développer",
|
||||
@@ -836,7 +831,6 @@
|
||||
"Node": "Nœud",
|
||||
"Node Search Box": "Boîte de Recherche de Nœud",
|
||||
"Node Widget": "Widget de Nœud",
|
||||
"NodeInputConversionSubmenus": "Sous-menus de Conversion d'Entrée de Nœud",
|
||||
"NodeLibrary": "Bibliothèque de Nœuds",
|
||||
"Pointer": "Pointeur",
|
||||
"Queue": "File d'Attente",
|
||||
|
||||
@@ -157,9 +157,6 @@
|
||||
"Show all": "Afficher tout"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeInputConversionSubmenus": {
|
||||
"name": "Dans le menu contextuel du nœud, placez les entrées qui convertissent entre l'entrée/widget dans des sous-menus."
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl": {
|
||||
"name": "Implémentation de la boîte de recherche de nœud",
|
||||
"options": {
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
"yellow": "黄色"
|
||||
},
|
||||
"contextMenu": {
|
||||
" to input": " 入力へ",
|
||||
" to widget": " ウィジェットへ",
|
||||
"Add Group": "グループを追加",
|
||||
"Add Group For Selected Nodes": "選択したノードのグループを追加",
|
||||
"Add Node": "ノードを追加",
|
||||
@@ -29,9 +27,6 @@
|
||||
"Clone": "クローン",
|
||||
"Collapse": "折りたたむ",
|
||||
"Colors": "色",
|
||||
"Convert ": "変換 ",
|
||||
"Convert Input to Widget": "入力をウィジェットに変換",
|
||||
"Convert Widget to Input": "ウィジェットを入力に変換",
|
||||
"Convert to Group Node": "グループノードに変換",
|
||||
"Copy (Clipspace)": "コピー (Clipspace)",
|
||||
"Expand": "展開",
|
||||
@@ -836,7 +831,6 @@
|
||||
"Node": "ノード",
|
||||
"Node Search Box": "ノード検索ボックス",
|
||||
"Node Widget": "ノードウィジェット",
|
||||
"NodeInputConversionSubmenus": "ノード入力変換サブメニュー",
|
||||
"NodeLibrary": "ノードライブラリ",
|
||||
"Pointer": "ポインタ",
|
||||
"Queue": "キュー",
|
||||
|
||||
@@ -157,9 +157,6 @@
|
||||
"Show all": "すべて表示"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeInputConversionSubmenus": {
|
||||
"name": "ノードのコンテキストメニューに、入力/ウィジェット間の変換を行うエントリをサブメニューに配置します。"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl": {
|
||||
"name": "ノード検索ボックスの実装",
|
||||
"options": {
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
"yellow": "노란색"
|
||||
},
|
||||
"contextMenu": {
|
||||
" to input": " 위젯을 입력으로",
|
||||
" to widget": " 입력을 위젯으로",
|
||||
"Add Group": "그룹 추가",
|
||||
"Add Group For Selected Nodes": "선택한 노드 그룹 추가",
|
||||
"Add Node": "노드 추가",
|
||||
@@ -29,9 +27,6 @@
|
||||
"Clone": "복제",
|
||||
"Collapse": "접기",
|
||||
"Colors": "색상",
|
||||
"Convert ": "[변환] ",
|
||||
"Convert Input to Widget": "입력을 위젯으로 변환",
|
||||
"Convert Widget to Input": "위젯을 입력으로 변환",
|
||||
"Convert to Group Node": "그룹 노드로 변환",
|
||||
"Copy (Clipspace)": "복사 (Clipspace)",
|
||||
"Expand": "확장",
|
||||
@@ -836,7 +831,6 @@
|
||||
"Node": "노드",
|
||||
"Node Search Box": "노드 검색 상자",
|
||||
"Node Widget": "노드 위젯",
|
||||
"NodeInputConversionSubmenus": "노드 입력 변환 하위 메뉴",
|
||||
"NodeLibrary": "노드 라이브러리",
|
||||
"Pointer": "포인터",
|
||||
"Queue": "실행 큐",
|
||||
|
||||
@@ -157,9 +157,6 @@
|
||||
"Show all": "모두 표시"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeInputConversionSubmenus": {
|
||||
"name": "노드 컨텍스트 메뉴에서 입력/위젯 간 변환 항목을 하위 메뉴에 배치합니다."
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl": {
|
||||
"name": "노드 검색 상자 구현",
|
||||
"options": {
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
"yellow": "Жёлтый"
|
||||
},
|
||||
"contextMenu": {
|
||||
" to input": " во вход",
|
||||
" to widget": " в виджет",
|
||||
"Add Group": "Добавить группу",
|
||||
"Add Group For Selected Nodes": "Добавить группу для выбранных узлов",
|
||||
"Add Node": "Добавить узел",
|
||||
@@ -29,9 +27,6 @@
|
||||
"Clone": "Клонировать",
|
||||
"Collapse": "Свернуть",
|
||||
"Colors": "Цвета",
|
||||
"Convert ": "Преобразовать ",
|
||||
"Convert Input to Widget": "Преобразовать вход в виджет",
|
||||
"Convert Widget to Input": "Преобразовать виджет во вход",
|
||||
"Convert to Group Node": "Преобразовать в групповой узел",
|
||||
"Copy (Clipspace)": "Копировать (Clipspace)",
|
||||
"Expand": "Развернуть",
|
||||
@@ -836,7 +831,6 @@
|
||||
"Node": "Нода",
|
||||
"Node Search Box": "Поисковая строка нод",
|
||||
"Node Widget": "Виджет ноды",
|
||||
"NodeInputConversionSubmenus": "Подменю преобразования ввода ноды",
|
||||
"NodeLibrary": "Библиотека нод",
|
||||
"Pointer": "Указатель",
|
||||
"Queue": "Очередь",
|
||||
|
||||
@@ -157,9 +157,6 @@
|
||||
"Show all": "Показать все"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeInputConversionSubmenus": {
|
||||
"name": "В контекстном меню ноды разместите элементы, которые конвертируют между вводом/виджетом в подменю."
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl": {
|
||||
"name": "Реализация поискового поля нод",
|
||||
"options": {
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
"yellow": "黄色"
|
||||
},
|
||||
"contextMenu": {
|
||||
" to input": " 为输入",
|
||||
" to widget": " 为控件",
|
||||
"Add Group": "添加组",
|
||||
"Add Group For Selected Nodes": "为选定节点添加组",
|
||||
"Add Node": "添加节点",
|
||||
@@ -29,9 +27,6 @@
|
||||
"Clone": "克隆",
|
||||
"Collapse": "折叠",
|
||||
"Colors": "颜色",
|
||||
"Convert ": "转换 ",
|
||||
"Convert Input to Widget": "将输入转换为控件",
|
||||
"Convert Widget to Input": "将控件转换为输入",
|
||||
"Convert to Group Node": "转换为组节点",
|
||||
"Copy (Clipspace)": "复制 (Clipspace)",
|
||||
"Expand": "展开",
|
||||
@@ -836,7 +831,6 @@
|
||||
"Node": "节点",
|
||||
"Node Search Box": "节点搜索框",
|
||||
"Node Widget": "节点组件",
|
||||
"NodeInputConversionSubmenus": "节点输入转换子菜单",
|
||||
"NodeLibrary": "节点库",
|
||||
"Pointer": "指针",
|
||||
"Queue": "队列",
|
||||
|
||||
@@ -157,9 +157,6 @@
|
||||
"Show all": "显示全部"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeInputConversionSubmenus": {
|
||||
"name": "在节点上下文菜单中,将输入/组件之间转换的条目放置在子菜单中。"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl": {
|
||||
"name": "节点搜索框",
|
||||
"options": {
|
||||
|
||||
@@ -364,7 +364,6 @@ const zSettings = z.record(z.any()).and(
|
||||
z.string(),
|
||||
zBookmarkCustomization
|
||||
),
|
||||
'Comfy.NodeInputConversionSubmenus': z.boolean(),
|
||||
'Comfy.LinkRelease.Action': zLinkReleaseTriggerAction,
|
||||
'Comfy.LinkRelease.ActionShift': zLinkReleaseTriggerAction,
|
||||
'Comfy.ModelLibrary.AutoLoadAll': z.boolean(),
|
||||
|
||||
@@ -66,7 +66,7 @@ import {
|
||||
import { $el, ComfyUI } from './ui'
|
||||
import { ComfyAppMenu } from './ui/menu/index'
|
||||
import { clone } from './utils'
|
||||
import { type ComfyWidgetConstructor, ComfyWidgets } from './widgets'
|
||||
import { type ComfyWidgetConstructor } from './widgets'
|
||||
|
||||
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
|
||||
|
||||
@@ -180,10 +180,7 @@ export class ComfyApp {
|
||||
* @deprecated Use useWidgetStore().widgets instead
|
||||
*/
|
||||
get widgets(): Record<string, ComfyWidgetConstructor> {
|
||||
if (this.vueAppReady) {
|
||||
return useWidgetStore().widgets
|
||||
}
|
||||
return ComfyWidgets
|
||||
return Object.fromEntries(useWidgetStore().widgets.entries())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -132,12 +132,7 @@ abstract class BaseDOMWidgetImpl<V extends object | string>
|
||||
}
|
||||
|
||||
isVisible(): boolean {
|
||||
return (
|
||||
!_.isNil(this.computedHeight) &&
|
||||
this.computedHeight > 0 &&
|
||||
!['converted-widget', 'hidden'].includes(this.type) &&
|
||||
!this.node.collapsed
|
||||
)
|
||||
return !['hidden'].includes(this.type) && this.node.isWidgetVisible(this)
|
||||
}
|
||||
|
||||
draw(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,23 +10,21 @@ import { ComfyWidgetConstructor, ComfyWidgets } from '@/scripts/widgets'
|
||||
|
||||
export const useWidgetStore = defineStore('widget', () => {
|
||||
const coreWidgets = ComfyWidgets
|
||||
const customWidgets = ref<Record<string, ComfyWidgetConstructor>>({})
|
||||
const widgets = computed(() => ({
|
||||
...customWidgets.value,
|
||||
...coreWidgets
|
||||
}))
|
||||
const customWidgets = ref<Map<string, ComfyWidgetConstructor>>(new Map())
|
||||
const widgets = computed<Map<string, ComfyWidgetConstructor>>(
|
||||
() => new Map([...customWidgets.value, ...Object.entries(coreWidgets)])
|
||||
)
|
||||
|
||||
function inputIsWidget(spec: InputSpecV2 | InputSpecV1) {
|
||||
const type = Array.isArray(spec) ? getInputSpecType(spec) : spec.type
|
||||
return type in widgets.value
|
||||
return widgets.value.has(type)
|
||||
}
|
||||
|
||||
function registerCustomWidgets(
|
||||
newWidgets: Record<string, ComfyWidgetConstructor>
|
||||
) {
|
||||
customWidgets.value = {
|
||||
...customWidgets.value,
|
||||
...newWidgets
|
||||
for (const [type, widget] of Object.entries(newWidgets)) {
|
||||
customWidgets.value.set(type, widget)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
12
src/types/litegraph-augmentation.d.ts
vendored
@@ -37,6 +37,8 @@ declare module '@comfyorg/litegraph/dist/types/widgets' {
|
||||
/**
|
||||
* Whether the widget defaults to input state. Can still be converted back
|
||||
* to widget state.
|
||||
* @deprecated Widget to input conversion is no longer necessary, as they co-exist now.
|
||||
* This option no longer has any effect.
|
||||
*/
|
||||
defaultInput?: boolean
|
||||
}
|
||||
@@ -60,16 +62,6 @@ declare module '@comfyorg/litegraph/dist/types/widgets' {
|
||||
* See extensions/core/dynamicPrompts.ts
|
||||
*/
|
||||
dynamicPrompts?: boolean
|
||||
|
||||
/**
|
||||
* Widget conversion fields
|
||||
*/
|
||||
origType?: string
|
||||
origComputeSize?: (width: number) => Size
|
||||
origSerializeValue?: (
|
||||
node: LGraphNode,
|
||||
index: number
|
||||
) => Promise<unknown> | unknown
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,13 @@ export const graphToPrompt = async (
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all unconnected widget input slots
|
||||
for (const node of workflow.nodes) {
|
||||
node.inputs = node.inputs?.filter(
|
||||
(input) => !(input.widget && input.link === null)
|
||||
)
|
||||
}
|
||||
|
||||
const output: ComfyApiWorkflow = {}
|
||||
// Process nodes in order of execution
|
||||
for (const outerNode of graph.computeExecutionOrder(false)) {
|
||||
|
||||