Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
401 changes: 387 additions & 14 deletions src/core/graph/widgets/dynamicWidgets.ts

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion src/extensions/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import './groupNodeManage'
import './groupOptions'
import './load3d'
import './maskeditor'
import './matchType'
import './nodeTemplates'
import './noteNode'
import './previewAny'
Expand Down
155 changes: 0 additions & 155 deletions src/extensions/core/matchType.ts

This file was deleted.

14 changes: 1 addition & 13 deletions src/lib/litegraph/src/LGraphNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ export class LGraphNode
block_delete?: boolean
selected?: boolean
showAdvanced?: boolean
comfyMatchType?: Record<string, Record<string, string>>

declare comfyClass?: string
declare isVirtualNode?: boolean
Expand Down Expand Up @@ -1650,19 +1651,6 @@ export class LGraphNode
this.onInputRemoved?.(slot, slot_info[0])
this.setDirtyCanvas(true, true)
}
spliceInputs(
startIndex: number,
deleteCount = -1,
...toAdd: INodeInputSlot[]
): INodeInputSlot[] {
if (deleteCount < 0) return this.inputs.splice(startIndex)
const ret = this.inputs.splice(startIndex, deleteCount, ...toAdd)
this.inputs.slice(startIndex).forEach((input, index) => {
const link = input.link && this.graph?.links?.get(input.link)
if (link) link.target_slot = startIndex + index
})
return ret
}

/**
* computes the minimum size of a node according to its inputs and output slots
Expand Down
21 changes: 14 additions & 7 deletions src/schemas/nodeDefSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ const zRemoteWidgetConfig = z.object({
timeout: z.number().gte(0).optional(),
max_retries: z.number().gte(0).optional()
})
const zWidgetTemplate = z.object({
template_id: z.string(),
allowed_types: z.string().optional()
})
const zMultiSelectOption = z.object({
placeholder: z.string().optional(),
chip: z.boolean().optional()
Expand All @@ -32,7 +28,6 @@ export const zBaseInputOptions = z
hidden: z.boolean().optional(),
advanced: z.boolean().optional(),
widgetType: z.string().optional(),
template: zWidgetTemplate.optional(),
/** Backend-only properties. */
rawLink: z.boolean().optional(),
lazy: z.boolean().optional()
Expand Down Expand Up @@ -186,7 +181,7 @@ const zInputSpec = z.union([
zCustomInputSpec
])

