mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-13 11:11:00 +00:00
Compare commits
2 Commits
docs/weekl
...
codex/clou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59fceb3098 | ||
|
|
3ae9a83c4c |
@@ -9,6 +9,9 @@ import type { HasInitialMinSize } from '@/services/litegraphService'
|
||||
|
||||
setActivePinia(createTestingPinia())
|
||||
type DynamicInputs = ('INT' | 'STRING' | 'IMAGE' | DynamicInputs)[][]
|
||||
type TestAutogrowNode = LGraphNode & {
|
||||
comfyDynamic: { autogrow: Record<string, unknown> }
|
||||
}
|
||||
|
||||
const { addNodeInput } = useLitegraphService()
|
||||
|
||||
@@ -182,6 +185,20 @@ describe('Autogrow', () => {
|
||||
await nextTick()
|
||||
expect(node.inputs.length).toBe(5)
|
||||
})
|
||||
test('Removing a connection ignores stale autogrow callbacks after group removal', async () => {
|
||||
const graph = new LGraph()
|
||||
const node = testNode()
|
||||
graph.add(node)
|
||||
addAutogrow(node, { min: 1, input: inputsSpec, prefix: 'test' })
|
||||
|
||||
connectInput(node, 0, graph)
|
||||
expect(node.inputs.length).toBe(2)
|
||||
|
||||
node.disconnectInput(0)
|
||||
delete (node as TestAutogrowNode).comfyDynamic.autogrow['0']
|
||||
|
||||
await expect(nextTick()).resolves.toBeUndefined()
|
||||
})
|
||||
test('Can deserialize a complex node', async () => {
|
||||
const graph = new LGraph()
|
||||
const node = testNode()
|
||||
|
||||
@@ -460,7 +460,10 @@ function autogrowInputDisconnected(index: number, node: AutogrowNode) {
|
||||
const input = node.inputs[index]
|
||||
if (!input) return
|
||||
const groupName = input.name.slice(0, input.name.lastIndexOf('.'))
|
||||
const { min = 1, inputSpecs } = node.comfyDynamic.autogrow[groupName]
|
||||
const autogrowGroup = node.comfyDynamic.autogrow[groupName]
|
||||
if (!autogrowGroup) return
|
||||
|
||||
const { min = 1, inputSpecs } = autogrowGroup
|
||||
const ordinal = resolveAutogrowOrdinal(input.name, groupName, node)
|
||||
if (ordinal == undefined || ordinal + 1 < min) return
|
||||
|
||||
|
||||
@@ -1,51 +1,16 @@
|
||||
import { shallowReactive } from 'vue'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LLink } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LLink } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
|
||||
import { applyFirstWidgetValueToGraph } from './widgetValuePropagation'
|
||||
|
||||
function applyToGraph(this: LGraphNode, extraLinks: LLink[] = []) {
|
||||
if (!this.outputs[0].links?.length || !this.graph) return
|
||||
|
||||
const links = [
|
||||
...this.outputs[0].links.map((l) => this.graph!.links[l]),
|
||||
...extraLinks
|
||||
]
|
||||
let v = this.widgets?.[0].value
|
||||
// For each output link copy our value over the original widget value
|
||||
for (const linkInfo of links) {
|
||||
const node = this.graph?.getNodeById(linkInfo.target_id)
|
||||
const input = node?.inputs[linkInfo.target_slot]
|
||||
if (!input) {
|
||||
console.warn('Unable to resolve node or input for link', linkInfo)
|
||||
continue
|
||||
}
|
||||
|
||||
const widgetName = input.widget?.name
|
||||
if (!widgetName) {
|
||||
console.warn('Invalid widget or widget name', input.widget)
|
||||
continue
|
||||
}
|
||||
|
||||
const widget = node.widgets?.find((w) => w.name === widgetName)
|
||||
if (!widget) {
|
||||
console.warn(`Unable to find widget "${widgetName}" on node [${node.id}]`)
|
||||
continue
|
||||
}
|
||||
|
||||
widget.value = v
|
||||
widget.callback?.(
|
||||
widget.value,
|
||||
app.canvas,
|
||||
node,
|
||||
app.canvas.graph_mouse,
|
||||
{} as CanvasPointerEvent
|
||||
)
|
||||
}
|
||||
applyFirstWidgetValueToGraph(this, extraLinks)
|
||||
}
|
||||
|
||||
function onCustomComboCreated(this: LGraphNode) {
|
||||
|
||||
@@ -26,6 +26,8 @@ import { CONFIG, GET_CONFIG } from '@/services/litegraphService'
|
||||
import { mergeInputSpec } from '@/utils/nodeDefUtil'
|
||||
import { applyTextReplacements } from '@/utils/searchAndReplace'
|
||||
|
||||
import { applyFirstWidgetValueToGraph } from './widgetValuePropagation'
|
||||
|
||||
const replacePropertyName = 'Run widget replace on values'
|
||||
export class PrimitiveNode extends LGraphNode {
|
||||
controlValues?: TWidgetValue[]
|
||||
@@ -43,49 +45,15 @@ export class PrimitiveNode extends LGraphNode {
|
||||
}
|
||||
|
||||
override applyToGraph(extraLinks: LLink[] = []) {
|
||||
if (!this.outputs[0].links?.length || !this.graph) return
|
||||
const sourceWidget = this.widgets?.[0]
|
||||
const graph = this.graph
|
||||
if (!sourceWidget || !graph) return
|
||||
|
||||
const links = [
|
||||
...this.outputs[0].links.map((l) => this.graph!.links[l]),
|
||||
...extraLinks
|
||||
]
|
||||
let v = this.widgets?.[0].value
|
||||
let v = sourceWidget.value
|
||||
if (v && this.properties[replacePropertyName]) {
|
||||
v = applyTextReplacements(this.graph, v as string)
|
||||
}
|
||||
|
||||
// For each output link copy our value over the original widget value
|
||||
for (const linkInfo of links) {
|
||||
const node = this.graph?.getNodeById(linkInfo.target_id)
|
||||
const input = node?.inputs[linkInfo.target_slot]
|
||||
if (!input) {
|
||||
console.warn('Unable to resolve node or input for link', linkInfo)
|
||||
continue
|
||||
}
|
||||
|
||||
const widgetName = input.widget?.name
|
||||
if (!widgetName) {
|
||||
console.warn('Invalid widget or widget name', input.widget)
|
||||
continue
|
||||
}
|
||||
|
||||
const widget = node.widgets?.find((w) => w.name === widgetName)
|
||||
if (!widget) {
|
||||
console.warn(
|
||||
`Unable to find widget "${widgetName}" on node [${node.id}]`
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
widget.value = v
|
||||
widget.callback?.(
|
||||
widget.value,
|
||||
app.canvas,
|
||||
node,
|
||||
app.canvas.graph_mouse,
|
||||
{} as CanvasPointerEvent
|
||||
)
|
||||
v = applyTextReplacements(graph, v as string)
|
||||
}
|
||||
applyFirstWidgetValueToGraph(this, extraLinks, () => v)
|
||||
}
|
||||
|
||||
override refreshComboInNode() {
|
||||
@@ -98,7 +66,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
if (!widget.options.values.includes(widget.value as string)) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
widget.value = widget.options.values[0]
|
||||
;(widget.callback as Function)(widget.value)
|
||||
widget.callback?.(widget.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,7 +241,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
)
|
||||
if (this.widgets?.[1]) widget.linkedWidgets = [this.widgets[1]]
|
||||
|
||||
let filter = this.widgets_values?.[2]
|
||||
const filter = this.widgets_values?.[2]
|
||||
if (filter && this.widgets && this.widgets.length === 3) {
|
||||
this.widgets[2].value = filter
|
||||
}
|
||||
|
||||
125
src/extensions/core/widgetValuePropagation.test.ts
Normal file
125
src/extensions/core/widgetValuePropagation.test.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
LLink
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
canvas: {
|
||||
graph_mouse: {}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
import { applyFirstWidgetValueToGraph } from './widgetValuePropagation'
|
||||
|
||||
type SourceNode = Pick<LGraphNode, 'graph' | 'outputs' | 'widgets'>
|
||||
|
||||
function createWidget(
|
||||
name: string,
|
||||
value: unknown,
|
||||
callback = vi.fn()
|
||||
): IBaseWidget {
|
||||
return {
|
||||
name,
|
||||
value,
|
||||
callback
|
||||
} as unknown as IBaseWidget
|
||||
}
|
||||
|
||||
function createTargetNode(
|
||||
widget: IBaseWidget,
|
||||
id = 7
|
||||
): Pick<LGraphNode, 'id' | 'inputs' | 'widgets'> {
|
||||
return {
|
||||
id,
|
||||
inputs: [
|
||||
{
|
||||
widget: { name: widget.name }
|
||||
} as unknown as INodeInputSlot
|
||||
],
|
||||
widgets: [widget]
|
||||
}
|
||||
}
|
||||
|
||||
function createLink(targetId: LLink['target_id'], targetSlot = 0): LLink {
|
||||
return {
|
||||
target_id: targetId,
|
||||
target_slot: targetSlot
|
||||
} as LLink
|
||||
}
|
||||
|
||||
function createSourceNode(options: {
|
||||
link: LLink
|
||||
targetNode: Pick<LGraphNode, 'id' | 'inputs' | 'widgets'>
|
||||
widgets?: IBaseWidget[]
|
||||
}): SourceNode {
|
||||
return {
|
||||
graph: {
|
||||
links: { 1: options.link },
|
||||
getNodeById: vi.fn((id: LLink['target_id']) =>
|
||||
id === options.targetNode.id ? options.targetNode : null
|
||||
)
|
||||
} as unknown as NonNullable<LGraphNode['graph']>,
|
||||
outputs: [{ links: [1] } as INodeOutputSlot],
|
||||
widgets: options.widgets ?? []
|
||||
}
|
||||
}
|
||||
|
||||
describe('applyFirstWidgetValueToGraph', () => {
|
||||
it('returns early when the source widget is missing', () => {
|
||||
const targetCallback = vi.fn()
|
||||
const targetWidget = createWidget('value', 'unchanged', targetCallback)
|
||||
const targetNode = createTargetNode(targetWidget)
|
||||
const sourceNode = createSourceNode({
|
||||
link: createLink(targetNode.id),
|
||||
targetNode
|
||||
})
|
||||
|
||||
expect(() => applyFirstWidgetValueToGraph(sourceNode)).not.toThrow()
|
||||
expect(targetWidget.value).toBe('unchanged')
|
||||
expect(targetCallback).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('propagates the first widget value to the linked widget', () => {
|
||||
const targetCallback = vi.fn()
|
||||
const targetWidget = createWidget('value', 'old', targetCallback)
|
||||
const targetNode = createTargetNode(targetWidget)
|
||||
const sourceNode = createSourceNode({
|
||||
link: createLink(targetNode.id),
|
||||
targetNode,
|
||||
widgets: [createWidget('source', 'new value')]
|
||||
})
|
||||
|
||||
applyFirstWidgetValueToGraph(sourceNode)
|
||||
|
||||
expect(targetWidget.value).toBe('new value')
|
||||
expect(targetCallback).toHaveBeenCalledOnce()
|
||||
expect(targetCallback).toHaveBeenCalledWith(
|
||||
'new value',
|
||||
expect.anything(),
|
||||
targetNode,
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
|
||||
it('applies a transform before propagating the widget value', () => {
|
||||
const targetWidget = createWidget('value', 'old')
|
||||
const targetNode = createTargetNode(targetWidget)
|
||||
const sourceNode = createSourceNode({
|
||||
link: createLink(targetNode.id),
|
||||
targetNode,
|
||||
widgets: [createWidget('source', 'draft')]
|
||||
})
|
||||
|
||||
applyFirstWidgetValueToGraph(sourceNode, [], (value) => `${value}-saved`)
|
||||
|
||||
expect(targetWidget.value).toBe('draft-saved')
|
||||
})
|
||||
})
|
||||
67
src/extensions/core/widgetValuePropagation.ts
Normal file
67
src/extensions/core/widgetValuePropagation.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { LLink } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
|
||||
import type { TWidgetValue } from '@/lib/litegraph/src/types/widgets'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
type SourceNode = Pick<LGraphNode, 'graph' | 'outputs' | 'widgets'>
|
||||
|
||||
export function applyFirstWidgetValueToGraph(
|
||||
node: SourceNode,
|
||||
extraLinks: LLink[] = [],
|
||||
transformValue?: (value: TWidgetValue) => TWidgetValue
|
||||
) {
|
||||
const output = node.outputs[0]
|
||||
if (!output?.links?.length || !node.graph) return
|
||||
|
||||
const sourceWidget = node.widgets?.[0]
|
||||
if (!sourceWidget) return
|
||||
|
||||
let value = sourceWidget.value
|
||||
if (transformValue) {
|
||||
value = transformValue(value)
|
||||
}
|
||||
|
||||
const graphMouse = app.canvas?.graph_mouse ?? ({} as CanvasPointerEvent)
|
||||
|
||||
const links = [
|
||||
...output.links.map((linkId) => node.graph!.links[linkId]),
|
||||
...extraLinks
|
||||
]
|
||||
|
||||
for (const linkInfo of links) {
|
||||
if (!linkInfo) continue
|
||||
|
||||
const targetNode = node.graph.getNodeById(linkInfo.target_id)
|
||||
const input = targetNode?.inputs[linkInfo.target_slot]
|
||||
if (!targetNode || !input) {
|
||||
console.warn('Unable to resolve node or input for link', linkInfo)
|
||||
continue
|
||||
}
|
||||
|
||||
const widgetName = input.widget?.name
|
||||
if (!widgetName) {
|
||||
console.warn('Invalid widget or widget name', input.widget)
|
||||
continue
|
||||
}
|
||||
|
||||
const targetWidget = targetNode.widgets?.find(
|
||||
(widget) => widget.name === widgetName
|
||||
)
|
||||
if (!targetWidget) {
|
||||
console.warn(
|
||||
`Unable to find widget "${widgetName}" on node [${targetNode.id}]`
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
targetWidget.value = value
|
||||
targetWidget.callback?.(
|
||||
targetWidget.value,
|
||||
app.canvas,
|
||||
targetNode,
|
||||
graphMouse,
|
||||
{} as CanvasPointerEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,11 @@ function makeOutput(
|
||||
}
|
||||
|
||||
describe(flattenNodeOutput, () => {
|
||||
it('returns empty array for nullish node output', () => {
|
||||
expect(flattenNodeOutput(['1', null])).toEqual([])
|
||||
expect(flattenNodeOutput(['1', undefined])).toEqual([])
|
||||
})
|
||||
|
||||
it('returns empty array for output with no known media types', () => {
|
||||
const result = flattenNodeOutput(['1', makeOutput({ unknown: 'hello' })])
|
||||
expect(result).toEqual([])
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ResultItemImpl } from '@/stores/queueStore'
|
||||
|
||||
export function flattenNodeOutput([nodeId, nodeOutput]: [
|
||||
string | number,
|
||||
NodeExecutionOutput
|
||||
NodeExecutionOutput | null | undefined
|
||||
]): ResultItemImpl[] {
|
||||
return parseNodeOutput(nodeId, nodeOutput)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ function makeOutput(
|
||||
}
|
||||
|
||||
describe(parseNodeOutput, () => {
|
||||
it('returns empty array for nullish node output', () => {
|
||||
expect(parseNodeOutput('1', null)).toEqual([])
|
||||
expect(parseNodeOutput('1', undefined)).toEqual([])
|
||||
})
|
||||
|
||||
it('returns empty array for output with no known media types', () => {
|
||||
const result = parseNodeOutput('1', makeOutput({ text: 'hello' }))
|
||||
expect(result).toEqual([])
|
||||
@@ -152,6 +157,22 @@ describe(parseNodeOutput, () => {
|
||||
})
|
||||
|
||||
describe(parseTaskOutput, () => {
|
||||
it('ignores nullish node outputs', () => {
|
||||
const taskOutput: Record<string, NodeExecutionOutput | null | undefined> = {
|
||||
'1': null,
|
||||
'2': undefined,
|
||||
'3': makeOutput({
|
||||
images: [{ filename: 'a.png', subfolder: '', type: 'output' }]
|
||||
})
|
||||
}
|
||||
|
||||
const result = parseTaskOutput(taskOutput)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].nodeId).toBe('3')
|
||||
expect(result[0].filename).toBe('a.png')
|
||||
})
|
||||
|
||||
it('flattens across multiple nodes', () => {
|
||||
const taskOutput: Record<string, NodeExecutionOutput> = {
|
||||
'1': makeOutput({
|
||||
|
||||
@@ -29,8 +29,10 @@ function isResultItem(item: unknown): item is ResultItem {
|
||||
|
||||
export function parseNodeOutput(
|
||||
nodeId: string | number,
|
||||
nodeOutput: NodeExecutionOutput
|
||||
nodeOutput: NodeExecutionOutput | null | undefined
|
||||
): ResultItemImpl[] {
|
||||
if (!nodeOutput) return []
|
||||
|
||||
return Object.entries(nodeOutput)
|
||||
.filter(([key, value]) => !METADATA_KEYS.has(key) && Array.isArray(value))
|
||||
.flatMap(([mediaType, items]) =>
|
||||
@@ -41,7 +43,7 @@ export function parseNodeOutput(
|
||||
}
|
||||
|
||||
export function parseTaskOutput(
|
||||
taskOutput: Record<string, NodeExecutionOutput>
|
||||
taskOutput: Record<string, NodeExecutionOutput | null | undefined>
|
||||
): ResultItemImpl[] {
|
||||
return Object.entries(taskOutput).flatMap(([nodeId, nodeOutput]) =>
|
||||
parseNodeOutput(nodeId, nodeOutput)
|
||||
|
||||
Reference in New Issue
Block a user