mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 10:12:11 +00:00
Support display of multitype slots (#7457)
Example with forcibly modified types for testing <img width="736" height="425" alt="image" src="https://github.com/user-attachments/assets/e885a7d0-5946-41be-b9b4-b9b195f50c92" /> Vue mode doesn't currently seem to display optional inputs, but the SVGs here include support for being made hollow with `--shape: url(#hollow)` <img width="765" height="360" alt="image" src="https://github.com/user-attachments/assets/0ea57179-99a4-4001-aa18-856e172287c0" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7457-Support-display-of-multitype-slots-2c86d73d3650818594afd988e73827e3) by [Unito](https://www.unito.io)
This commit is contained in:
19
packages/design-system/src/icons/nodeSlot2.svg
Normal file
19
packages/design-system/src/icons/nodeSlot2.svg
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 693 B |
20
packages/design-system/src/icons/nodeSlot3.svg
Normal file
20
packages/design-system/src/icons/nodeSlot3.svg
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 763 B |
@@ -30,6 +30,8 @@ export interface IDrawOptions {
|
|||||||
highlight?: boolean
|
highlight?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ROTATION_OFFSET = -Math.PI / 2
|
||||||
|
|
||||||
/** 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 {
|
||||||
pos?: Point
|
pos?: Point
|
||||||
@@ -130,6 +132,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
|
|||||||
slot_type === SlotType.Array ? SlotShape.Grid : this.shape
|
slot_type === SlotType.Array ? SlotShape.Grid : this.shape
|
||||||
) as SlotShape
|
) as SlotShape
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
let doFill = true
|
let doFill = true
|
||||||
|
|
||||||
@@ -163,16 +166,52 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
|
|||||||
if (lowQuality) {
|
if (lowQuality) {
|
||||||
ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8)
|
ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8)
|
||||||
} else {
|
} else {
|
||||||
let radius: number
|
|
||||||
if (slot_shape === SlotShape.HollowCircle) {
|
if (slot_shape === SlotShape.HollowCircle) {
|
||||||
|
const path = new Path2D()
|
||||||
|
path.arc(pos[0], pos[1], 10, 0, Math.PI * 2)
|
||||||
|
path.arc(pos[0], pos[1], highlight ? 2.5 : 1.5, 0, Math.PI * 2)
|
||||||
|
ctx.clip(path, 'evenodd')
|
||||||
|
}
|
||||||
|
const radius = highlight ? 5 : 4
|
||||||
|
const typesSet = new Set(
|
||||||
|
`${this.type}`
|
||||||
|
.split(',')
|
||||||
|
.map(
|
||||||
|
this.isConnected
|
||||||
|
? (type) => colorContext.getConnectedColor(type)
|
||||||
|
: (type) => colorContext.getDisconnectedColor(type)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const types = [...typesSet].slice(0, 3)
|
||||||
|
if (types.length > 1) {
|
||||||
doFill = false
|
doFill = false
|
||||||
doStroke = true
|
const arcLen = (Math.PI * 2) / types.length
|
||||||
ctx.lineWidth = 3
|
types.forEach((type, idx) => {
|
||||||
ctx.strokeStyle = ctx.fillStyle
|
ctx.moveTo(pos[0], pos[1])
|
||||||
radius = highlight ? 4 : 3
|
ctx.fillStyle = type
|
||||||
} else {
|
ctx.arc(
|
||||||
// Normal circle
|
pos[0],
|
||||||
radius = highlight ? 5 : 4
|
pos[1],
|
||||||
|
radius,
|
||||||
|
arcLen * idx + ROTATION_OFFSET,
|
||||||
|
Math.PI * 2 + ROTATION_OFFSET
|
||||||
|
)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.beginPath()
|
||||||
|
})
|
||||||
|
//add stroke dividers
|
||||||
|
ctx.save()
|
||||||
|
ctx.strokeStyle = 'black'
|
||||||
|
ctx.lineWidth = 0.5
|
||||||
|
types.forEach((_, idx) => {
|
||||||
|
ctx.moveTo(pos[0], pos[1])
|
||||||
|
const xOffset = Math.cos(arcLen * idx + ROTATION_OFFSET) * radius
|
||||||
|
const yOffset = Math.sin(arcLen * idx + ROTATION_OFFSET) * radius
|
||||||
|
ctx.lineTo(pos[0] + xOffset, pos[1] + yOffset)
|
||||||
|
})
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.restore()
|
||||||
|
ctx.beginPath()
|
||||||
}
|
}
|
||||||
ctx.arc(pos[0], pos[1], radius, 0, Math.PI * 2)
|
ctx.arc(pos[0], pos[1], radius, 0, Math.PI * 2)
|
||||||
}
|
}
|
||||||
@@ -180,6 +219,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
|
|||||||
|
|
||||||
if (doFill) ctx.fill()
|
if (doFill) ctx.fill()
|
||||||
if (!lowQuality && doStroke) ctx.stroke()
|
if (!lowQuality && doStroke) ctx.stroke()
|
||||||
|
ctx.restore()
|
||||||
|
|
||||||
// render slot label
|
// render slot label
|
||||||
const hideLabel = lowQuality || this.isWidgetInputSlot
|
const hideLabel = lowQuality || this.isWidgetInputSlot
|
||||||
|
|||||||
@@ -20,13 +20,13 @@
|
|||||||
<!-- Connection Dot -->
|
<!-- Connection Dot -->
|
||||||
<SlotConnectionDot
|
<SlotConnectionDot
|
||||||
ref="connectionDotRef"
|
ref="connectionDotRef"
|
||||||
:color="slotColor"
|
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'-translate-x-1/2 w-3',
|
'-translate-x-1/2 w-3',
|
||||||
hasSlotError && 'ring-2 ring-error ring-offset-0 rounded-full'
|
hasSlotError && 'ring-2 ring-error ring-offset-0 rounded-full'
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
:slot-data
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
@dblclick="onDoubleClick"
|
@dblclick="onDoubleClick"
|
||||||
@pointerdown="onPointerDown"
|
@pointerdown="onPointerDown"
|
||||||
@@ -54,7 +54,6 @@ import { computed, onErrorCaptured, ref, watchEffect } from 'vue'
|
|||||||
import type { ComponentPublicInstance } from 'vue'
|
import type { ComponentPublicInstance } from 'vue'
|
||||||
|
|
||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
import { getSlotColor } from '@/constants/slotColors'
|
|
||||||
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
|
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
|
||||||
import { useSlotLinkDragUIState } from '@/renderer/core/canvas/links/slotLinkDragUIState'
|
import { useSlotLinkDragUIState } from '@/renderer/core/canvas/links/slotLinkDragUIState'
|
||||||
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
||||||
@@ -111,13 +110,6 @@ onErrorCaptured((error) => {
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
const slotColor = computed(() => {
|
|
||||||
if (hasSlotError.value) {
|
|
||||||
return 'var(--color-error)'
|
|
||||||
}
|
|
||||||
return getSlotColor(props.slotData.type)
|
|
||||||
})
|
|
||||||
|
|
||||||
const { state: dragState } = useSlotLinkDragUIState()
|
const { state: dragState } = useSlotLinkDragUIState()
|
||||||
const slotKey = computed(() =>
|
const slotKey = computed(() =>
|
||||||
getSlotKey(props.nodeId ?? '', props.index, true)
|
getSlotKey(props.nodeId ?? '', props.index, true)
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
<!-- Connection Dot -->
|
<!-- Connection Dot -->
|
||||||
<SlotConnectionDot
|
<SlotConnectionDot
|
||||||
ref="connectionDotRef"
|
ref="connectionDotRef"
|
||||||
:color="slotColor"
|
|
||||||
class="w-3 translate-x-1/2"
|
class="w-3 translate-x-1/2"
|
||||||
|
:slot-data
|
||||||
@pointerdown="onPointerDown"
|
@pointerdown="onPointerDown"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -22,7 +22,6 @@ import { computed, onErrorCaptured, ref, watchEffect } from 'vue'
|
|||||||
import type { ComponentPublicInstance } from 'vue'
|
import type { ComponentPublicInstance } from 'vue'
|
||||||
|
|
||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
import { getSlotColor } from '@/constants/slotColors'
|
|
||||||
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
|
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
|
||||||
import { useSlotLinkDragUIState } from '@/renderer/core/canvas/links/slotLinkDragUIState'
|
import { useSlotLinkDragUIState } from '@/renderer/core/canvas/links/slotLinkDragUIState'
|
||||||
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
||||||
@@ -67,9 +66,6 @@ onErrorCaptured((error) => {
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get slot color based on type
|
|
||||||
const slotColor = computed(() => getSlotColor(props.slotData.type))
|
|
||||||
|
|
||||||
const { state: dragState } = useSlotLinkDragUIState()
|
const { state: dragState } = useSlotLinkDragUIState()
|
||||||
const slotKey = computed(() =>
|
const slotKey = computed(() =>
|
||||||
getSlotKey(props.nodeId ?? '', props.index, false)
|
getSlotKey(props.nodeId ?? '', props.index, false)
|
||||||
|
|||||||
@@ -1,20 +1,45 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTemplateRef } from 'vue'
|
import { computed, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
|
import { getSlotColor } from '@/constants/slotColors'
|
||||||
|
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
import type { ClassValue } from '@/utils/tailwindUtil'
|
import type { ClassValue } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
color?: string
|
slotData?: INodeSlot
|
||||||
multi?: boolean
|
|
||||||
class?: ClassValue
|
class?: ClassValue
|
||||||
|
hasError?: boolean
|
||||||
|
multi?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const slotElRef = useTemplateRef('slot-el')
|
const slotElRef = useTemplateRef('slot-el')
|
||||||
|
|
||||||
|
function getTypes() {
|
||||||
|
if (props.hasError) return ['var(--color-error)']
|
||||||
|
//TODO Support connected/disconnected colors?
|
||||||
|
if (!props.slotData) return [getSlotColor()]
|
||||||
|
const typesSet = new Set(
|
||||||
|
`${props.slotData.type}`.split(',').map(getSlotColor)
|
||||||
|
)
|
||||||
|
return [...typesSet].slice(0, 3)
|
||||||
|
}
|
||||||
|
const types = getTypes()
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
slotElRef
|
slotElRef
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const slotClass = computed(() =>
|
||||||
|
cn(
|
||||||
|
'bg-slate-300 rounded-full slot-dot',
|
||||||
|
'transition-all duration-150',
|
||||||
|
'border border-solid border-node-component-slot-dot-outline',
|
||||||
|
props.multi
|
||||||
|
? 'w-3 h-6'
|
||||||
|
: 'size-3 cursor-crosshair group-hover/slot:[--node-component-slot-dot-outline-opacity-mult:5] group-hover/slot:scale-125'
|
||||||
|
)
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -27,19 +52,26 @@ defineExpose({
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
v-if="types.length === 1"
|
||||||
ref="slot-el"
|
ref="slot-el"
|
||||||
class="slot-dot"
|
:style="{ backgroundColor: types[0] }"
|
||||||
:style="{ backgroundColor: color }"
|
:class="slotClass"
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'bg-slate-300 rounded-full',
|
|
||||||
'transition-all duration-150',
|
|
||||||
'border border-solid border-node-component-slot-dot-outline',
|
|
||||||
!multi &&
|
|
||||||
'cursor-crosshair group-hover/slot:[--node-component-slot-dot-outline-opacity-mult:5] group-hover/slot:scale-125',
|
|
||||||
multi ? 'w-3 h-6' : 'size-3'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
ref="slot-el"
|
||||||
|
:style="{
|
||||||
|
'--type1': types[0],
|
||||||
|
'--type2': types[1],
|
||||||
|
'--type3': types[2]
|
||||||
|
}"
|
||||||
|
:class="slotClass"
|
||||||
|
>
|
||||||
|
<i-comfy:node-slot2
|
||||||
|
v-if="types.length === 2"
|
||||||
|
class="size-full -translate-y-1/2"
|
||||||
|
/>
|
||||||
|
<i-comfy:node-slot3 v-else class="size-full -translate-y-1/2" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user