Skip to content

Commit d63b0f0

Browse files
authored
Subgraph io fixes (#12281)
Fixes 3 different bugs when making links to and from subgraph IO from vue nodes - When dragging a link from a node to a subgraph IO, there is no feedback if a slot is not a valid connection target or if a slot is actively hovered - When a link is made from a subgraph IO to a node, the reactivity is not triggered on the node to indicate a change of link state. - When dragging a link from a subgraph IO to a node, the link would not snap to the valid connection targets on nodes - The fix for this one is not as thorough as I would like. It only allows connections to the slot, not connections to the hovered widget. We have two deeply disconnected linking systems and properly reconciling them would be a multi-week project. Resolves FE-561 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12281-Subgraph-io-fixes-3606d73d365081089f7ef19331c6d70a) by [Unito](https://www.unito.io)
1 parent cd2f467 commit d63b0f0

11 files changed

Lines changed: 120 additions & 16 deletions

File tree

browser_tests/fixtures/helpers/SubgraphHelper.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/w
1111
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
1212
import { SubgraphEditor } from '@e2e/fixtures/components/SubgraphEditor'
1313
import { TestIds } from '@e2e/fixtures/selectors'
14+
import type { Position, Size } from '@e2e/fixtures/types'
1415
import type { NodeReference } from '@e2e/fixtures/utils/litegraphUtils'
1516
import { SubgraphSlotReference } from '@e2e/fixtures/utils/litegraphUtils'
1617

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

245+
async getInputBounds(): Promise<Position & Size> {
246+
return await this.comfyPage.page.evaluate(() => {
247+
const graph = app!.canvas.graph as Subgraph
248+
const inputNode = graph.inputNode
249+
const [x, y] = app!.canvas.ds.convertOffsetToCanvas(inputNode.pos)
250+
const width = inputNode.size[0] * app!.canvas.ds.scale
251+
const height = inputNode.size[1] * app!.canvas.ds.scale
252+
return { x, y, width, height }
253+
})
254+
}
255+
244256
/**
245257
* Connect a regular node output to a subgraph input.
246258
* This creates a new input slot on the subgraph if targetInputName is not provided.

browser_tests/tests/subgraph/subgraphSlots.spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,3 +632,72 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
632632
})
633633
})
634634
})
635+
636+
test(
637+
'link interactions',
638+
{ tag: ['@vue-nodes', '@subgraph'] },
639+
async ({ comfyPage }) => {
640+
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
641+
await comfyPage.vueNodes.enterSubgraph('2')
642+
643+
const ksampler = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
644+
const seedSlot = ksampler.getSlot('seed')
645+
const seedIOSlot = await comfyPage.subgraph.getInputSlot('seed')
646+
647+
await test.step('Make second INT typed connection', async () => {
648+
const toPos = await seedIOSlot.getOpenSlotPosition()
649+
await seedSlot.dragTo(comfyPage.canvas, { targetPosition: toPos })
650+
const isConnected = () => comfyPage.vueNodes.isSlotConnected(seedSlot)
651+
await expect.poll(isConnected).toBe(true)
652+
})
653+
654+
const stepsSlot = ksampler.getSlot('steps')
655+
656+
await test.step('Node -> I/O hover effect', async () => {
657+
await stepsSlot.hover()
658+
await stepsSlot.click({ trial: true })
659+
await comfyPage.page.mouse.down()
660+
await comfyPage.canvas.hover({ position: await seedIOSlot.getPosition() })
661+
662+
const rawClip = await comfyPage.subgraph.getInputBounds()
663+
const absolutePos = await comfyPage.canvasOps.toAbsolute(rawClip)
664+
const clip = { ...rawClip, ...absolutePos }
665+
await expect(comfyPage.page).toHaveScreenshot('vue-io-highlight.png', {
666+
clip
667+
})
668+
669+
//cancel link operation
670+
await stepsSlot.hover()
671+
await comfyPage.page.mouse.up()
672+
})
673+
674+
await ksampler.title.hover()
675+
676+
const slotParent = stepsSlot.locator('../..')
677+
await expect(slotParent, 'unconnected slot is hidden').toHaveCSS(
678+
'opacity',
679+
'0'
680+
)
681+
682+
await test.step('Connect I/O to node with snap', async () => {
683+
const hasSnap = () =>
684+
comfyPage.page.evaluate(() => !!app!.canvas._highlight_pos)
685+
expect(await hasSnap()).toBe(false)
686+
687+
const emptySlotPos = await seedIOSlot.getOpenSlotPosition()
688+
await comfyPage.canvas.hover({ position: emptySlotPos })
689+
await comfyPage.page.mouse.down()
690+
await stepsSlot.hover()
691+
await expect.poll(hasSnap).toBe(true)
692+
await comfyPage.page.mouse.up()
693+
694+
//move hover off the slot
695+
await ksampler.title.hover()
696+
})
697+
698+
await expect(slotParent, 'connected slot is visible').not.toHaveCSS(
699+
'opacity',
700+
'0'
701+
)
702+
}
703+
)
5.03 KB
Loading

src/lib/litegraph/src/LGraphCanvas.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { toString } from 'es-toolkit/compat'
22
import { toValue } from 'vue'
33

44
import { MovingInputLink } from '@/lib/litegraph/src/canvas/MovingInputLink'
5+
import type { RenderLink } from '@/lib/litegraph/src/canvas/RenderLink'
56
import { AutoPanController } from '@/renderer/core/canvas/useAutoPan'
67
import { LitegraphLinkAdapter } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
78
import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
@@ -3306,11 +3307,15 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
33063307
if (result != null) this.dirty_canvas = result
33073308
}
33083309
}
3310+
const firstLink: RenderLink | undefined = linkConnector.renderLinks.at(0)
3311+
const isSubgraphIOLink =
3312+
linkConnector.isConnecting && firstLink?.isIoNodeLink
33093313

33103314
// get node over
3311-
const node = LiteGraph.vueNodesMode
3312-
? null
3313-
: graph.getNodeOnPos(x, y, this.visible_nodes)
3315+
const node =
3316+
LiteGraph.vueNodesMode && !isSubgraphIOLink
3317+
? null
3318+
: graph.getNodeOnPos(x, y, this.visible_nodes)
33143319

33153320
const dragRect = this.dragging_rectangle
33163321
if (dragRect) {
@@ -3401,8 +3406,6 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
34013406

34023407
// Check if link is over anything it could connect to - record position of valid target for snap / highlight
34033408
if (linkConnector.isConnecting) {
3404-
const firstLink = linkConnector.renderLinks.at(0)
3405-
34063409
// Default: nothing highlighted
34073410
let highlightPos: Point | undefined
34083411
let highlightInput: INodeInputSlot | undefined
@@ -3453,7 +3456,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
34533456
highlightInput = node.inputs[inputId]
34543457
}
34553458

3456-
if (highlightInput) {
3459+
if (highlightInput && !LiteGraph.vueNodesMode) {
34573460
const widget = node.getWidgetFromSlot(highlightInput)
34583461
if (widget) linkConnector.overWidget = widget
34593462
}

src/lib/litegraph/src/canvas/RenderLink.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export interface RenderLink {
4343
/** The reroute that the link is being connected from. */
4444
readonly fromReroute?: Reroute
4545

46+
readonly isIoNodeLink?: boolean
47+
4648
/**
4749
* Capability checks used for hit-testing and validation during drag.
4850
* Implementations should return `false` when a connection is not possible

src/lib/litegraph/src/canvas/ToInputFromIoNodeLink.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class ToInputFromIoNodeLink implements RenderLink {
2424
readonly fromPos: Point
2525
fromDirection: LinkDirection = LinkDirection.RIGHT
2626
readonly existingLink?: LLink
27+
readonly isIoNodeLink = true
2728

2829
constructor(
2930
readonly network: LinkNetwork,

src/lib/litegraph/src/canvas/ToOutputFromIoNodeLink.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export class ToOutputFromIoNodeLink implements RenderLink {
2323
readonly fromPos: Point
2424
readonly fromSlotIndex: SlotIndex
2525
fromDirection: LinkDirection = LinkDirection.LEFT
26+
readonly isIoNodeLink = true
2627

2728
constructor(
2829
readonly network: LinkNetwork,

src/lib/litegraph/src/subgraph/SubgraphInput.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,13 @@ export class SubgraphInput extends SubgraphSlot {
136136
}
137137
subgraph.incrementVersion()
138138

139+
subgraph.trigger('node:slot-links:changed', {
140+
nodeId: node.id,
141+
slotType: NodeSlotType.INPUT,
142+
slotIndex: inputIndex,
143+
connected: true,
144+
linkId: link.id
145+
})
139146
node.onConnectionsChange?.(NodeSlotType.INPUT, inputIndex, true, link, slot)
140147

141148
subgraph.afterChange()
@@ -239,11 +246,8 @@ export class SubgraphInput extends SubgraphSlot {
239246
override isValidTarget(
240247
fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput
241248
): boolean {
242-
if (isNodeSlot(fromSlot)) {
243-
return (
244-
'link' in fromSlot &&
245-
LiteGraph.isValidConnection(this.type, fromSlot.type)
246-
)
249+
if (isNodeSlot(fromSlot) && 'link' in fromSlot) {
250+
return LiteGraph.isValidConnection(this.type, fromSlot.type)
247251
}
248252

249253
if (isSubgraphOutput(fromSlot)) {

src/lib/litegraph/src/subgraph/SubgraphInputNode.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,13 @@ export class SubgraphInputNode
226226
link,
227227
subgraphInput
228228
)
229+
subgraph.trigger('node:slot-links:changed', {
230+
nodeId: node.id,
231+
slotType: NodeSlotType.INPUT,
232+
slotIndex: slotIndex,
233+
connected: false,
234+
linkId: link.id
235+
})
229236
}
230237
}
231238

src/lib/litegraph/src/subgraph/SubgraphOutput.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,8 @@ export class SubgraphOutput extends SubgraphSlot {
140140
override isValidTarget(
141141
fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput
142142
): boolean {
143-
if (isNodeSlot(fromSlot)) {
144-
return (
145-
'links' in fromSlot &&
146-
LiteGraph.isValidConnection(fromSlot.type, this.type)
147-
)
143+
if (isNodeSlot(fromSlot) && 'links' in fromSlot) {
144+
return LiteGraph.isValidConnection(fromSlot.type, this.type)
148145
}
149146

150147
if (isSubgraphInput(fromSlot)) {

0 commit comments

Comments
 (0)