Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 12 additions & 0 deletions browser_tests/fixtures/helpers/SubgraphHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/w
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { SubgraphEditor } from '@e2e/fixtures/components/SubgraphEditor'
import { TestIds } from '@e2e/fixtures/selectors'
import type { Position, Size } from '@e2e/fixtures/types'
import type { NodeReference } from '@e2e/fixtures/utils/litegraphUtils'
import { SubgraphSlotReference } from '@e2e/fixtures/utils/litegraphUtils'

Expand Down Expand Up @@ -241,6 +242,17 @@ export class SubgraphHelper {
return new SubgraphSlotReference('output', slotName || '', this.comfyPage)
}

async getInputBounds(): Promise<Position & Size> {
return await this.comfyPage.page.evaluate(() => {
const graph = app!.canvas.graph as Subgraph
const inputNode = graph.inputNode
const [x, y] = app!.canvas.ds.convertOffsetToCanvas(inputNode.pos)
const width = inputNode.size[0] * app!.canvas.ds.scale
const height = inputNode.size[1] * app!.canvas.ds.scale
return { x, y, width, height }
})
}
Comment thread
AustinMroz marked this conversation as resolved.

/**
* Connect a regular node output to a subgraph input.
* This creates a new input slot on the subgraph if targetInputName is not provided.
Expand Down
69 changes: 69 additions & 0 deletions browser_tests/tests/subgraph/subgraphSlots.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,3 +632,72 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
})
})
})

test(
'link interactions',
{ tag: ['@vue-nodes', '@subgraph'] },
async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.vueNodes.enterSubgraph('2')

const ksampler = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
const seedSlot = ksampler.getSlot('seed')
const seedIOSlot = await comfyPage.subgraph.getInputSlot('seed')

await test.step('Make second INT typed connection', async () => {
const toPos = await seedIOSlot.getOpenSlotPosition()
await seedSlot.dragTo(comfyPage.canvas, { targetPosition: toPos })
const isConnected = () => comfyPage.vueNodes.isSlotConnected(seedSlot)
await expect.poll(isConnected).toBe(true)
})

const stepsSlot = ksampler.getSlot('steps')

await test.step('Node -> I/O hover effect', async () => {
await stepsSlot.hover()
await stepsSlot.click({ trial: true })
await comfyPage.page.mouse.down()
await comfyPage.canvas.hover({ position: await seedIOSlot.getPosition() })

const rawClip = await comfyPage.subgraph.getInputBounds()
const absolutePos = await comfyPage.canvasOps.toAbsolute(rawClip)
const clip = { ...rawClip, ...absolutePos }
await expect(comfyPage.page).toHaveScreenshot('vue-io-highlight.png', {
clip
})

//cancel link operation
await stepsSlot.hover()
await comfyPage.page.mouse.up()
})

await ksampler.title.hover()

const slotParent = stepsSlot.locator('../..')
await expect(slotParent, 'unconnected slot is hidden').toHaveCSS(
'opacity',
'0'
)

await test.step('Connect I/O to node with snap', async () => {
const hasSnap = () =>
comfyPage.page.evaluate(() => !!app!.canvas._highlight_pos)
expect(await hasSnap()).toBe(false)

const emptySlotPos = await seedIOSlot.getOpenSlotPosition()
await comfyPage.canvas.hover({ position: emptySlotPos })
await comfyPage.page.mouse.down()
await stepsSlot.hover()
await expect.poll(hasSnap).toBe(true)
await comfyPage.page.mouse.up()

//move hover off the slot
await ksampler.title.hover()
})

await expect(slotParent, 'connected slot is visible').not.toHaveCSS(
'opacity',
'0'
)
}
)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 9 additions & 6 deletions src/lib/litegraph/src/LGraphCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { toValue } from 'vue'

import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
import { MovingInputLink } from '@/lib/litegraph/src/canvas/MovingInputLink'
import type { RenderLink } from '@/lib/litegraph/src/canvas/RenderLink'
import { AutoPanController } from '@/renderer/core/canvas/useAutoPan'
import { LitegraphLinkAdapter } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
Expand Down Expand Up @@ -3307,11 +3308,15 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
if (result != null) this.dirty_canvas = result
}
}
const firstLink: RenderLink | undefined = linkConnector.renderLinks.at(0)
const isSubgraphIOLink =
linkConnector.isConnecting && firstLink?.isIoNodeLink

// get node over
const node = LiteGraph.vueNodesMode
? null
: graph.getNodeOnPos(x, y, this.visible_nodes)
const node =
LiteGraph.vueNodesMode && !isSubgraphIOLink
? null
: graph.getNodeOnPos(x, y, this.visible_nodes)

const dragRect = this.dragging_rectangle
if (dragRect) {
Expand Down Expand Up @@ -3402,8 +3407,6 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>

// Check if link is over anything it could connect to - record position of valid target for snap / highlight
if (linkConnector.isConnecting) {
const firstLink = linkConnector.renderLinks.at(0)

// Default: nothing highlighted
let highlightPos: Point | undefined
let highlightInput: INodeInputSlot | undefined
Expand Down Expand Up @@ -3454,7 +3457,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
highlightInput = node.inputs[inputId]
}

if (highlightInput) {
if (highlightInput && !LiteGraph.vueNodesMode) {
const widget = node.getWidgetFromSlot(highlightInput)
if (widget) linkConnector.overWidget = widget
}
Expand Down
2 changes: 2 additions & 0 deletions src/lib/litegraph/src/canvas/RenderLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export interface RenderLink {
/** The reroute that the link is being connected from. */
readonly fromReroute?: Reroute

