From d7546e68eff414b9450cb4b72d5e580b6b5d635e Mon Sep 17 00:00:00 2001 From: AustinMroz Date: Mon, 23 Feb 2026 11:54:54 -0800 Subject: [PATCH] Add functionality to quickly disconnect moved input links (#7459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Disconnections are frequently performed by dragging a link from an input slot and dropping it on the canvas, but needing to wait for the searchbox to pop up, and then needing to manually close out of this can make it feel slow. Sometimes, this will even result in users disabling the link release action for more responsive graph building. Instead, this PR introduces new functionality where a link which is moved only a short distance from a node input and dropped will be immediately disconnected instead of performing the default link release action. ![fast-disco_00001](https://github.com/user-attachments/assets/5b0795da-12c0-4347-8ea1-6fc1bcafaae2) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7459-Add-functionality-to-quickly-disconnect-moved-input-links-2c86d73d365081919052f3856db8e672) by [Unito](https://www.unito.io) --- src/lib/litegraph/src/LGraphCanvas.ts | 8 ++++++ src/lib/litegraph/src/canvas/LinkConnector.ts | 24 ++++++++++++++--- .../litegraph/src/canvas/MovingInputLink.ts | 27 ++++++++++++++++++- .../core/canvas/links/linkConnectorAdapter.ts | 13 +++++++-- .../composables/useSlotLinkInteraction.ts | 3 ++- 5 files changed, 68 insertions(+), 7 deletions(-) diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index 08b6ef7c1..b5b4b3423 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -2,6 +2,7 @@ import { toString } from 'es-toolkit/compat' import { toValue } from 'vue' import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants' +import { MovingInputLink } from '@/lib/litegraph/src/canvas/MovingInputLink' import { LitegraphLinkAdapter } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter' import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter' import { getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculations' @@ -5014,6 +5015,7 @@ export class LGraphCanvas implements CustomEventDispatcher } ) } + if (renderLink instanceof MovingInputLink) this.setDirty(false, true) ctx.fillStyle = colour ctx.beginPath() @@ -5908,6 +5910,12 @@ export class LGraphCanvas implements CustomEventDispatcher // Never draw slots when the pointer is down if (!this.pointer.isDown) reroute.drawSlots(ctx) } + + const highlightPos = this._getHighlightPosition() + this.linkConnector.renderLinks + .filter((rl) => rl instanceof MovingInputLink) + .forEach((rl) => rl.drawConnectionCircle(ctx, highlightPos)) + ctx.globalAlpha = 1 } diff --git a/src/lib/litegraph/src/canvas/LinkConnector.ts b/src/lib/litegraph/src/canvas/LinkConnector.ts index 2610011aa..73d57e40d 100644 --- a/src/lib/litegraph/src/canvas/LinkConnector.ts +++ b/src/lib/litegraph/src/canvas/LinkConnector.ts @@ -1,3 +1,5 @@ +import { remove } from 'es-toolkit' + import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' import { LLink } from '@/lib/litegraph/src/LLink' import type { Reroute } from '@/lib/litegraph/src/Reroute' @@ -13,7 +15,8 @@ import type { INodeOutputSlot, ItemLocator, LinkNetwork, - LinkSegment + LinkSegment, + Point } from '@/lib/litegraph/src/interfaces' import { EmptySubgraphInput } from '@/lib/litegraph/src/subgraph/EmptySubgraphInput' import { EmptySubgraphOutput } from '@/lib/litegraph/src/subgraph/EmptySubgraphOutput' @@ -130,7 +133,11 @@ export class LinkConnector { } /** Drag an existing link to a different input. */ - moveInputLink(network: LinkNetwork, input: INodeInputSlot): void { + moveInputLink( + network: LinkNetwork, + input: INodeInputSlot, + opts?: { startPoint?: Point } + ): void { if (this.isConnecting) throw new Error('Already dragging links.') const { state, inputLinks, renderLinks } = this @@ -221,7 +228,13 @@ export class LinkConnector { // Regular node links try { const reroute = network.getReroute(link.parentId) - const renderLink = new MovingInputLink(network, link, reroute) + const renderLink = new MovingInputLink( + network, + link, + reroute, + undefined, + opts?.startPoint + ) const mayContinue = this.events.dispatch( 'before-move-input', @@ -860,6 +873,11 @@ export class LinkConnector { } dropOnNothing(event: CanvasPointerEvent): void { + remove( + this.renderLinks, + (link) => link instanceof MovingInputLink && link.disconnectOnDrop + ).forEach((link) => (link as MovingLinkBase).disconnect()) + if (this.renderLinks.length === 0) return // For external event only. const mayContinue = this.events.dispatch('dropped-on-canvas', event) if (mayContinue === false) return diff --git a/src/lib/litegraph/src/canvas/MovingInputLink.ts b/src/lib/litegraph/src/canvas/MovingInputLink.ts index fb094222c..3e8ddce58 100644 --- a/src/lib/litegraph/src/canvas/MovingInputLink.ts +++ b/src/lib/litegraph/src/canvas/MovingInputLink.ts @@ -1,4 +1,5 @@ import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' import type { LLink } from '@/lib/litegraph/src/LLink' import type { Reroute } from '@/lib/litegraph/src/Reroute' import type { CustomEventTarget } from '@/lib/litegraph/src/infrastructure/CustomEventTarget' @@ -24,12 +25,15 @@ export class MovingInputLink extends MovingLinkBase { readonly fromPos: Point readonly fromDirection: LinkDirection readonly fromSlotIndex: number + disconnectOnDrop: boolean + readonly disconnectOrigin: Point constructor( network: LinkNetwork, link: LLink, fromReroute?: Reroute, - dragDirection: LinkDirection = LinkDirection.CENTER + dragDirection: LinkDirection = LinkDirection.CENTER, + startPoint?: Point ) { super(network, link, 'input', fromReroute, dragDirection) @@ -38,6 +42,8 @@ export class MovingInputLink extends MovingLinkBase { this.fromPos = fromReroute?.pos ?? this.outputPos this.fromDirection = LinkDirection.NONE this.fromSlotIndex = this.outputIndex + this.disconnectOnDrop = true + this.disconnectOrigin = startPoint ?? this.inputPos } canConnectToInput( @@ -130,4 +136,23 @@ export class MovingInputLink extends MovingLinkBase { disconnect(): boolean { return this.inputNode.disconnectInput(this.inputIndex, true) } + + drawConnectionCircle(ctx: CanvasRenderingContext2D, to: Readonly) { + if (!this.disconnectOnDrop) return + + const [originX, originY] = this.disconnectOrigin + const radius = 35 + const distSquared = (originX - to[0]) ** 2 + (originY - to[1]) ** 2 + + ctx.save() + ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR + ctx.lineWidth = 2 + ctx.beginPath() + ctx.moveTo(originX + radius, originY) + ctx.arc(originX, originY, radius, 0, Math.PI * 2) + ctx.stroke() + ctx.restore() + + this.disconnectOnDrop = distSquared < radius ** 2 + } } diff --git a/src/renderer/core/canvas/links/linkConnectorAdapter.ts b/src/renderer/core/canvas/links/linkConnectorAdapter.ts index b7937faf2..1e8e312ef 100644 --- a/src/renderer/core/canvas/links/linkConnectorAdapter.ts +++ b/src/renderer/core/canvas/links/linkConnectorAdapter.ts @@ -1,3 +1,5 @@ +import type { SlotLayout } from '@/renderer/core/layout/types' +import type { Point } from '@/lib/litegraph/src/interfaces' import type { LGraph } from '@/lib/litegraph/src/LGraph' import type { NodeId } from '@/lib/litegraph/src/LGraphNode' import type { RerouteId } from '@/lib/litegraph/src/Reroute' @@ -72,7 +74,11 @@ export class LinkConnectorAdapter { beginFromInput( nodeId: NodeId, inputIndex: number, - opts?: { moveExisting?: boolean; fromRerouteId?: RerouteId } + opts?: { + fromRerouteId?: RerouteId + layout?: SlotLayout + moveExisting?: boolean + } ): void { const node = this.network.getNodeById(nodeId) const input = node?.inputs?.[inputIndex] @@ -81,7 +87,10 @@ export class LinkConnectorAdapter { const fromReroute = this.network.getReroute(opts?.fromRerouteId) if (opts?.moveExisting) { - this.linkConnector.moveInputLink(this.network, input) + const startPoint: Point | undefined = opts.layout + ? [opts.layout.position.x, opts.layout.position.y] + : undefined + this.linkConnector.moveInputLink(this.network, input, { startPoint }) } else { this.linkConnector.dragNewFromInput( this.network, diff --git a/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts b/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts index 6e11902d8..6d4bdf187 100644 --- a/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts +++ b/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts @@ -671,7 +671,8 @@ export function useSlotLinkInteraction({ }) } else { activeAdapter.beginFromInput(localNodeId, index, { - moveExisting: shouldMoveExistingInput + moveExisting: shouldMoveExistingInput, + layout }) }