Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
19 changes: 19 additions & 0 deletions packages/design-system/src/icons/nodeSlot.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions packages/design-system/src/icons/nodeSlot2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions packages/design-system/src/icons/nodeSlot3.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 48 additions & 8 deletions src/lib/litegraph/src/node/NodeSlot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export interface IDrawOptions {
highlight?: boolean
}

const ROTATION_OFFSET = -Math.PI / 2

/** Shared base class for {@link LGraphNode} input and output slots. */
export abstract class NodeSlot extends SlotBase implements INodeSlot {
pos?: Point
Expand Down Expand Up @@ -130,6 +132,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
slot_type === SlotType.Array ? SlotShape.Grid : this.shape
) as SlotShape

ctx.save()
ctx.beginPath()
let doFill = true

Expand Down Expand Up @@ -163,23 +166,60 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
if (lowQuality) {
ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8)
} else {
let radius: number
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)
Comment on lines +176 to +185
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Truncation of types beyond 3 is silent.

The .slice(0, 3) operation silently drops types beyond the first three with no indication to the user. While limiting visual complexity is reasonable, consider logging a warning or adding a visual indicator when types are truncated.

🤖 Prompt for AI Agents
In src/lib/litegraph/src/node/NodeSlot.ts around lines 175 to 184, the types
array is silently truncated with .slice(0, 3); detect when the computed typesSet
contains more than three entries and surface that fact instead of dropping them
silently — e.g., compute the full typesSet first, if its size > 3 set a
truncation indicator (like this.truncatedTypeCount or this.hasTruncatedTypes)
and either log a single warning via the existing logging mechanism or add an
explicit visual marker to the returned data (such as appending a "...+N"
indicator) so the UI can show there are more types; ensure you still pass the
first three types to existing consumers but also expose the truncation metadata
for display/debugging.

if (types.length > 1) {
doFill = false
doStroke = true
ctx.lineWidth = 3
ctx.strokeStyle = ctx.fillStyle
radius = highlight ? 4 : 3
} else {
// Normal circle
radius = highlight ? 5 : 4
const arcLen = (Math.PI * 2) / types.length
types.forEach((type, idx) => {
ctx.moveTo(pos[0], pos[1])
ctx.fillStyle = type
ctx.arc(
pos[0],
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)
}
}

if (doFill) ctx.fill()
if (!lowQuality && doStroke) ctx.stroke()
ctx.restore()

// render slot label
const hideLabel = lowQuality || this.isWidgetInputSlot
Expand Down
10 changes: 1 addition & 9 deletions src/renderer/extensions/vueNodes/components/InputSlot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@
<!-- Connection Dot -->
<SlotConnectionDot
ref="connectionDotRef"
:color="slotColor"
:class="
cn(
'-translate-x-1/2 w-3',
hasSlotError && 'ring-2 ring-error ring-offset-0 rounded-full'
)
"
:slot-data
@click="onClick"
@dblclick="onDoubleClick"
@pointerdown="onPointerDown"
Expand Down Expand Up @@ -54,7 +54,6 @@ import { computed, onErrorCaptured, ref, watchEffect } from 'vue'
import type { ComponentPublicInstance } from 'vue'

import { useErrorHandling } from '@/composables/useErrorHandling'
import { getSlotColor } from '@/constants/slotColors'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import { useSlotLinkDragUIState } from '@/renderer/core/canvas/links/slotLinkDragUIState'
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
Expand Down Expand Up @@ -111,13 +110,6 @@ onErrorCaptured((error) => {
return false
})

const slotColor = computed(() => {
if (hasSlotError.value) {
return 'var(--color-error)'
}
return getSlotColor(props.slotData.type)
})

const { state: dragState } = useSlotLinkDragUIState()
const slotKey = computed(() =>
getSlotKey(props.nodeId ?? '', props.index, true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
<!-- Connection Dot -->
<SlotConnectionDot
ref="connectionDotRef"
:color="slotColor"
class="w-3 translate-x-1/2"
:slot-data
@pointerdown="onPointerDown"
/>
</div>
Expand All @@ -22,7 +22,6 @@ import { computed, onErrorCaptured, ref, watchEffect } from 'vue'
import type { ComponentPublicInstance } from 'vue'

import { useErrorHandling } from '@/composables/useErrorHandling'
import { getSlotColor } from '@/constants/slotColors'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import { useSlotLinkDragUIState } from '@/renderer/core/canvas/links/slotLinkDragUIState'
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
Expand Down Expand Up @@ -67,9 +66,6 @@ onErrorCaptured((error) => {
return false
})

// Get slot color based on type
const slotColor = computed(() => getSlotColor(props.slotData.type))

const { state: dragState } = useSlotLinkDragUIState()
const slotKey = computed(() =>
getSlotKey(props.nodeId ?? '', props.index, false)
Expand Down
62 changes: 47 additions & 15 deletions src/renderer/extensions/vueNodes/components/SlotConnectionDot.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,45 @@
<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 type { ClassValue } from '@/utils/tailwindUtil'

const props = defineProps<{
color?: string
multi?: boolean
slotData?: INodeSlot
class?: ClassValue
hasError?: boolean
multi?: boolean
}>()

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({
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>

<template>
Expand All @@ -27,19 +52,26 @@ defineExpose({
"
>
<div
v-if="types.length === 1"
ref="slot-el"
class="slot-dot"
:style="{ backgroundColor: color }"
: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'
)
"
:style="{ backgroundColor: types[0] }"
:class="slotClass"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this pulled out of the template?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used twice. I wanted to avoid the extra lines and the chance that the two copies become desynced.

I'm not 100% happy with how the border outline or scaling of optional multi-type inputs work and would like to eventually separate out/re-inline the class, but the amount of extra work this would require is not something I can justify right now.

/>
<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>
</template>
Loading