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 }) }