mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
Further dynamic input fixes (#8026)
- 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)
This commit is contained in:
@@ -1,19 +0,0 @@
|
|||||||
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<clipPath id="hollow">
|
|
||||||
<path
|
|
||||||
d="M -50 50
|
|
||||||
A 100 100, 0, 0, 1, 150 50
|
|
||||||
A 100 100, 0, 0, 1, -50 50
|
|
||||||
M 30 50
|
|
||||||
A 20 20, 0, 0, 0, 70 50
|
|
||||||
A 20 20, 0, 0, 0, 30 50"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
<g clip-path="var(--shape)" stroke-width="4">
|
|
||||||
<path d="M 50 0 A 50 50, 0, 0, 1, 50 100" fill="var(--type1, red)"/>
|
|
||||||
<path d="M 50 100 A 50 50, 0, 0, 1, 50 0" fill="var(--type2, blue)"/>
|
|
||||||
<path d="M50 0L50 100" stroke="var(--inner-stroke, black)"/>
|
|
||||||
<path d="M50 2A48 48 0 0 1 50 98A48 48 0 0 1 50 2" fill="transparent" stroke="var(--outer-stroke, transparent)"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 693 B |
@@ -1,20 +0,0 @@
|
|||||||
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<clipPath id="hollow">
|
|
||||||
<path
|
|
||||||
d="M-50 50
|
|
||||||
A100 100 0 0 1 150 50
|
|
||||||
A100 100 0 0 1 -50 50
|
|
||||||
M30 50
|
|
||||||
A20 20 0 0 0 70 50
|
|
||||||
A20 20 0 0 0 30 50"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
<g clip-path="var(--shape)" stroke-width="4">
|
|
||||||
<path d="M50 0A50 50 0 0 1 93 75L50 50" fill="var(--type1, red)"/>
|
|
||||||
<path d="M93 75A50 50 0 0 1 7 75L50 50" fill="var(--type2, blue)"/>
|
|
||||||
<path d="M7 75A50 50 0 0 1 50 0L50 50" fill="var(--type3, green)"/>
|
|
||||||
<path d="M50 50L50 0M50 50L93 75M50 50L7 75" stroke="var(--inner-stroke, black)"/>
|
|
||||||
<path d="M50 2A48 48 0 0 1 50 98A48 48 0 0 1 50 2" fill="transparent" stroke="var(--outer-stroke, transparent)"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 763 B |
@@ -175,4 +175,32 @@ describe('Autogrow', () => {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
expect(node.inputs.length).toBe(5)
|
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'
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { remove } from 'es-toolkit'
|
import { remove } from 'es-toolkit'
|
||||||
|
import { shallowReactive } from 'vue'
|
||||||
|
|
||||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||||
import type {
|
import type {
|
||||||
@@ -342,7 +343,9 @@ function applyMatchType(node: LGraphNode, inputSpec: InputSpecV2) {
|
|||||||
//ensure outputs get updated
|
//ensure outputs get updated
|
||||||
const index = node.inputs.length - 1
|
const index = node.inputs.length - 1
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const input = node.inputs.at(index)!
|
const input = node.inputs[index]
|
||||||
|
if (!input) return
|
||||||
|
node.inputs[index] = shallowReactive(input)
|
||||||
node.onConnectionsChange?.(
|
node.onConnectionsChange?.(
|
||||||
LiteGraph.INPUT,
|
LiteGraph.INPUT,
|
||||||
index,
|
index,
|
||||||
@@ -385,11 +388,7 @@ function addAutogrowGroup(
|
|||||||
...autogrowOrdinalToName(ordinal, input.name, groupName, node)
|
...autogrowOrdinalToName(ordinal, input.name, groupName, node)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const newInputs = namedSpecs
|
const newInputs = namedSpecs.map((namedSpec) => {
|
||||||
.filter(
|
|
||||||
(namedSpec) => !node.inputs.some((inp) => inp.name === namedSpec.name)
|
|
||||||
)
|
|
||||||
.map((namedSpec) => {
|
|
||||||
addNodeInput(node, namedSpec)
|
addNodeInput(node, namedSpec)
|
||||||
const input = spliceInputs(node, node.inputs.length - 1, 1)[0]
|
const input = spliceInputs(node, node.inputs.length - 1, 1)[0]
|
||||||
if (inputSpecs.length !== 1 || (INLINE_INPUTS && !input.widget))
|
if (inputSpecs.length !== 1 || (INLINE_INPUTS && !input.widget))
|
||||||
@@ -397,8 +396,24 @@ function addAutogrowGroup(
|
|||||||
return 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) =>
|
const lastIndex = node.inputs.findLastIndex((inp) =>
|
||||||
inp.name.startsWith(groupName)
|
inp.name.startsWith(targetName)
|
||||||
)
|
)
|
||||||
const insertionIndex = lastIndex === -1 ? node.inputs.length : lastIndex + 1
|
const insertionIndex = lastIndex === -1 ? node.inputs.length : lastIndex + 1
|
||||||
spliceInputs(node, insertionIndex, 0, ...newInputs)
|
spliceInputs(node, insertionIndex, 0, ...newInputs)
|
||||||
@@ -427,13 +442,14 @@ function autogrowInputConnected(index: number, node: AutogrowNode) {
|
|||||||
const input = node.inputs[index]
|
const input = node.inputs[index]
|
||||||
const groupName = input.name.slice(0, input.name.lastIndexOf('.'))
|
const groupName = input.name.slice(0, input.name.lastIndexOf('.'))
|
||||||
const lastInput = node.inputs.findLast((inp) =>
|
const lastInput = node.inputs.findLast((inp) =>
|
||||||
inp.name.startsWith(groupName)
|
inp.name.startsWith(groupName + '.')
|
||||||
)
|
)
|
||||||
const ordinal = resolveAutogrowOrdinal(input.name, groupName, node)
|
const ordinal = resolveAutogrowOrdinal(input.name, groupName, node)
|
||||||
if (
|
if (
|
||||||
!lastInput ||
|
!lastInput ||
|
||||||
ordinal == undefined ||
|
ordinal == undefined ||
|
||||||
ordinal !== resolveAutogrowOrdinal(lastInput.name, groupName, node)
|
(ordinal !== resolveAutogrowOrdinal(lastInput.name, groupName, node) &&
|
||||||
|
!app.configuringGraph)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
addAutogrowGroup(ordinal + 1, groupName, node)
|
addAutogrowGroup(ordinal + 1, groupName, node)
|
||||||
@@ -453,6 +469,7 @@ function autogrowInputDisconnected(index: number, node: AutogrowNode) {
|
|||||||
inp.name.lastIndexOf('.') === groupName.length
|
inp.name.lastIndexOf('.') === groupName.length
|
||||||
)
|
)
|
||||||
const stride = inputSpecs.length
|
const stride = inputSpecs.length
|
||||||
|
if (stride + index >= node.inputs.length) return
|
||||||
if (groupInputs.length % stride !== 0) {
|
if (groupInputs.length % stride !== 0) {
|
||||||
console.error('Failed to group multi-input autogrow inputs')
|
console.error('Failed to group multi-input autogrow inputs')
|
||||||
return
|
return
|
||||||
@@ -473,10 +490,24 @@ function autogrowInputDisconnected(index: number, node: AutogrowNode) {
|
|||||||
const curIndex = node.inputs.findIndex((inp) => inp === curInput)
|
const curIndex = node.inputs.findIndex((inp) => inp === curInput)
|
||||||
if (curIndex === -1) throw new Error('missing input')
|
if (curIndex === -1) throw new Error('missing input')
|
||||||
link.target_slot = curIndex
|
link.target_slot = curIndex
|
||||||
|
node.onConnectionsChange?.(
|
||||||
|
LiteGraph.INPUT,
|
||||||
|
curIndex,
|
||||||
|
true,
|
||||||
|
link,
|
||||||
|
curInput
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const lastInput = groupInputs.at(column - stride)
|
const lastInput = groupInputs.at(column - stride)
|
||||||
if (!lastInput) continue
|
if (!lastInput) continue
|
||||||
lastInput.link = null
|
lastInput.link = null
|
||||||
|
node.onConnectionsChange?.(
|
||||||
|
LiteGraph.INPUT,
|
||||||
|
node.inputs.length + column - stride,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
lastInput
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const removalChecks = groupInputs.slice((min - 1) * stride)
|
const removalChecks = groupInputs.slice((min - 1) * stride)
|
||||||
let i
|
let i
|
||||||
@@ -564,5 +595,6 @@ function applyAutogrow(node: LGraphNode, inputSpecV2: InputSpecV2) {
|
|||||||
prefix,
|
prefix,
|
||||||
inputSpecs: inputsV2
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
addOption(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export interface IDrawOptions {
|
|||||||
highlight?: boolean
|
highlight?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const ROTATION_OFFSET = -Math.PI / 2
|
const ROTATION_OFFSET = -Math.PI
|
||||||
|
|
||||||
/** Shared base class for {@link LGraphNode} input and output slots. */
|
/** Shared base class for {@link LGraphNode} input and output slots. */
|
||||||
export abstract class NodeSlot extends SlotBase implements INodeSlot {
|
export abstract class NodeSlot extends SlotBase implements INodeSlot {
|
||||||
|
|||||||
@@ -13,18 +13,29 @@ const props = defineProps<{
|
|||||||
multi?: boolean
|
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')
|
const slotElRef = useTemplateRef('slot-el')
|
||||||
|
|
||||||
function getTypes() {
|
const types = computed(() => {
|
||||||
if (props.hasError) return ['var(--color-error)']
|
if (props.hasError) return ['var(--color-error)']
|
||||||
//TODO Support connected/disconnected colors?
|
//TODO Support connected/disconnected colors?
|
||||||
if (!props.slotData) return [getSlotColor()]
|
if (!props.slotData) return [getSlotColor()]
|
||||||
|
if (props.slotData.type === '*') return ['']
|
||||||
const typesSet = new Set(
|
const typesSet = new Set(
|
||||||
`${props.slotData.type}`.split(',').map(getSlotColor)
|
`${props.slotData.type}`.split(',').map(getSlotColor)
|
||||||
)
|
)
|
||||||
return [...typesSet].slice(0, 3)
|
return [...typesSet].slice(0, 3)
|
||||||
}
|
})
|
||||||
const types = getTypes()
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
slotElRef
|
slotElRef
|
||||||
@@ -52,26 +63,65 @@ const slotClass = computed(() =>
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="types.length === 1"
|
v-if="types.length === 1 && slotData?.shape == undefined"
|
||||||
ref="slot-el"
|
ref="slot-el"
|
||||||
:style="{ backgroundColor: types[0] }"
|
:style="{ backgroundColor: types.length === 1 ? types[0] : undefined }"
|
||||||
:class="slotClass"
|
:class="slotClass"
|
||||||
/>
|
/>
|
||||||
<div
|
<svg
|
||||||
v-else
|
v-else
|
||||||
ref="slot-el"
|
ref="slot-el"
|
||||||
:style="{
|
|
||||||
'--type1': types[0],
|
|
||||||
'--type2': types[1],
|
|
||||||
'--type3': types[2]
|
|
||||||
}"
|
|
||||||
:class="slotClass"
|
:class="slotClass"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<i-comfy:node-slot2
|
<defs>
|
||||||
v-if="types.length === 2"
|
<clipPath id="square">
|
||||||
class="size-full -translate-y-1/2"
|
<rect x="20" y="20" width="60" height="60" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="hollow">
|
||||||
|
<path
|
||||||
|
d="M-50 50
|
||||||
|
A100 100 0 0 1 150 50
|
||||||
|
A100 100 0 0 1 -50 50
|
||||||
|
M30 50
|
||||||
|
A20 20 0 0 0 70 50
|
||||||
|
A20 20 0 0 0 30 50"
|
||||||
/>
|
/>
|
||||||
<i-comfy:node-slot3 v-else class="size-full -translate-y-1/2" />
|
</clipPath>
|
||||||
</div>
|
</defs>
|
||||||
|
<circle
|
||||||
|
v-if="types.length === 1"
|
||||||
|
:clip-path
|
||||||
|
cx="50"
|
||||||
|
cy="50"
|
||||||
|
r="50"
|
||||||
|
:fill="types[0]"
|
||||||
|
/>
|
||||||
|
<g v-else-if="types.length === 2" :clip-path stroke-width="4">
|
||||||
|
<path d="M0 50 A 50 50 0 0 1 100 50" :fill="types[0]" />
|
||||||
|
<path d="M100 50 A 50 50 0 0 1 0 50" :fill="types[1]" />
|
||||||
|
<path d="M0 50L100 50" stroke="var(--inner-stroke, black)" />
|
||||||
|
<path
|
||||||
|
d="M50 2A48 48 0 0 1 50 98A48 48 0 0 1 50 2"
|
||||||
|
fill="transparent"
|
||||||
|
stroke="var(--outer-stroke, transparent)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g v-else :clip-path stroke-width="4">
|
||||||
|
<path d="M0 50A50 50 0 0 0 75 93L50 50" :fill="types[0]" />
|
||||||
|
<path d="M75 93A50 50 0 0 0 75 7L50 50" :fill="types[1]" />
|
||||||
|
<path d="M75 7A50 50 0 0 0 0 50L50 50" :fill="types[2]" />
|
||||||
|
<path
|
||||||
|
d="M50 50L0 50M50 50L75 93M50 50L75 7"
|
||||||
|
stroke="var(--inner-stroke, black)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M50 2A48 48 0 0 1 50 98A48 48 0 0 1 50 2"
|
||||||
|
fill="transparent"
|
||||||
|
stroke="var(--outer-stroke, transparent)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user