Support dragging from output to output

This commit is contained in:
Benjamin Lu
2025-09-22 11:42:22 -07:00
parent b99d70d0f0
commit 19c538c36c
3 changed files with 129 additions and 9 deletions

View File

@@ -39,8 +39,8 @@ export function evaluateCompatibility(
}
if (source.type === 'output') {
if (candidate.layout.type !== 'input') {
return { allowable: false }
if (candidate.layout.type === 'output') {
return { allowable: Boolean(source.multiOutputDrag), targetNode }
}
const outputSlot = sourceNode.outputs?.[source.slotIndex]

View File

@@ -1,5 +1,8 @@
import { reactive, readonly } from 'vue'
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LinkId } from '@/lib/litegraph/src/LLink'
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
import type { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
@@ -14,6 +17,7 @@ export interface SlotDragSource {
direction: LinkDirection
position: Readonly<Point>
linkId?: number
multiOutputDrag?: boolean
}
export interface SlotDropCandidate {
@@ -21,6 +25,14 @@ export interface SlotDropCandidate {
compatible: boolean
}
// Types shared by multi-output drag logic
export interface MovedOutputNormalLink {
linkId: LinkId
inputNodeId: NodeId
inputSlotIndex: number
parentRerouteId?: RerouteId
}
interface PointerPosition {
client: Point
canvas: Point

View File

@@ -5,10 +5,12 @@ import { onBeforeUnmount } from 'vue'
import { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
import type { LGraph } from '@/lib/litegraph/src/LGraph'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LLink } from '@/lib/litegraph/src/LLink'
import { LLink, type LinkId } from '@/lib/litegraph/src/LLink'
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
import { evaluateCompatibility } from '@/renderer/core/canvas/links/slotLinkCompatibility'
import type { MovedOutputNormalLink } from '@/renderer/core/canvas/links/slotLinkDragState'
import {
type SlotDropCandidate,
useSlotLinkDragState
@@ -113,6 +115,12 @@ export function useSlotLinkInteraction({
const pointerSession = createPointerSession()
const draggingLinkIds = new Set<LinkId>()
const draggingRerouteIds = new Set<RerouteId>()
const movedOutputNormalLinks: MovedOutputNormalLink[] = []
const movedOutputFloatingLinks: LLink[] = []
const resolveLinkOrigin = (
graph: LGraph,
link: LLink | undefined
@@ -258,20 +266,32 @@ export function useSlotLinkInteraction({
const canvas = app.canvas
const graph = canvas?.graph
const source = state.source
if (!canvas || !graph || !source) return
if (!canvas || !graph) return
if (source.linkId != null) {
if (source?.linkId != null) {
const activeLink = graph.getLink(source.linkId)
if (activeLink) {
delete activeLink._dragging
}
if (activeLink) delete activeLink._dragging
}
for (const id of draggingLinkIds) {
const link = graph.getLink(id)
if (link) delete link._dragging
}
for (const id of draggingRerouteIds) {
const reroute = graph.getReroute(id)
if (reroute) reroute._dragging = undefined
}
draggingLinkIds.clear()
draggingRerouteIds.clear()
}
const cleanupInteraction = () => {
clearDraggingFlags()
pointerSession.clear()
endDrag()
movedOutputNormalLinks.length = 0
movedOutputFloatingLinks.length = 0
}
const disconnectSourceLink = (): boolean => {
@@ -318,6 +338,49 @@ export function useSlotLinkInteraction({
const targetNode = graph.getNodeById(Number(slotLayout.nodeId))
if (!sourceNode || !targetNode) return false
// Output ➝ Output (shiftdrag move all links)
if (source.type === 'output' && slotLayout.type === 'output') {
if (!source.multiOutputDrag) return false
const targetOutput = targetNode.outputs?.[slotLayout.index]
if (!targetOutput) return false
// Reconnect all normal links captured at drag start
for (const {
inputNodeId,
inputSlotIndex,
parentRerouteId
} of movedOutputNormalLinks) {
const inputNode = graph.getNodeById(inputNodeId)
const inputSlot = inputNode?.inputs?.[inputSlotIndex]
if (!inputNode || !inputSlot) continue
targetNode.connectSlots(
targetOutput,
inputNode,
inputSlot,
parentRerouteId
)
}
// Move any floating links across to the new output
const sourceNodeAtStart = graph.getNodeById(Number(source.nodeId))
const sourceOutputAtStart = sourceNodeAtStart?.outputs?.[source.slotIndex]
if (sourceOutputAtStart?._floatingLinks?.size) {
for (const floatingLink of movedOutputFloatingLinks) {
sourceOutputAtStart._floatingLinks?.delete(floatingLink)
floatingLink.origin_id = targetNode.id
floatingLink.origin_slot = slotLayout.index
targetOutput._floatingLinks ??= new Set()
targetOutput._floatingLinks.add(floatingLink)
}
}
return true
}
if (source.type === 'output' && slotLayout.type === 'input') {
const outputSlot = sourceNode.outputs?.[source.slotIndex]
const inputSlot = targetNode.inputs?.[slotLayout.index]
@@ -459,6 +522,50 @@ export function useSlotLinkInteraction({
existingLink._dragging = true
}
const outputSlot =
type === 'output' ? resolvedNode?.outputs?.[index] : undefined
const isMultiOutputDrag =
type === 'output' &&
Boolean(
outputSlot &&
(outputSlot.links?.length || outputSlot._floatingLinks?.size)
) &&
event.shiftKey
if (isMultiOutputDrag && outputSlot) {
movedOutputNormalLinks.length = 0
movedOutputFloatingLinks.length = 0
if (outputSlot.links?.length) {
for (const linkId of outputSlot.links) {
const link = graph.getLink(linkId)
if (!link) continue
const firstReroute = LLink.getFirstReroute(graph, link)
if (firstReroute) {
firstReroute._dragging = true
draggingRerouteIds.add(firstReroute.id)
} else {
link._dragging = true
draggingLinkIds.add(link.id)
}
movedOutputNormalLinks.push({
linkId: link.id,
inputNodeId: link.target_id,
inputSlotIndex: link.target_slot,
parentRerouteId: link.parentId
})
}
}
if (outputSlot._floatingLinks?.size) {
for (const floatingLink of outputSlot._floatingLinks) {
movedOutputFloatingLinks.push(floatingLink)
}
}
}
const direction = existingAnchor?.direction ?? baseDirection
const startPosition = existingAnchor?.position ?? {
x: layout.position.x,
@@ -472,7 +579,8 @@ export function useSlotLinkInteraction({
type,
direction,
position: startPosition,
linkId: !shouldBreakExistingLink ? existingLink?.id : undefined
linkId: !shouldBreakExistingLink ? existingLink?.id : undefined,
multiOutputDrag: isMultiOutputDrag
},
event.pointerId
)