const zComfyInputsSpec = z.object({
export const zComfyInputsSpec = z.object({
required: z.record(zInputSpec).optional(),
optional: z.record(zInputSpec).optional(),
// Frontend repo is not using it, but some custom nodes are using the
Expand Down Expand Up @@ -230,9 +225,21 @@ export const zComfyNodeDef = z.object({
input_order: z.record(z.array(z.string())).optional()
})

export const zAutogrowOptions = z.object({
...zBaseInputOptions.shape,
template: z.object({
input: zComfyInputsSpec,
names: z.array(z.string()).optional(),
max: z.number().optional(),
//Backend defines as mandatory with min 1, Frontend is more forgiving
min: z.number().optional(),
prefix: z.string().optional()
})
})

export const zDynamicComboInputSpec = z.tuple([
z.literal('COMFY_DYNAMICCOMBO_V3'),
zComboInputOptions.extend({
zBaseInputOptions.extend({
options: z.array(
z.object({
inputs: zComfyInputsSpec,
Expand Down
7 changes: 6 additions & 1 deletion src/services/litegraphService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useNodeCanvasImagePreview } from '@/composables/node/useNodeCanvasImage
import { useNodeImage, useNodeVideo } from '@/composables/node/useNodeImage'
import { addWidgetPromotionOptions } from '@/core/graph/subgraph/proxyWidgetUtils'
import { showSubgraphNodeDialog } from '@/core/graph/subgraph/useSubgraphNodeDialog'
import { applyDynamicInputs } from '@/core/graph/widgets/dynamicWidgets'
import { st, t } from '@/i18n'
import {
LGraphCanvas,
Expand Down Expand Up @@ -92,7 +93,11 @@ export const useLitegraphService = () => {
const widgetConstructor = widgetStore.widgets.get(
inputSpec.widgetType ?? inputSpec.type
)
if (widgetConstructor && !inputSpec.forceInput) return
if (
(widgetConstructor && !inputSpec.forceInput) ||
applyDynamicInputs(node, inputSpec)
)
return

node.addInput(inputName, inputSpec.type, {
shape: inputSpec.isOptional ? RenderShape.HollowCircle : undefined,
Expand Down
4 changes: 4 additions & 0 deletions src/utils/typeGuardUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@ export const isResultItemType = (
): value is ResultItemType => {
return value === 'input' || value === 'output' || value === 'temp'
}

export function isStrings(types: unknown[]): types is string[] {
return types.every((t) => typeof t === 'string')
}
90 changes: 89 additions & 1 deletion tests-ui/tests/widgets/dynamicCombo.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { describe, expect, test } from 'vitest'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
import type { InputSpec } from '@/schemas/nodeDefSchema'
import { useLitegraphService } from '@/services/litegraphService'
Expand All @@ -12,6 +12,10 @@ type DynamicInputs = ('INT' | 'STRING' | 'IMAGE' | DynamicInputs)[][]

const { addNodeInput } = useLitegraphService()

function nextTick() {
return new Promise<void>((r) => requestAnimationFrame(() => r()))
}

function addDynamicCombo(node: LGraphNode, inputs: DynamicInputs) {
const namePrefix = `${node.widgets?.length ?? 0}`
function getSpec(
Expand Down Expand Up @@ -40,6 +44,21 @@ function addDynamicCombo(node: LGraphNode, inputs: DynamicInputs) {
transformInputSpecV1ToV2(inputSpec, { name: namePrefix, isOptional: false })
)
}
function addAutogrow(node: LGraphNode, template: unknown) {
addNodeInput(
node,
transformInputSpecV1ToV2(['COMFY_AUTOGROW_V3', { template }], {
name: `${node.inputs.length}`,
isOptional: false
})
)
}
function connectInput(node: LGraphNode, inputIndex: number, graph: LGraph) {
const node2 = testNode()
node2.addOutput('out', '*')
graph.add(node2)
node2.connect(0, node, inputIndex)
}
function testNode() {
const node: LGraphNode & Partial<HasInitialMinSize> = new LGraphNode('test')
node.widgets = []
Expand Down Expand Up @@ -88,3 +107,72 @@ describe('Dynamic Combos', () => {
expect(node.inputs[3].name).toBe('2.0.0')
})
})
describe('Autogrow', () => {
const inputsSpec = { required: { image: ['IMAGE', {}] } }
test('Can name by prefix', () => {
const graph = new LGraph()
const node = testNode()
graph.add(node)
addAutogrow(node, { input: inputsSpec, prefix: 'test' })
connectInput(node, 0, graph)
connectInput(node, 1, graph)
connectInput(node, 2, graph)
expect(node.inputs.length).toBe(4)
expect(node.inputs[0].name).toBe('test0')
expect(node.inputs[2].name).toBe('test2')
})
test('Can name by list of names', () => {
const graph = new LGraph()
const node = testNode()
graph.add(node)
addAutogrow(node, { input: inputsSpec, names: ['a', 'b', 'c'] })
connectInput(node, 0, graph)
connectInput(node, 1, graph)
connectInput(node, 2, graph)
expect(node.inputs.length).toBe(3)
expect(node.inputs[0].name).toBe('a')
expect(node.inputs[2].name).toBe('c')
})
test('Can add autogrow with min input count', () => {
const node = testNode()
addAutogrow(node, { min: 4, input: inputsSpec })
expect(node.inputs.length).toBe(4)
})
test('Adding connections will cause growth up to max', () => {
const graph = new LGraph()
const node = testNode()
graph.add(node)
addAutogrow(node, { min: 1, input: inputsSpec, prefix: 'test', max: 3 })
expect(node.inputs.length).toBe(1)

connectInput(node, 0, graph)
expect(node.inputs.length).toBe(2)
connectInput(node, 1, graph)
expect(node.inputs.length).toBe(3)
connectInput(node, 2, graph)
expect(node.inputs.length).toBe(3)
})
test('Removing connections decreases to min', async () => {
const graph = new LGraph()
const node = testNode()
graph.add(node)
addAutogrow(node, { min: 4, input: inputsSpec, prefix: 'test' })
connectInput(node, 3, graph)
connectInput(node, 4, graph)
connectInput(node, 5, graph)
expect(node.inputs.length).toBe(7)

node.disconnectInput(4)
await nextTick()
expect(node.inputs.length).toBe(6)
node.disconnectInput(3)
await nextTick()
expect(node.inputs.length).toBe(5)

connectInput(node, 0, graph)
expect(node.inputs.length).toBe(5)
node.disconnectInput(0)
await nextTick()
expect(node.inputs.length).toBe(5)
})
})
Loading