Skip to content

Commit a568c06

Browse files
benceruleanluclaudechristian-byrne
authored
Fix "Dragging from input slot connected to SubgraphInputNode creates new link instead of moving existing one" (#1184)
Co-authored-by: Claude <[email protected]> Co-authored-by: bymyself <[email protected]>
1 parent 46a486c commit a568c06

File tree

5 files changed

+127
-23
lines changed

5 files changed

+127
-23
lines changed

src/canvas/LinkConnector.ts

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -150,25 +150,59 @@ export class LinkConnector {
150150
const link = network.links.get(linkId)
151151
if (!link) return
152152

153-
try {
154-
const reroute = network.getReroute(link.parentId)
155-
const renderLink = new MovingInputLink(network, link, reroute)
153+
// Special handling for links from subgraph input nodes
154+
if (link.origin_id === SUBGRAPH_INPUT_ID) {
155+
// For subgraph input links, we need to handle them differently
156+
// since they don't have a regular output node
157+
const subgraphInput = network.inputNode?.slots[link.origin_slot]
158+
if (!subgraphInput) {
159+
console.warn(`Could not find subgraph input for slot [${link.origin_slot}]`)
160+
return
161+
}
156162

157-
const mayContinue = this.events.dispatch("before-move-input", renderLink)
158-
if (mayContinue === false) return
163+
try {
164+
const reroute = network.getReroute(link.parentId)
165+
const renderLink = new ToInputFromIoNodeLink(network, network.inputNode, subgraphInput, reroute, LinkDirection.CENTER, link)
159166

160-
renderLinks.push(renderLink)
167+
// Note: We don't dispatch the before-move-input event for subgraph input links
168+
// as the event type doesn't support ToInputFromIoNodeLink
161169

162-
this.listenUntilReset("input-moved", (e) => {
163-
e.detail.link.disconnect(network, "output")
164-
})
165-
} catch (error) {
166-
console.warn(`Could not create render link for link id: [${link.id}].`, link, error)
167-
return
168-
}
170+
renderLinks.push(renderLink)
171+
172+
this.listenUntilReset("input-moved", () => {
173+
link.disconnect(network, "input")
174+
})
175+
} catch (error) {
176+
console.warn(`Could not create render link for subgraph input link id: [${link.id}].`, link, error)
177+
return
178+
}
179+
180+
link._dragging = true
181+
inputLinks.push(link)
182+
} else {
183+
// Regular node links
184+
try {
185+
const reroute = network.getReroute(link.parentId)
186+
const renderLink = new MovingInputLink(network, link, reroute)
187+
188+
const mayContinue = this.events.dispatch("before-move-input", renderLink)
189+
if (mayContinue === false) return
190+
191+
renderLinks.push(renderLink)
192+
193+
this.listenUntilReset("input-moved", (e) => {
194+
if ("link" in e.detail && e.detail.link) {
195+
e.detail.link.disconnect(network, "output")
196+
}
197+
})
198+
} catch (error) {
199+
console.warn(`Could not create render link for link id: [${link.id}].`, link, error)
200+
return
201+
}
169202

170-
link._dragging = true
171-
inputLinks.push(link)
203+
link._dragging = true
204+
inputLinks.push(link)
205+
}
172206
}
173207

174208
state.connectingTo = "input"

src/canvas/ToInputFromIoNodeLink.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ export class ToInputFromIoNodeLink implements RenderLink {
1818
readonly fromSlotIndex: number
1919
readonly fromPos: Point
2020
fromDirection: LinkDirection = LinkDirection.RIGHT
21+
readonly existingLink?: LLink
2122

2223
constructor(
2324
readonly network: LinkNetwork,
2425
readonly node: SubgraphInputNode,
2526
readonly fromSlot: SubgraphInput,
2627
readonly fromReroute?: Reroute,
2728
public dragDirection: LinkDirection = LinkDirection.CENTER,
29+
existingLink?: LLink,
2830
) {
2931
const outputIndex = node.slots.indexOf(fromSlot)
3032
if (outputIndex === -1 && fromSlot !== node.emptySlot) {
@@ -35,6 +37,7 @@ export class ToInputFromIoNodeLink implements RenderLink {
3537
this.fromPos = fromReroute
3638
? fromReroute.pos
3739
: fromSlot.pos
40+
this.existingLink = existingLink
3841
}
3942

4043
canConnectToInput(inputNode: NodeLike, input: INodeInputSlot): boolean {
@@ -46,10 +49,17 @@ export class ToInputFromIoNodeLink implements RenderLink {
4649
}
4750

4851
connectToInput(node: LGraphNode, input: INodeInputSlot, events: CustomEventTarget<LinkConnectorEventMap>) {
49-
const { fromSlot, fromReroute } = this
52+
const { fromSlot, fromReroute, existingLink } = this
5053

5154
const newLink = fromSlot.connect(input, node, fromReroute?.id)
52-
events.dispatch("link-created", newLink)
55+
56+
if (existingLink) {
57+
// Moving an existing link
58+
events.dispatch("input-moved", this)
59+
} else {
60+
// Creating a new link
61+
events.dispatch("link-created", newLink)
62+
}
5363
}
5464

5565
connectToSubgraphOutput(): void {
@@ -96,7 +106,14 @@ export class ToInputFromIoNodeLink implements RenderLink {
96106
}
97107
}
98108
}
99-
events.dispatch("link-created", newLink)
109+
110+
if (this.existingLink) {
111+
// Moving an existing link
112+
events.dispatch("input-moved", this)
113+
} else {
114+
// Creating a new link
115+
events.dispatch("link-created", newLink)
116+
}
100117
}
101118

102119
connectToOutput() {

src/infrastructure/LinkConnectorEventMap.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { FloatingRenderLink } from "@/canvas/FloatingRenderLink"
22
import type { MovingInputLink } from "@/canvas/MovingInputLink"
33
import type { MovingOutputLink } from "@/canvas/MovingOutputLink"
44
import type { RenderLink } from "@/canvas/RenderLink"
5+
import type { ToInputFromIoNodeLink } from "@/canvas/ToInputFromIoNodeLink"
56
import type { ToInputRenderLink } from "@/canvas/ToInputRenderLink"
67
import type { LGraphNode } from "@/LGraphNode"
78
import type { LLink } from "@/LLink"
@@ -26,7 +27,7 @@ export interface LinkConnectorEventMap {
2627
"before-move-input": MovingInputLink | FloatingRenderLink
2728
"before-move-output": MovingOutputLink | FloatingRenderLink
2829

29-
"input-moved": MovingInputLink | FloatingRenderLink
30+
"input-moved": MovingInputLink | FloatingRenderLink | ToInputFromIoNodeLink
3031
"output-moved": MovingOutputLink | FloatingRenderLink
3132

3233
"link-created": LLink | null | undefined

test/subgraph/SubgraphSlotConnections.test.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import { describe, expect, it } from "vitest"
1+
import { describe, expect, it, vi } from "vitest"
22

3-
import { LGraphNode } from "@/litegraph"
3+
import { LinkConnector } from "@/canvas/LinkConnector"
4+
import { ToInputFromIoNodeLink } from "@/canvas/ToInputFromIoNodeLink"
5+
import { SUBGRAPH_INPUT_ID } from "@/constants"
6+
import { LGraphNode, type LinkNetwork } from "@/litegraph"
47
import { NodeInputSlot } from "@/node/NodeInputSlot"
58
import { NodeOutputSlot } from "@/node/NodeOutputSlot"
69
import { isSubgraphInput, isSubgraphOutput } from "@/subgraph/subgraphUtils"
@@ -103,6 +106,55 @@ describe("Subgraph slot connections", () => {
103106
})
104107
})
105108

109+
describe("LinkConnector dragging behavior", () => {
110+
it("should drag existing link when dragging from input slot connected to subgraph input node", () => {
111+
// Create a subgraph with one input
112+
const subgraph = createTestSubgraph({
113+
inputs: [{ name: "input1", type: "number" }],
114+
})
115+
116+
// Create a node inside the subgraph
117+
const internalNode = new LGraphNode("InternalNode")
118+
internalNode.id = 100
119+
internalNode.addInput("in", "number")
120+
subgraph.add(internalNode)
121+
122+
// Connect the subgraph input to the internal node's input
123+
const link = subgraph.inputNode.slots[0].connect(internalNode.inputs[0], internalNode)
124+
expect(link).toBeDefined()
125+
expect(link!.origin_id).toBe(SUBGRAPH_INPUT_ID)
126+
expect(link!.target_id).toBe(internalNode.id)
127+
128+
// Verify the input slot has the link
129+
expect(internalNode.inputs[0].link).toBe(link!.id)
130+
131+
// Create a LinkConnector
132+
const setConnectingLinks = vi.fn()
133+
const connector = new LinkConnector(setConnectingLinks)
134+
135+
// Now try to drag from the input slot
136+
connector.moveInputLink(subgraph as LinkNetwork, internalNode.inputs[0])
137+
138+
// Verify that we're dragging the existing link
139+
expect(connector.isConnecting).toBe(true)
140+
expect(connector.state.connectingTo).toBe("input")
141+
expect(connector.state.draggingExistingLinks).toBe(true)
142+
143+
// Check that we have exactly one render link
144+
expect(connector.renderLinks).toHaveLength(1)
145+
146+
// The render link should be a ToInputFromIoNodeLink, not MovingInputLink
147+
expect(connector.renderLinks[0]).toBeInstanceOf(ToInputFromIoNodeLink)
148+
149+
// The input links collection should contain our link
150+
expect(connector.inputLinks).toHaveLength(1)
151+
expect(connector.inputLinks[0]).toBe(link)
152+
153+
// Verify the link is marked as dragging
154+
expect(link!._dragging).toBe(true)
155+
})
156+
})
157+
106158
describe("Type compatibility", () => {
107159
it("should respect type compatibility for SubgraphInput connections", () => {
108160
const subgraph = createTestSubgraph({

test/subgraph/SubgraphWidgetPromotion.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ISlotType } from "@/interfaces"
22
import type { TWidgetType } from "@/types/widgets"
33

4-
import { describe, expect, it, vi } from "vitest"
4+
import { describe, expect, it } from "vitest"
55

66
import { LGraphNode, Subgraph } from "@/litegraph"
77
import { BaseWidget } from "@/widgets/BaseWidget"
@@ -339,7 +339,7 @@ describe("SubgraphWidgetPromotion", () => {
339339

340340
// The promoted widget should preserve the original tooltip
341341
expect(promotedWidget.tooltip).toBe(originalTooltip)
342-
342+
343343
// The promoted widget should still function normally
344344
expect(promotedWidget.name).toBe("value") // Uses subgraph input name
345345
expect(promotedWidget.type).toBe("number")

0 commit comments

Comments
 (0)