mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-12 00:20:15 +00:00
Slot functionality for vue nodes (#5628)
Allows for simple slot functionality in vue nodes mode. Has: - Drag new link from slot - Connect new link from dropping on slot Now: - Tests After: - Drop on reroute - Correct link color on connect - Drop on node - Hover effects ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5628-Slot-functionality-for-vue-nodes-2716d73d365081c59a3cef7c8a5e539e) by [Unito](https://www.unito.io) --------- Co-authored-by: bymyself <cbyrne@comfy.org> Co-authored-by: AustinMroz <austin@comfy.org> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
73
src/renderer/core/canvas/links/slotLinkCompatibility.ts
Normal file
73
src/renderer/core/canvas/links/slotLinkCompatibility.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { getActivePinia } from 'pinia'
|
||||
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import type {
|
||||
SlotDragSource,
|
||||
SlotDropCandidate
|
||||
} from '@/renderer/core/canvas/links/slotLinkDragState'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
interface CompatibilityResult {
|
||||
allowable: boolean
|
||||
targetNode?: LGraphNode
|
||||
targetSlot?: INodeInputSlot | INodeOutputSlot
|
||||
}
|
||||
|
||||
function resolveNode(nodeId: NodeId) {
|
||||
const pinia = getActivePinia()
|
||||
const canvasStore = pinia ? useCanvasStore() : null
|
||||
const graph = canvasStore?.canvas?.graph ?? app.canvas?.graph
|
||||
if (!graph) return null
|
||||
const id = typeof nodeId === 'string' ? Number(nodeId) : nodeId
|
||||
if (Number.isNaN(id)) return null
|
||||
return graph.getNodeById(id)
|
||||
}
|
||||
|
||||
export function evaluateCompatibility(
|
||||
source: SlotDragSource,
|
||||
candidate: SlotDropCandidate
|
||||
): CompatibilityResult {
|
||||
if (candidate.layout.nodeId === source.nodeId) {
|
||||
return { allowable: false }
|
||||
}
|
||||
|
||||
const isOutputToInput =
|
||||
source.type === 'output' && candidate.layout.type === 'input'
|
||||
const isInputToOutput =
|
||||
source.type === 'input' && candidate.layout.type === 'output'
|
||||
|
||||
if (!isOutputToInput && !isInputToOutput) {
|
||||
return { allowable: false }
|
||||
}
|
||||
|
||||
const sourceNode = resolveNode(source.nodeId)
|
||||
const targetNode = resolveNode(candidate.layout.nodeId)
|
||||
if (!sourceNode || !targetNode) {
|
||||
return { allowable: false }
|
||||
}
|
||||
|
||||
if (isOutputToInput) {
|
||||
const outputSlot = sourceNode.outputs?.[source.slotIndex]
|
||||
const inputSlot = targetNode.inputs?.[candidate.layout.index]
|
||||
if (!outputSlot || !inputSlot) {
|
||||
return { allowable: false }
|
||||
}
|
||||
|
||||
const allowable = sourceNode.canConnectTo(targetNode, inputSlot, outputSlot)
|
||||
return { allowable, targetNode, targetSlot: inputSlot }
|
||||
}
|
||||
|
||||
const inputSlot = sourceNode.inputs?.[source.slotIndex]
|
||||
const outputSlot = targetNode.outputs?.[candidate.layout.index]
|
||||
if (!inputSlot || !outputSlot) {
|
||||
return { allowable: false }
|
||||
}
|
||||
|
||||
const allowable = targetNode.canConnectTo(sourceNode, inputSlot, outputSlot)
|
||||
return { allowable, targetNode, targetSlot: outputSlot }
|
||||
}
|
||||
95
src/renderer/core/canvas/links/slotLinkDragState.ts
Normal file
95
src/renderer/core/canvas/links/slotLinkDragState.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { reactive, readonly } from 'vue'
|
||||
|
||||
import type { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import type { Point, SlotLayout } from '@/renderer/core/layout/types'
|
||||
|
||||
type SlotDragType = 'input' | 'output'
|
||||
|
||||
export interface SlotDragSource {
|
||||
nodeId: string
|
||||
slotIndex: number
|
||||
type: SlotDragType
|
||||
direction: LinkDirection
|
||||
position: Readonly<Point>
|
||||
}
|
||||
|
||||
export interface SlotDropCandidate {
|
||||
layout: SlotLayout
|
||||
compatible: boolean
|
||||
}
|
||||
|
||||
interface PointerPosition {
|
||||
client: Point
|
||||
canvas: Point
|
||||
}
|
||||
|
||||
interface SlotDragState {
|
||||
active: boolean
|
||||
pointerId: number | null
|
||||
source: SlotDragSource | null
|
||||
pointer: PointerPosition
|
||||
candidate: SlotDropCandidate | null
|
||||
}
|
||||
|
||||
const state = reactive<SlotDragState>({
|
||||
active: false,
|
||||
pointerId: null,
|
||||
source: null,
|
||||
pointer: {
|
||||
client: { x: 0, y: 0 },
|
||||
canvas: { x: 0, y: 0 }
|
||||
},
|
||||
candidate: null
|
||||
})
|
||||
|
||||
function updatePointerPosition(
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
canvasX: number,
|
||||
canvasY: number
|
||||
) {
|
||||
state.pointer.client.x = clientX
|
||||
state.pointer.client.y = clientY
|
||||
state.pointer.canvas.x = canvasX
|
||||
state.pointer.canvas.y = canvasY
|
||||
}
|
||||
|
||||
function setCandidate(candidate: SlotDropCandidate | null) {
|
||||
state.candidate = candidate
|
||||
}
|
||||
|
||||
function beginDrag(source: SlotDragSource, pointerId: number) {
|
||||
state.active = true
|
||||
state.source = source
|
||||
state.pointerId = pointerId
|
||||
state.candidate = null
|
||||
}
|
||||
|
||||
function endDrag() {
|
||||
state.active = false
|
||||
state.pointerId = null
|
||||
state.source = null
|
||||
state.pointer.client.x = 0
|
||||
state.pointer.client.y = 0
|
||||
state.pointer.canvas.x = 0
|
||||
state.pointer.canvas.y = 0
|
||||
state.candidate = null
|
||||
}
|
||||
|
||||
function getSlotLayout(nodeId: string, slotIndex: number, isInput: boolean) {
|
||||
const slotKey = getSlotKey(nodeId, slotIndex, isInput)
|
||||
return layoutStore.getSlotLayout(slotKey)
|
||||
}
|
||||
|
||||
export function useSlotLinkDragState() {
|
||||
return {
|
||||
state: readonly(state),
|
||||
beginDrag,
|
||||
endDrag,
|
||||
updatePointerPosition,
|
||||
setCandidate,
|
||||
getSlotLayout
|
||||
}
|
||||
}
|
||||
95
src/renderer/core/canvas/links/slotLinkPreviewRenderer.ts
Normal file
95
src/renderer/core/canvas/links/slotLinkPreviewRenderer.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
ReadOnlyPoint
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { resolveConnectingLinkColor } from '@/lib/litegraph/src/utils/linkColors'
|
||||
import {
|
||||
type SlotDragSource,
|
||||
useSlotLinkDragState
|
||||
} from '@/renderer/core/canvas/links/slotLinkDragState'
|
||||
import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
|
||||
|
||||
function buildContext(canvas: LGraphCanvas): LinkRenderContext {
|
||||
return {
|
||||
renderMode: canvas.links_render_mode,
|
||||
connectionWidth: canvas.connections_width,
|
||||
renderBorder: canvas.render_connections_border,
|
||||
lowQuality: canvas.low_quality,
|
||||
highQualityRender: canvas.highquality_render,
|
||||
scale: canvas.ds.scale,
|
||||
linkMarkerShape: canvas.linkMarkerShape,
|
||||
renderConnectionArrows: canvas.render_connection_arrows,
|
||||
highlightedLinks: new Set(Object.keys(canvas.highlighted_links)),
|
||||
defaultLinkColor: canvas.default_link_color,
|
||||
linkTypeColors: (canvas.constructor as typeof LGraphCanvas)
|
||||
.link_type_colors,
|
||||
disabledPattern: canvas._pattern
|
||||
}
|
||||
}
|
||||
|
||||
export function attachSlotLinkPreviewRenderer(canvas: LGraphCanvas) {
|
||||
const originalOnDrawForeground = canvas.onDrawForeground?.bind(canvas)
|
||||
const patched = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
area: LGraphCanvas['visible_area']
|
||||
) => {
|
||||
originalOnDrawForeground?.(ctx, area)
|
||||
|
||||
const { state } = useSlotLinkDragState()
|
||||
if (!state.active || !state.source) return
|
||||
|
||||
const { pointer, source } = state
|
||||
const start = source.position
|
||||
const sourceSlot = resolveSourceSlot(canvas, source)
|
||||
|
||||
const linkRenderer = canvas.linkRenderer
|
||||
if (!linkRenderer) return
|
||||
|
||||
const context = buildContext(canvas)
|
||||
|
||||
const from: ReadOnlyPoint = [start.x, start.y]
|
||||
const to: ReadOnlyPoint = [pointer.canvas.x, pointer.canvas.y]
|
||||
|
||||
const startDir = source.direction ?? LinkDirection.RIGHT
|
||||
const endDir = LinkDirection.CENTER
|
||||
|
||||
const colour = resolveConnectingLinkColor(sourceSlot?.type)
|
||||
|
||||
ctx.save()
|
||||
|
||||
linkRenderer.renderDraggingLink(
|
||||
ctx,
|
||||
from,
|
||||
to,
|
||||
colour,
|
||||
startDir,
|
||||
endDir,
|
||||
context
|
||||
)
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
canvas.onDrawForeground = patched
|
||||
}
|
||||
|
||||
function resolveSourceSlot(
|
||||
canvas: LGraphCanvas,
|
||||
source: SlotDragSource
|
||||
): INodeInputSlot | INodeOutputSlot | undefined {
|
||||
const graph = canvas.graph
|
||||
if (!graph) return undefined
|
||||
|
||||
const nodeId = Number(source.nodeId)
|
||||
if (!Number.isFinite(nodeId)) return undefined
|
||||
|
||||
const node = graph.getNodeById(nodeId)
|
||||
if (!node) return undefined
|
||||
|
||||
return source.type === 'output'
|
||||
? node.outputs?.[source.slotIndex]
|
||||
: node.inputs?.[source.slotIndex]
|
||||
}
|
||||
@@ -7,13 +7,10 @@
|
||||
* Maintains backward compatibility with existing litegraph integration.
|
||||
*/
|
||||
import type { LGraph } from '@/lib/litegraph/src/LGraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LLink } from '@/lib/litegraph/src/LLink'
|
||||
import type { Reroute } from '@/lib/litegraph/src/Reroute'
|
||||
import type {
|
||||
CanvasColour,
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
ReadOnlyPoint
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
@@ -27,7 +24,6 @@ import {
|
||||
type ArrowShape,
|
||||
CanvasPathRenderer,
|
||||
type Direction,
|
||||
type DragLinkData,
|
||||
type LinkRenderData,
|
||||
type RenderContext as PathRenderContext,
|
||||
type Point,
|
||||
@@ -209,7 +205,6 @@ export class LitegraphLinkAdapter {
|
||||
case LinkDirection.DOWN:
|
||||
return 'down'
|
||||
case LinkDirection.CENTER:
|
||||
case LinkDirection.NONE:
|
||||
return 'none'
|
||||
default:
|
||||
return 'right'
|
||||
@@ -502,57 +497,33 @@ export class LitegraphLinkAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a link being dragged from a slot to mouse position
|
||||
* Used during link creation/reconnection
|
||||
*/
|
||||
renderDraggingLink(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
fromNode: LGraphNode | null,
|
||||
fromSlot: INodeOutputSlot | INodeInputSlot,
|
||||
fromSlotIndex: number,
|
||||
toPosition: ReadOnlyPoint,
|
||||
context: LinkRenderContext,
|
||||
options: {
|
||||
fromInput?: boolean
|
||||
color?: CanvasColour
|
||||
disabled?: boolean
|
||||
} = {}
|
||||
from: ReadOnlyPoint,
|
||||
to: ReadOnlyPoint,
|
||||
colour: CanvasColour,
|
||||
startDir: LinkDirection,
|
||||
endDir: LinkDirection,
|
||||
context: LinkRenderContext
|
||||
): void {
|
||||
if (!fromNode) return
|
||||
|
||||
// Get slot position using layout tree if available
|
||||
const slotPos = getSlotPosition(
|
||||
fromNode,
|
||||
fromSlotIndex,
|
||||
options.fromInput || false
|
||||
this.renderLinkDirect(
|
||||
ctx,
|
||||
from,
|
||||
to,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
colour,
|
||||
startDir,
|
||||
endDir,
|
||||
{
|
||||
...context,
|
||||
linkMarkerShape: LinkMarkerShape.None
|
||||
},
|
||||
{
|
||||
disabled: false
|
||||
}
|
||||
)
|
||||
if (!slotPos) return
|
||||
|
||||
// Get slot direction
|
||||
const slotDir =
|
||||
fromSlot.dir ||
|
||||
(options.fromInput ? LinkDirection.LEFT : LinkDirection.RIGHT)
|
||||
|
||||
// Create drag data
|
||||
const dragData: DragLinkData = {
|
||||
fixedPoint: { x: slotPos[0], y: slotPos[1] },
|
||||
fixedDirection: this.convertDirection(slotDir),
|
||||
dragPoint: { x: toPosition[0], y: toPosition[1] },
|
||||
color: options.color ? String(options.color) : undefined,
|
||||
type: fromSlot.type !== undefined ? String(fromSlot.type) : undefined,
|
||||
disabled: options.disabled || false,
|
||||
fromInput: options.fromInput || false
|
||||
}
|
||||
|
||||
// Convert context
|
||||
const pathContext = this.convertToPathRenderContext(context)
|
||||
|
||||
// Hide center marker when dragging links
|
||||
pathContext.style.showCenterMarker = false
|
||||
|
||||
// Render using pure renderer
|
||||
this.pathRenderer.drawDraggingLink(ctx, dragData, pathContext)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -70,7 +70,7 @@ export interface RenderContext {
|
||||
highlightedIds?: Set<string>
|
||||
}
|
||||
|
||||
export interface DragLinkData {
|
||||
interface DragLinkData {
|
||||
/** Fixed end - the slot being dragged from */
|
||||
fixedPoint: Point
|
||||
fixedDirection: Direction
|
||||
|
||||
Reference in New Issue
Block a user