Add functionality to quickly disconnect moved input links (#7459)

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)
This commit is contained in:
AustinMroz
2026-02-23 11:54:54 -08:00
committed by GitHub
parent 2634acdd8c
commit d7546e68ef
5 changed files with 68 additions and 7 deletions

View File

@@ -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<LGraphCanvasEventMap>
}
)
}
if (renderLink instanceof MovingInputLink) this.setDirty(false, true)
ctx.fillStyle = colour
ctx.beginPath()
@@ -5908,6 +5910,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
// 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
}

View File

@@ -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

View File

@@ -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<Point>) {
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
}
}

View File

@@ -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,

View File

@@ -671,7 +671,8 @@ export function useSlotLinkInteraction({
})
} else {
activeAdapter.beginFromInput(localNodeId, index, {
moveExisting: shouldMoveExistingInput
moveExisting: shouldMoveExistingInput,
layout
})
}