From fcebbbcba82d997dad4ae07b8973743c1da6e8b6 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Tue, 29 Jul 2025 00:20:37 -0700 Subject: [PATCH] [fix] Allow creating connections from empty subgraph slots (#1167) --- src/LGraphCanvas.ts | 6 +++++- src/canvas/LinkConnector.ts | 18 ++++++++-------- src/subgraph/SubgraphInputNode.ts | 12 +++++++++++ test/subgraph/SubgraphIO.test.ts | 35 +++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/LGraphCanvas.ts b/src/LGraphCanvas.ts index 6d4764f35..a644c3273 100644 --- a/src/LGraphCanvas.ts +++ b/src/LGraphCanvas.ts @@ -6026,7 +6026,11 @@ export class LGraphCanvas implements CustomEventDispatcher } const { name } = slotX iSlotConn = nodeX.slots.findIndex(s => s.name === name) - slotX = nodeX.slots[iSlotConn] + // If it's not found in the main slots, it might be the empty slot from a Subgraph node. + // In that case, the original `slotX` object is the correct one, so don't overwrite it. + if (iSlotConn !== -1) { + slotX = nodeX.slots[iSlotConn] + } if (!slotX) { console.warn("Cant get slot information", slotX) return diff --git a/src/canvas/LinkConnector.ts b/src/canvas/LinkConnector.ts index b12893147..c2bd9cf76 100644 --- a/src/canvas/LinkConnector.ts +++ b/src/canvas/LinkConnector.ts @@ -283,6 +283,8 @@ export class LinkConnector { this.renderLinks.push(renderLink) this.state.connectingTo = "input" + + this.#setLegacyLinks(false) } dragNewFromSubgraphOutput(network: LinkNetwork, outputNode: SubgraphOutputNode, output: SubgraphOutput, fromReroute?: Reroute): void { @@ -292,6 +294,8 @@ export class LinkConnector { this.renderLinks.push(renderLink) this.state.connectingTo = "output" + + this.#setLegacyLinks(true) } /** @@ -425,19 +429,15 @@ export class LinkConnector { } /** - * Connects the links being droppe + * Connects the links being dropped * @param event Contains the drop location, in canvas space */ dropLinks(locator: ItemLocator, event: CanvasPointerEvent): void { if (!this.isConnecting) { - console.warn("Attempted to drop links when not connecting to anything.") - return + const mayContinue = this.events.dispatch("before-drop-links", { renderLinks: this.renderLinks, event }) + if (mayContinue === false) return } - const { renderLinks } = this - const mayContinue = this.events.dispatch("before-drop-links", { renderLinks, event }) - if (mayContinue === false) return - try { const { canvasX, canvasY } = event @@ -461,11 +461,11 @@ export class LinkConnector { } } } finally { - this.events.dispatch("after-drop-links", { renderLinks, event }) + this.events.dispatch("after-drop-links", { renderLinks: this.renderLinks, event }) } } - dropOnIoNode(ioNode: SubgraphInputNode | SubgraphOutputNode, event: CanvasPointerEvent) { + dropOnIoNode(ioNode: SubgraphInputNode | SubgraphOutputNode, event: CanvasPointerEvent): void { const { renderLinks, state } = this const { connectingTo } = state const { canvasX, canvasY } = event diff --git a/src/subgraph/SubgraphInputNode.ts b/src/subgraph/SubgraphInputNode.ts index f2cf2dd10..2e28848fc 100644 --- a/src/subgraph/SubgraphInputNode.ts +++ b/src/subgraph/SubgraphInputNode.ts @@ -103,6 +103,18 @@ export class SubgraphInputNode extends SubgraphIONodeBase impleme const inputSlot = target_node.findInputByType(target_slotType) if (!inputSlot) return + if (slot === -1) { + // This indicates a connection is being made from the "Empty" slot. + // We need to create a new, concrete input on the subgraph that matches the target. + const newSubgraphInput = this.subgraph.addInput(inputSlot.slot.name, String(inputSlot.slot.type ?? "")) + const newSlotIndex = this.slots.indexOf(newSubgraphInput) + if (newSlotIndex === -1) { + console.error("Could not find newly created subgraph input slot.") + return + } + slot = newSlotIndex + } + return this.slots[slot].connect(inputSlot.slot, target_node, optsIn?.afterRerouteId) } diff --git a/test/subgraph/SubgraphIO.test.ts b/test/subgraph/SubgraphIO.test.ts index b4bf27916..be6679854 100644 --- a/test/subgraph/SubgraphIO.test.ts +++ b/test/subgraph/SubgraphIO.test.ts @@ -349,3 +349,38 @@ describe("SubgraphIO - Advanced Scenarios", () => { expect(instance3.outputs.length).toBe(2) }) }) + +describe("SubgraphIO - Empty Slot Connection", () => { + subgraphTest("creates new input and connects when dragging from empty slot inside subgraph", ({ subgraphWithNode }) => { + const { subgraph, subgraphNode } = subgraphWithNode + + // Create a node inside the subgraph that will receive the connection + const internalNode = new LGraphNode("Internal Node") + internalNode.addInput("in", "string") + subgraph.add(internalNode) + + // Simulate the connection process from the empty slot to an internal node + // The -1 indicates a connection from the "empty" slot + subgraph.inputNode.connectByType(-1, internalNode, "string") + + // 1. A new input should have been created on the subgraph + expect(subgraph.inputs.length).toBe(2) // Fixture adds one input already + const newInput = subgraph.inputs[1] + expect(newInput.name).toBe("in") + expect(newInput.type).toBe("string") + + // 2. The subgraph node should now have a corresponding real input slot + expect(subgraphNode.inputs.length).toBe(2) + const subgraphInputSlot = subgraphNode.inputs[1] + expect(subgraphInputSlot.name).toBe("in") + + // 3. A link should be established inside the subgraph + expect(internalNode.inputs[0].link).not.toBe(null) + const link = subgraph.links.get(internalNode.inputs[0].link!) + expect(link).toBeDefined() + expect(link.target_id).toBe(internalNode.id) + expect(link.target_slot).toBe(0) + expect(link.origin_id).toBe(subgraph.inputNode.id) + expect(link.origin_slot).toBe(1) // Should be the second slot + }) +})