Skip to content

Commit 5897e00

Browse files
authored
Fix disconnection of subgraphInput links (#6258)
`LLink.disconnect` is intended to cleanup only the link itself. #4800 mistakenly assumed that it would perform all required steps for disconnection. Later, #5015 would partially resolve this by adding some of the missing functionality into `LLink.disconnect`, but this still left output cleanup unhandled and failed to call `node.onConnectionsChanged`. This PR instead moves the disconnection code to call the function that already has robust handling for these items and removes the no-longer-needed and potentially misleading workaround. Resolves #6247 Also un-skipped several SubgraphIO tests. They appear to function fine. I'm assuming the reasons for them being skipped have been resolved. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6258-Fix-disconnection-of-subgraphInput-links-2966d73d36508112ad1fe602cdcf461b) by [Unito](https://www.unito.io)
1 parent 233004c commit 5897e00

File tree

4 files changed

+37
-100
lines changed

4 files changed

+37
-100
lines changed

src/lib/litegraph/src/LLink.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -418,18 +418,6 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
418418
* If `input` or `output`, reroutes will not be automatically removed, and retain a connection to the input or output, respectively.
419419
*/
420420
disconnect(network: LinkNetwork, keepReroutes?: 'input' | 'output'): void {
421-
// Clean up the target node's input slot
422-
if (this.target_id !== -1) {
423-
const targetNode = network.getNodeById(this.target_id)
424-
if (targetNode) {
425-
const targetInput = targetNode.inputs?.[this.target_slot]
426-
if (targetInput && targetInput.link === this.id) {
427-
targetInput.link = null
428-
targetNode.setDirtyCanvas?.(true, false)
429-
}
430-
}
431-
}
432-
433421
const reroutes = LLink.getReroutes(network, this)
434422

435423
const lastReroute = reroutes.at(-1)

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ export class ToInputFromIoNodeLink implements RenderLink {
137137
}
138138
disconnect(): boolean {
139139
if (!this.existingLink) return false
140-
this.existingLink.disconnect(this.network, 'input')
140+
const { input, inputNode } = this.existingLink.resolve(this.network)
141+
if (!inputNode || !input) return false
142+
this.node._disconnectNodeInput(inputNode, input, this.existingLink)
141143
return true
142144
}
143145
}
Lines changed: 2 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { describe, expect, it, vi } from 'vitest'
1+
import { describe, expect } from 'vitest'
22

3-
import { LGraph, LGraphNode, LLink } from '@/lib/litegraph/src/litegraph'
3+
import { LLink } from '@/lib/litegraph/src/litegraph'
44

55
import { test } from './fixtures/testExtensions'
66

@@ -14,84 +14,4 @@ describe('LLink', () => {
1414
const link = new LLink(1, 'float', 4, 2, 5, 3)
1515
expect(link.serialize()).toMatchSnapshot('Basic')
1616
})
17-
18-
describe('disconnect', () => {
19-
it('should clear the target input link reference when disconnecting', () => {
20-
// Create a graph and nodes
21-
const graph = new LGraph()
22-
const sourceNode = new LGraphNode('Source')
23-
const targetNode = new LGraphNode('Target')
24-
25-
// Add nodes to graph
26-
graph.add(sourceNode)
27-
graph.add(targetNode)
28-
29-
// Add slots
30-
sourceNode.addOutput('out', 'number')
31-
targetNode.addInput('in', 'number')
32-
33-
// Connect the nodes
34-
const link = sourceNode.connect(0, targetNode, 0)
35-
expect(link).toBeDefined()
36-
expect(targetNode.inputs[0].link).toBe(link?.id)
37-
38-
// Mock setDirtyCanvas
39-
const setDirtyCanvasSpy = vi.spyOn(targetNode, 'setDirtyCanvas')
40-
41-
// Disconnect the link
42-
link?.disconnect(graph)
43-
44-
// Verify the target input's link reference is cleared
45-
expect(targetNode.inputs[0].link).toBeNull()
46-
47-
// Verify setDirtyCanvas was called
48-
expect(setDirtyCanvasSpy).toHaveBeenCalledWith(true, false)
49-
})
50-
51-
it('should handle disconnecting when target node is not found', () => {
52-
// Create a link with invalid target
53-
const graph = new LGraph()
54-
const link = new LLink(1, 'number', 1, 0, 999, 0) // Invalid target id
55-
56-
// Should not throw when disconnecting
57-
expect(() => link.disconnect(graph)).not.toThrow()
58-
})
59-
60-
it('should only clear link reference if it matches the current link id', () => {
61-
// Create a graph and nodes
62-
const graph = new LGraph()
63-
const sourceNode1 = new LGraphNode('Source1')
64-
const sourceNode2 = new LGraphNode('Source2')
65-
const targetNode = new LGraphNode('Target')
66-
67-
// Add nodes to graph
68-
graph.add(sourceNode1)
69-
graph.add(sourceNode2)
70-
graph.add(targetNode)
71-
72-
// Add slots
73-
sourceNode1.addOutput('out', 'number')
74-
sourceNode2.addOutput('out', 'number')
75-
targetNode.addInput('in', 'number')
76-
77-
// Create first connection
78-
const link1 = sourceNode1.connect(0, targetNode, 0)
79-
expect(link1).toBeDefined()
80-
81-
// Disconnect first connection
82-
targetNode.disconnectInput(0)
83-
84-
// Create second connection
85-
const link2 = sourceNode2.connect(0, targetNode, 0)
86-
expect(link2).toBeDefined()
87-
expect(targetNode.inputs[0].link).toBe(link2?.id)
88-
89-
// Try to disconnect the first link (which is already disconnected)
90-
// It should not affect the current connection
91-
link1?.disconnect(graph)
92-
93-
// The input should still have the second link
94-
expect(targetNode.inputs[0].link).toBe(link2?.id)
95-
})
96-
})
9717
})