readonly isIoNodeLink?: boolean

/**
* Capability checks used for hit-testing and validation during drag.
* Implementations should return `false` when a connection is not possible
Expand Down
1 change: 1 addition & 0 deletions src/lib/litegraph/src/canvas/ToInputFromIoNodeLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class ToInputFromIoNodeLink implements RenderLink {
readonly fromPos: Point
fromDirection: LinkDirection = LinkDirection.RIGHT
readonly existingLink?: LLink
readonly isIoNodeLink = true

constructor(
readonly network: LinkNetwork,
Expand Down
1 change: 1 addition & 0 deletions src/lib/litegraph/src/canvas/ToOutputFromIoNodeLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class ToOutputFromIoNodeLink implements RenderLink {
readonly fromPos: Point
readonly fromSlotIndex: SlotIndex
fromDirection: LinkDirection = LinkDirection.LEFT
readonly isIoNodeLink = true

constructor(
readonly network: LinkNetwork,
Expand Down
14 changes: 9 additions & 5 deletions src/lib/litegraph/src/subgraph/SubgraphInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,13 @@ export class SubgraphInput extends SubgraphSlot {
}
subgraph.incrementVersion()

subgraph.trigger('node:slot-links:changed', {
nodeId: node.id,
slotType: NodeSlotType.INPUT,
slotIndex: inputIndex,
connected: true,
linkId: link.id
})
node.onConnectionsChange?.(NodeSlotType.INPUT, inputIndex, true, link, slot)

subgraph.afterChange()
Expand Down Expand Up @@ -239,11 +246,8 @@ export class SubgraphInput extends SubgraphSlot {
override isValidTarget(
fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput
): boolean {
if (isNodeSlot(fromSlot)) {
return (
'link' in fromSlot &&
LiteGraph.isValidConnection(this.type, fromSlot.type)
)
if (isNodeSlot(fromSlot) && 'link' in fromSlot) {
return LiteGraph.isValidConnection(this.type, fromSlot.type)
}

if (isSubgraphOutput(fromSlot)) {
Expand Down
7 changes: 7 additions & 0 deletions src/lib/litegraph/src/subgraph/SubgraphInputNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,13 @@ export class SubgraphInputNode
link,
subgraphInput
)
subgraph.trigger('node:slot-links:changed', {
nodeId: node.id,
slotType: NodeSlotType.INPUT,
slotIndex: slotIndex,
connected: false,
linkId: link.id
})
}
}

Expand Down
7 changes: 2 additions & 5 deletions src/lib/litegraph/src/subgraph/SubgraphOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,8 @@ export class SubgraphOutput extends SubgraphSlot {
override isValidTarget(
fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput
): boolean {
if (isNodeSlot(fromSlot)) {
return (
'links' in fromSlot &&
LiteGraph.isValidConnection(fromSlot.type, this.type)
)
if (isNodeSlot(fromSlot) && 'links' in fromSlot) {
return LiteGraph.isValidConnection(fromSlot.type, this.type)
}

if (isSubgraphInput(fromSlot)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,12 +411,20 @@ export function useSlotLinkInteraction({
}
const raf = createRafBatch(processPointerMoveFrame)

const canvas = app.canvas
const node = canvas.graph?.getNodeById(nodeId)
Comment thread
AustinMroz marked this conversation as resolved.
const handlePointerMove = (event: PointerEvent) => {
if (!pointerSession.matches(event)) return
event.stopPropagation()

autoPan?.updatePointer(event.clientX, event.clientY)

if (canvas.subgraph && node) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we also guard on canvas.linkConnector.isConnecting or potentially the direction we are linking to not need to call both input & output

Copy link
Copy Markdown
Collaborator Author

@AustinMroz AustinMroz May 19, 2026

Choose a reason for hiding this comment

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

Can we also guard on canvas.linkConnector.isConnecting

The 'handlePointerMove' function this is inside only fires while connecting links.

potentially the direction we are linking to not need to call both input & output

I had intentionally included both for parity with the litegraph implementation, as it will also dim links on the other IONode.

From further review, the litegraph implementations were half-baked and did not filter on input/output. I have corrected this as well.

augmentToCanvasPointerEvent(event, node, canvas)
canvas.subgraph.inputNode.onPointerMove(event)
canvas.subgraph.outputNode.onPointerMove(event)
}

dragContext.pendingPointerMove = {
clientX: event.clientX,
clientY: event.clientY,
Expand Down
Loading