From 4a5e7c8bcb5131f4b9029a8e89564ff3594d807e Mon Sep 17 00:00:00 2001 From: AustinMroz Date: Thu, 22 Jan 2026 00:02:49 -0800 Subject: [PATCH] Further dynamic input fixes (#8026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix deserialization of matchtype inputs spawned by autogrow. - Rotate multitype slot indicators to align with design changes. - Fix several instance of incorrect group matching - MatchType reactively updates input type in vue - Support the "hollow circle" optional input indicator in vue - Custom combo sends index of selection to backend ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8026-Further-dynamic-input-fixes-2e76d73d3650819680fef327a94f4294) by [Unito](https://www.unito.io) --- .../design-system/src/icons/nodeSlot2.svg | 19 ----- .../design-system/src/icons/nodeSlot3.svg | 20 ----- src/core/graph/widgets/dynamicWidgets.test.ts | 28 +++++++ src/core/graph/widgets/dynamicWidgets.ts | 64 +++++++++++---- src/extensions/core/customCombo.ts | 13 +++ src/lib/litegraph/src/node/NodeSlot.ts | 2 +- .../vueNodes/components/SlotConnectionDot.vue | 82 +++++++++++++++---- 7 files changed, 156 insertions(+), 72 deletions(-) delete mode 100644 packages/design-system/src/icons/nodeSlot2.svg delete mode 100644 packages/design-system/src/icons/nodeSlot3.svg diff --git a/packages/design-system/src/icons/nodeSlot2.svg b/packages/design-system/src/icons/nodeSlot2.svg deleted file mode 100644 index cc1280570..000000000 --- a/packages/design-system/src/icons/nodeSlot2.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - diff --git a/packages/design-system/src/icons/nodeSlot3.svg b/packages/design-system/src/icons/nodeSlot3.svg deleted file mode 100644 index fc94a178b..000000000 --- a/packages/design-system/src/icons/nodeSlot3.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/core/graph/widgets/dynamicWidgets.test.ts b/src/core/graph/widgets/dynamicWidgets.test.ts index c2c4743f7..0252691d1 100644 --- a/src/core/graph/widgets/dynamicWidgets.test.ts +++ b/src/core/graph/widgets/dynamicWidgets.test.ts @@ -175,4 +175,32 @@ describe('Autogrow', () => { await nextTick() expect(node.inputs.length).toBe(5) }) + test('Can deserialize a complex node', async () => { + const graph = new LGraph() + const node = testNode() + graph.add(node) + addAutogrow(node, { min: 1, input: inputsSpec, prefix: 'a' }) + addAutogrow(node, { min: 1, input: inputsSpec, prefix: 'b' }) + addNodeInput(node, { name: 'aa', isOptional: false, type: 'IMAGE' }) + + connectInput(node, 0, graph) + connectInput(node, 1, graph) + connectInput(node, 3, graph) + connectInput(node, 4, graph) + + const serialized = graph.serialize() + graph.clear() + graph.configure(serialized) + const newNode = graph.nodes[0]! + + expect(newNode.inputs.map((i) => i.name)).toStrictEqual([ + '0.a0', + '0.a1', + '0.a2', + '1.b0', + '1.b1', + '1.b2', + 'aa' + ]) + }) }) diff --git a/src/core/graph/widgets/dynamicWidgets.ts b/src/core/graph/widgets/dynamicWidgets.ts index 406822d43..7e3f7fd71 100644 --- a/src/core/graph/widgets/dynamicWidgets.ts +++ b/src/core/graph/widgets/dynamicWidgets.ts @@ -1,4 +1,5 @@ import { remove } from 'es-toolkit' +import { shallowReactive } from 'vue' import { useChainCallback } from '@/composables/functional/useChainCallback' import type { @@ -342,7 +343,9 @@ function applyMatchType(node: LGraphNode, inputSpec: InputSpecV2) { //ensure outputs get updated const index = node.inputs.length - 1 requestAnimationFrame(() => { - const input = node.inputs.at(index)! + const input = node.inputs[index] + if (!input) return + node.inputs[index] = shallowReactive(input) node.onConnectionsChange?.( LiteGraph.INPUT, index, @@ -385,20 +388,32 @@ function addAutogrowGroup( ...autogrowOrdinalToName(ordinal, input.name, groupName, node) })) - const newInputs = namedSpecs - .filter( - (namedSpec) => !node.inputs.some((inp) => inp.name === namedSpec.name) - ) - .map((namedSpec) => { - addNodeInput(node, namedSpec) - const input = spliceInputs(node, node.inputs.length - 1, 1)[0] - if (inputSpecs.length !== 1 || (INLINE_INPUTS && !input.widget)) - ensureWidgetForInput(node, input) - return input - }) + const newInputs = namedSpecs.map((namedSpec) => { + addNodeInput(node, namedSpec) + const input = spliceInputs(node, node.inputs.length - 1, 1)[0] + if (inputSpecs.length !== 1 || (INLINE_INPUTS && !input.widget)) + ensureWidgetForInput(node, input) + return input + }) + for (const newInput of newInputs) { + for (const existingInput of remove( + node.inputs, + (inp) => inp.name === newInput.name + )) { + //NOTE: link.target_slot is updated on spliceInputs call + newInput.link ??= existingInput.link + } + } + + const targetName = autogrowOrdinalToName( + ordinal - 1, + inputSpecs.at(-1)!.name, + groupName, + node + ).name const lastIndex = node.inputs.findLastIndex((inp) => - inp.name.startsWith(groupName) + inp.name.startsWith(targetName) ) const insertionIndex = lastIndex === -1 ? node.inputs.length : lastIndex + 1 spliceInputs(node, insertionIndex, 0, ...newInputs) @@ -427,13 +442,14 @@ function autogrowInputConnected(index: number, node: AutogrowNode) { const input = node.inputs[index] const groupName = input.name.slice(0, input.name.lastIndexOf('.')) const lastInput = node.inputs.findLast((inp) => - inp.name.startsWith(groupName) + inp.name.startsWith(groupName + '.') ) const ordinal = resolveAutogrowOrdinal(input.name, groupName, node) if ( !lastInput || ordinal == undefined || - ordinal !== resolveAutogrowOrdinal(lastInput.name, groupName, node) + (ordinal !== resolveAutogrowOrdinal(lastInput.name, groupName, node) && + !app.configuringGraph) ) return addAutogrowGroup(ordinal + 1, groupName, node) @@ -453,6 +469,7 @@ function autogrowInputDisconnected(index: number, node: AutogrowNode) { inp.name.lastIndexOf('.') === groupName.length ) const stride = inputSpecs.length + if (stride + index >= node.inputs.length) return if (groupInputs.length % stride !== 0) { console.error('Failed to group multi-input autogrow inputs') return @@ -473,10 +490,24 @@ function autogrowInputDisconnected(index: number, node: AutogrowNode) { const curIndex = node.inputs.findIndex((inp) => inp === curInput) if (curIndex === -1) throw new Error('missing input') link.target_slot = curIndex + node.onConnectionsChange?.( + LiteGraph.INPUT, + curIndex, + true, + link, + curInput + ) } const lastInput = groupInputs.at(column - stride) if (!lastInput) continue lastInput.link = null + node.onConnectionsChange?.( + LiteGraph.INPUT, + node.inputs.length + column - stride, + false, + null, + lastInput + ) } const removalChecks = groupInputs.slice((min - 1) * stride) let i @@ -564,5 +595,6 @@ function applyAutogrow(node: LGraphNode, inputSpecV2: InputSpecV2) { prefix, inputSpecs: inputsV2 } - for (let i = 0; i < min; i++) addAutogrowGroup(i, inputSpecV2.name, node) + for (let i = 0; i === 0 || i < min; i++) + addAutogrowGroup(i, inputSpecV2.name, node) } diff --git a/src/extensions/core/customCombo.ts b/src/extensions/core/customCombo.ts index ed7356a40..58d41eb62 100644 --- a/src/extensions/core/customCombo.ts +++ b/src/extensions/core/customCombo.ts @@ -98,6 +98,19 @@ function onNodeCreated(this: LGraphNode) { } }) } + const widgets = this.widgets! + widgets.push({ + name: 'index', + type: 'hidden', + get value() { + return widgets.slice(2).findIndex((w) => w.value === comboWidget.value) + }, + set value(_) {}, + draw: () => undefined, + computeSize: () => [0, -4], + options: { hidden: true }, + y: 0 + }) addOption(this) } diff --git a/src/lib/litegraph/src/node/NodeSlot.ts b/src/lib/litegraph/src/node/NodeSlot.ts index 4e17069b2..9fac7816f 100644 --- a/src/lib/litegraph/src/node/NodeSlot.ts +++ b/src/lib/litegraph/src/node/NodeSlot.ts @@ -30,7 +30,7 @@ export interface IDrawOptions { highlight?: boolean } -const ROTATION_OFFSET = -Math.PI / 2 +const ROTATION_OFFSET = -Math.PI /** Shared base class for {@link LGraphNode} input and output slots. */ export abstract class NodeSlot extends SlotBase implements INodeSlot { diff --git a/src/renderer/extensions/vueNodes/components/SlotConnectionDot.vue b/src/renderer/extensions/vueNodes/components/SlotConnectionDot.vue index 3921128b0..67c978a0b 100644 --- a/src/renderer/extensions/vueNodes/components/SlotConnectionDot.vue +++ b/src/renderer/extensions/vueNodes/components/SlotConnectionDot.vue @@ -13,18 +13,29 @@ const props = defineProps<{ multi?: boolean }>() +const clipPath = computed(() => { + switch (props.slotData?.shape) { + case 6: + return 'url(#square)' + case 7: + return 'url(#hollow)' + default: + return undefined + } +}) + const slotElRef = useTemplateRef('slot-el') -function getTypes() { +const types = computed(() => { if (props.hasError) return ['var(--color-error)'] //TODO Support connected/disconnected colors? if (!props.slotData) return [getSlotColor()] + if (props.slotData.type === '*') return [''] const typesSet = new Set( `${props.slotData.type}`.split(',').map(getSlotColor) ) return [...typesSet].slice(0, 3) -} -const types = getTypes() +}) defineExpose({ slotElRef @@ -52,26 +63,65 @@ const slotClass = computed(() => " >
-
- + + + + + + + + - -
+ + + + + + + + + + + + + +