tests-ui/tests/litegraph/subgraph/SubgraphIO.test.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
import { describe, expect, it } from 'vitest'
33

44
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
5+
import { ToInputFromIoNodeLink } from '@/lib/litegraph/src/canvas/ToInputFromIoNodeLink'
6+
import { LinkDirection } from '@/lib/litegraph/src//types/globalEnums'
57

68
import { subgraphTest } from './fixtures/subgraphFixtures'
79
import {
810
createTestSubgraph,
911
createTestSubgraphNode
1012
} from './fixtures/subgraphHelpers'
1113

12-
describe.skip('SubgraphIO - Input Slot Dual-Nature Behavior', () => {
14+
describe('SubgraphIO - Input Slot Dual-Nature Behavior', () => {
1315
subgraphTest(
1416
'input accepts external connections from parent graph',
1517
({ subgraphWithNode }) => {
@@ -77,6 +79,31 @@ describe.skip('SubgraphIO - Input Slot Dual-Nature Behavior', () => {
7779
}
7880
)
7981

82+
subgraphTest('handles link disconnection', ({ subgraphWithNode }) => {
83+
const { subgraph } = subgraphWithNode
84+
const internalNode = new LGraphNode('External Source')
85+
internalNode.addInput('in', '*')
86+
internalNode.onConnectionsChange = vi.fn()
87+
subgraph.add(internalNode)
88+
89+
const link = subgraph.inputNode.slots[0].connect(
90+
internalNode.inputs[0],
91+
internalNode
92+
)
93+
new ToInputFromIoNodeLink(
94+
subgraph,
95+
subgraph.inputNode,
96+
subgraph.inputNode.slots[0],
97+
undefined,
98+
LinkDirection.CENTER,
99+
link
100+
).disconnect()
101+
102+
expect(internalNode.inputs[0].link).toBeNull()
103+
expect(subgraph.inputNode.slots[0].linkIds.length).toBe(0)
104+
expect(internalNode.onConnectionsChange).toHaveBeenCalled()
105+
})
106+
80107
subgraphTest(
81108
'handles slot renaming with active connections',
82109
({ subgraphWithNode }) => {
@@ -103,7 +130,7 @@ describe.skip('SubgraphIO - Input Slot Dual-Nature Behavior', () => {
103130
)
104131
})
105132

106-
describe.skip('SubgraphIO - Output Slot Dual-Nature Behavior', () => {
133+
describe('SubgraphIO - Output Slot Dual-Nature Behavior', () => {
107134
subgraphTest(
108135
'output provides connections to parent graph',
109136
({ subgraphWithNode }) => {
@@ -199,7 +226,7 @@ describe.skip('SubgraphIO - Output Slot Dual-Nature Behavior', () => {
199226
)
200227
})
201228

202-
describe.skip('SubgraphIO - Boundary Connection Management', () => {
229+
describe('SubgraphIO - Boundary Connection Management', () => {
203230
subgraphTest(
204231
'verifies cross-boundary link resolution',
205232
({ complexSubgraph }) => {
@@ -309,7 +336,7 @@ describe.skip('SubgraphIO - Boundary Connection Management', () => {
309336
)
310337
})
311338

312-
describe.skip('SubgraphIO - Advanced Scenarios', () => {
339+
describe('SubgraphIO - Advanced Scenarios', () => {
313340
it('handles multiple inputs and outputs with complex connections', () => {
314341
const subgraph = createTestSubgraph({
315342
name: 'Complex IO Test',
@@ -399,7 +426,7 @@ describe.skip('SubgraphIO - Advanced Scenarios', () => {
399426
})
400427
})
401428

402-
describe.skip('SubgraphIO - Empty Slot Connection', () => {
429+
describe('SubgraphIO - Empty Slot Connection', () => {
403430
subgraphTest(
404431
'creates new input and connects when dragging from empty slot inside subgraph',
405432
({ subgraphWithNode }) => {

0 commit comments

Comments
 (0)