mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
Switch to adapter approach
This commit is contained in:
@@ -47,8 +47,11 @@ export class MovingInputLink extends MovingLinkBase {
|
|||||||
return this.node.canConnectTo(inputNode, input, this.outputSlot)
|
return this.node.canConnectTo(inputNode, input, this.outputSlot)
|
||||||
}
|
}
|
||||||
|
|
||||||
canConnectToOutput(): false {
|
canConnectToOutput(
|
||||||
return false
|
outputNode: NodeLike,
|
||||||
|
output: INodeOutputSlot | SubgraphIO
|
||||||
|
): boolean {
|
||||||
|
return outputNode.canConnectTo(this.inputNode, this.inputSlot, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
canConnectToReroute(reroute: Reroute): boolean {
|
canConnectToReroute(reroute: Reroute): boolean {
|
||||||
@@ -73,8 +76,30 @@ export class MovingInputLink extends MovingLinkBase {
|
|||||||
return link
|
return link
|
||||||
}
|
}
|
||||||
|
|
||||||
connectToOutput(): never {
|
connectToOutput(
|
||||||
throw new Error('MovingInputLink cannot connect to an output.')
|
outputNode: LGraphNode,
|
||||||
|
output: INodeOutputSlot,
|
||||||
|
events: CustomEventTarget<LinkConnectorEventMap>
|
||||||
|
): LLink | null | undefined {
|
||||||
|
if (
|
||||||
|
outputNode === this.outputNode &&
|
||||||
|
output === this.outputSlot &&
|
||||||
|
this.inputSlot === this.inputNode.inputs[this.inputIndex]
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const afterRerouteId = this.fromReroute?.id ?? this.link.parentId
|
||||||
|
|
||||||
|
this.inputNode.disconnectInput(this.inputIndex, true)
|
||||||
|
const newLink = outputNode.connectSlots(
|
||||||
|
output,
|
||||||
|
this.inputNode,
|
||||||
|
this.inputSlot,
|
||||||
|
afterRerouteId
|
||||||
|
)
|
||||||
|
if (newLink) events.dispatch('input-moved', this)
|
||||||
|
return newLink
|
||||||
}
|
}
|
||||||
|
|
||||||
connectToSubgraphInput(): void {
|
connectToSubgraphInput(): void {
|
||||||
@@ -123,8 +148,34 @@ export class MovingInputLink extends MovingLinkBase {
|
|||||||
if (newLink) events.dispatch('input-moved', this)
|
if (newLink) events.dispatch('input-moved', this)
|
||||||
}
|
}
|
||||||
|
|
||||||
connectToRerouteOutput(): never {
|
connectToRerouteOutput(
|
||||||
throw new Error('MovingInputLink cannot connect to an output.')
|
reroute: Reroute,
|
||||||
|
outputNode: LGraphNode,
|
||||||
|
output: INodeOutputSlot,
|
||||||
|
events: CustomEventTarget<LinkConnectorEventMap>
|
||||||
|
): void {
|
||||||
|
const { inputNode, inputSlot, fromReroute } = this
|
||||||
|
|
||||||
|
this.inputNode.disconnectInput(this.inputIndex, true)
|
||||||
|
|
||||||
|
const floatingTerminus = reroute?.floating?.slotType === 'output'
|
||||||
|
|
||||||
|
if (fromReroute) {
|
||||||
|
fromReroute.parentId = reroute.id
|
||||||
|
} else {
|
||||||
|
this.link.parentId = reroute.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLink = outputNode.connectSlots(
|
||||||
|
output,
|
||||||
|
inputNode,
|
||||||
|
inputSlot,
|
||||||
|
this.link.parentId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (floatingTerminus) reroute.removeAllFloatingLinks()
|
||||||
|
|
||||||
|
if (newLink) events.dispatch('input-moved', this)
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect(): boolean {
|
disconnect(): boolean {
|
||||||
|
|||||||
187
src/renderer/core/canvas/links/linkConnectorAdapter.ts
Normal file
187
src/renderer/core/canvas/links/linkConnectorAdapter.ts
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import type { LGraph } from '@/lib/litegraph/src/LGraph'
|
||||||
|
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||||
|
import type { Reroute, RerouteId } from '@/lib/litegraph/src/Reroute'
|
||||||
|
import { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
|
||||||
|
import type { RenderLink } from '@/lib/litegraph/src/canvas/RenderLink'
|
||||||
|
import type { ItemLocator } from '@/lib/litegraph/src/interfaces'
|
||||||
|
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||||
|
import type { Point } from '@/renderer/core/layout/types'
|
||||||
|
import { app } from '@/scripts/app'
|
||||||
|
|
||||||
|
// Keep one adapter per graph so rendering and interaction share state.
|
||||||
|
const adapterByGraph = new WeakMap<LGraph, LinkConnectorAdapter>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderer‑agnostic adapter around LiteGraph's LinkConnector.
|
||||||
|
*
|
||||||
|
* - Uses layoutStore for hit‑testing (nodes/reroutes).
|
||||||
|
* - Exposes minimal, imperative APIs to begin link drags and query drop validity.
|
||||||
|
* - Preserves existing Vue composable behavior.
|
||||||
|
*/
|
||||||
|
export class LinkConnectorAdapter {
|
||||||
|
readonly linkConnector: LinkConnector
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
/** Network the links belong to (typically `app.canvas.graph`). */
|
||||||
|
readonly network: LGraph
|
||||||
|
) {
|
||||||
|
// No-op legacy setter to avoid side effects when connectors update
|
||||||
|
const setConnectingLinks = (_value: unknown[]) => {}
|
||||||
|
this.linkConnector = new LinkConnector(
|
||||||
|
setConnectingLinks as (value: any[]) => void
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently rendered/dragged links, typed for consumer use.
|
||||||
|
* Prefer this over accessing `linkConnector.renderLinks` directly.
|
||||||
|
*/
|
||||||
|
get renderLinks(): ReadonlyArray<RenderLink> {
|
||||||
|
return this.linkConnector
|
||||||
|
.renderLinks as unknown as ReadonlyArray<RenderLink>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ItemLocator backed by layoutStore for nodes/reroutes. */
|
||||||
|
get locator(): ItemLocator {
|
||||||
|
const graph = this.network
|
||||||
|
|
||||||
|
return {
|
||||||
|
getNodeOnPos: (x: number, y: number): LGraphNode | null => {
|
||||||
|
const id = layoutStore.queryNodeAtPoint(point(x, y))
|
||||||
|
if (id == null) return null
|
||||||
|
return graph.getNodeById(id) as LGraphNode | null
|
||||||
|
},
|
||||||
|
getRerouteOnPos: (x: number, y: number): Reroute | undefined => {
|
||||||
|
const r = layoutStore.queryRerouteAtPoint(point(x, y))
|
||||||
|
if (!r) return undefined
|
||||||
|
return graph.getReroute(r.id)
|
||||||
|
}
|
||||||
|
// getIoNodeOnPos: not required yet; can be added when UI exposes IO nodes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag helpers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin dragging from an output slot.
|
||||||
|
* @param nodeId Output node id
|
||||||
|
* @param outputIndex Output slot index
|
||||||
|
* @param opts Optional: moveExisting (shift), fromRerouteId
|
||||||
|
*/
|
||||||
|
beginFromOutput(
|
||||||
|
nodeId: NodeId,
|
||||||
|
outputIndex: number,
|
||||||
|
opts?: { moveExisting?: boolean; fromRerouteId?: RerouteId }
|
||||||
|
): void {
|
||||||
|
const node = this.network.getNodeById(nodeId)
|
||||||
|
const output = node?.outputs?.[outputIndex]
|
||||||
|
if (!node || !output) return
|
||||||
|
|
||||||
|
const fromReroute =
|
||||||
|
opts?.fromRerouteId != null
|
||||||
|
? this.network.getReroute(opts.fromRerouteId)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (opts?.moveExisting) {
|
||||||
|
console.log("I'm fully aware")
|
||||||
|
this.linkConnector.moveOutputLink(this.network, output)
|
||||||
|
} else {
|
||||||
|
this.linkConnector.dragNewFromOutput(
|
||||||
|
this.network,
|
||||||
|
node,
|
||||||
|
output,
|
||||||
|
fromReroute
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin dragging from an input slot.
|
||||||
|
* @param nodeId Input node id
|
||||||
|
* @param inputIndex Input slot index
|
||||||
|
* @param opts Optional: moveExisting (when a link/floating exists), fromRerouteId
|
||||||
|
*/
|
||||||
|
beginFromInput(
|
||||||
|
nodeId: NodeId,
|
||||||
|
inputIndex: number,
|
||||||
|
opts?: { moveExisting?: boolean; fromRerouteId?: RerouteId }
|
||||||
|
): void {
|
||||||
|
const node = this.network.getNodeById(nodeId)
|
||||||
|
const input = node?.inputs?.[inputIndex]
|
||||||
|
if (!node || !input) return
|
||||||
|
|
||||||
|
const fromReroute =
|
||||||
|
opts?.fromRerouteId != null
|
||||||
|
? this.network.getReroute(opts.fromRerouteId)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (opts?.moveExisting) {
|
||||||
|
this.linkConnector.moveInputLink(this.network, input)
|
||||||
|
} else {
|
||||||
|
this.linkConnector.dragNewFromInput(
|
||||||
|
this.network,
|
||||||
|
node,
|
||||||
|
input,
|
||||||
|
fromReroute
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation helpers
|
||||||
|
|
||||||
|
isNodeValidDrop(nodeId: NodeId): boolean {
|
||||||
|
const node = this.network.getNodeById(nodeId)
|
||||||
|
if (!node) return false
|
||||||
|
return this.linkConnector.isNodeValidDrop(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
isInputValidDrop(nodeId: NodeId, inputIndex: number): boolean {
|
||||||
|
const node = this.network.getNodeById(nodeId)
|
||||||
|
const input = node?.inputs?.[inputIndex]
|
||||||
|
if (!node || !input) return false
|
||||||
|
return this.linkConnector.isInputValidDrop(node, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
isOutputValidDrop(nodeId: NodeId, outputIndex: number): boolean {
|
||||||
|
const node = this.network.getNodeById(nodeId)
|
||||||
|
const output = node?.outputs?.[outputIndex]
|
||||||
|
if (!node || !output) return false
|
||||||
|
return (this.linkConnector.renderLinks as any[]).some(
|
||||||
|
(link) => link?.canConnectToOutput?.(node, output) === true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
isRerouteValidDrop(rerouteId: RerouteId): boolean {
|
||||||
|
const reroute = this.network.getReroute(rerouteId)
|
||||||
|
if (!reroute) return false
|
||||||
|
return this.linkConnector.isRerouteValidDrop(reroute)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop/cancel helpers for future flows
|
||||||
|
|
||||||
|
/** Disconnects moving links (drop on canvas/no target). */
|
||||||
|
disconnectMovingLinks(): void {
|
||||||
|
this.linkConnector.disconnectLinks()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resets connector state and clears any temporary flags. */
|
||||||
|
reset(): void {
|
||||||
|
this.linkConnector.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience creator using the current app canvas graph. */
|
||||||
|
export function createLinkConnectorAdapter(): LinkConnectorAdapter | null {
|
||||||
|
const graph = app.canvas?.graph as LGraph | undefined
|
||||||
|
if (!graph) return null
|
||||||
|
let adapter = adapterByGraph.get(graph)
|
||||||
|
if (!adapter) {
|
||||||
|
adapter = new LinkConnectorAdapter(graph)
|
||||||
|
adapterByGraph.set(graph, adapter)
|
||||||
|
}
|
||||||
|
return adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
function point(x: number, y: number): Point {
|
||||||
|
return { x, y }
|
||||||
|
}
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import { getActivePinia } from 'pinia'
|
|
||||||
|
|
||||||
import type {
|
|
||||||
INodeInputSlot,
|
|
||||||
INodeOutputSlot
|
|
||||||
} from '@/lib/litegraph/src/interfaces'
|
|
||||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
|
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
|
||||||
import type {
|
|
||||||
SlotDragSource,
|
|
||||||
SlotDropCandidate
|
|
||||||
} from '@/renderer/core/canvas/links/slotLinkDragState'
|
|
||||||
import { app } from '@/scripts/app'
|
|
||||||
|
|
||||||
interface CompatibilityResult {
|
|
||||||
allowable: boolean
|
|
||||||
targetNode?: LGraphNode
|
|
||||||
targetSlot?: INodeInputSlot | INodeOutputSlot
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveNode(nodeId: NodeId) {
|
|
||||||
const pinia = getActivePinia()
|
|
||||||
const canvasStore = pinia ? useCanvasStore() : null
|
|
||||||
const graph = canvasStore?.canvas?.graph ?? app.canvas?.graph
|
|
||||||
if (!graph) return null
|
|
||||||
const id = typeof nodeId === 'string' ? Number(nodeId) : nodeId
|
|
||||||
if (Number.isNaN(id)) return null
|
|
||||||
return graph.getNodeById(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function evaluateCompatibility(
|
|
||||||
source: SlotDragSource,
|
|
||||||
candidate: SlotDropCandidate
|
|
||||||
): CompatibilityResult {
|
|
||||||
const sourceNode = resolveNode(source.nodeId)
|
|
||||||
const targetNode = resolveNode(candidate.layout.nodeId)
|
|
||||||
if (!sourceNode || !targetNode) {
|
|
||||||
return { allowable: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source.type === 'output') {
|
|
||||||
if (candidate.layout.type === 'output') {
|
|
||||||
return { allowable: Boolean(source.multiOutputDrag), targetNode }
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputSlot = sourceNode.outputs?.[source.slotIndex]
|
|
||||||
const inputSlot = targetNode.inputs?.[candidate.layout.index]
|
|
||||||
if (!outputSlot || !inputSlot) {
|
|
||||||
return { allowable: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowable = sourceNode.canConnectTo(targetNode, inputSlot, outputSlot)
|
|
||||||
return { allowable, targetNode, targetSlot: inputSlot }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source.type === 'input') {
|
|
||||||
if (candidate.layout.type === 'output') {
|
|
||||||
const inputSlot = sourceNode.inputs?.[source.slotIndex]
|
|
||||||
const outputSlot = targetNode.outputs?.[candidate.layout.index]
|
|
||||||
if (!inputSlot || !outputSlot) {
|
|
||||||
return { allowable: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowable = targetNode.canConnectTo(
|
|
||||||
sourceNode,
|
|
||||||
inputSlot,
|
|
||||||
outputSlot
|
|
||||||
)
|
|
||||||
return { allowable, targetNode, targetSlot: outputSlot }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidate.layout.type === 'input') {
|
|
||||||
const graph = sourceNode.graph
|
|
||||||
if (!graph) {
|
|
||||||
return { allowable: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkId = source.linkId
|
|
||||||
if (linkId == null) {
|
|
||||||
return { allowable: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const link = graph.getLink(linkId)
|
|
||||||
if (!link) {
|
|
||||||
return { allowable: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputNode = resolveNode(link.origin_id)
|
|
||||||
const outputSlot = outputNode?.outputs?.[link.origin_slot]
|
|
||||||
const inputSlotTarget = targetNode.inputs?.[candidate.layout.index]
|
|
||||||
if (!outputNode || !outputSlot || !inputSlotTarget) {
|
|
||||||
return { allowable: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowable = outputNode.canConnectTo(
|
|
||||||
targetNode,
|
|
||||||
inputSlotTarget,
|
|
||||||
outputSlot
|
|
||||||
)
|
|
||||||
return { allowable, targetNode, targetSlot: inputSlotTarget }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { allowable: false }
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import { reactive, readonly } from 'vue'
|
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 type { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
|
||||||
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
||||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||||
@@ -17,7 +14,7 @@ export interface SlotDragSource {
|
|||||||
direction: LinkDirection
|
direction: LinkDirection
|
||||||
position: Readonly<Point>
|
position: Readonly<Point>
|
||||||
linkId?: number
|
linkId?: number
|
||||||
multiOutputDrag?: boolean
|
movingExistingOutput?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SlotDropCandidate {
|
export interface SlotDropCandidate {
|
||||||
@@ -25,14 +22,6 @@ export interface SlotDropCandidate {
|
|||||||
compatible: boolean
|
compatible: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Types shared by multi-output drag logic
|
|
||||||
export interface MovedOutputNormalLink {
|
|
||||||
linkId: LinkId
|
|
||||||
inputNodeId: NodeId
|
|
||||||
inputSlotIndex: number
|
|
||||||
parentRerouteId?: RerouteId
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PointerPosition {
|
interface PointerPosition {
|
||||||
client: Point
|
client: Point
|
||||||
canvas: Point
|
canvas: Point
|
||||||
|
|||||||
@@ -6,11 +6,14 @@ import type {
|
|||||||
} from '@/lib/litegraph/src/interfaces'
|
} from '@/lib/litegraph/src/interfaces'
|
||||||
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
|
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
|
||||||
import { resolveConnectingLinkColor } from '@/lib/litegraph/src/utils/linkColors'
|
import { resolveConnectingLinkColor } from '@/lib/litegraph/src/utils/linkColors'
|
||||||
|
import { createLinkConnectorAdapter } from '@/renderer/core/canvas/links/linkConnectorAdapter'
|
||||||
import {
|
import {
|
||||||
type SlotDragSource,
|
type SlotDragSource,
|
||||||
useSlotLinkDragState
|
useSlotLinkDragState
|
||||||
} from '@/renderer/core/canvas/links/slotLinkDragState'
|
} from '@/renderer/core/canvas/links/slotLinkDragState'
|
||||||
import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
|
import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
|
||||||
|
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
||||||
|
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||||
|
|
||||||
function buildContext(canvas: LGraphCanvas): LinkRenderContext {
|
function buildContext(canvas: LGraphCanvas): LinkRenderContext {
|
||||||
return {
|
return {
|
||||||
@@ -42,24 +45,59 @@ export function attachSlotLinkPreviewRenderer(canvas: LGraphCanvas) {
|
|||||||
if (!state.active || !state.source) return
|
if (!state.active || !state.source) return
|
||||||
|
|
||||||
const { pointer, source } = state
|
const { pointer, source } = state
|
||||||
const start = source.position
|
|
||||||
const sourceSlot = resolveSourceSlot(canvas, source)
|
|
||||||
|
|
||||||
const linkRenderer = canvas.linkRenderer
|
const linkRenderer = canvas.linkRenderer
|
||||||
if (!linkRenderer) return
|
if (!linkRenderer) return
|
||||||
|
|
||||||
const context = buildContext(canvas)
|
const context = buildContext(canvas)
|
||||||
|
|
||||||
|
// Prefer LinkConnector render links when available (multi-link drags, move-existing, reroutes)
|
||||||
|
const adapter = createLinkConnectorAdapter()
|
||||||
|
const renderLinks = adapter?.renderLinks
|
||||||
|
if (adapter && renderLinks && renderLinks.length > 0) {
|
||||||
|
const to: ReadOnlyPoint = [pointer.canvas.x, pointer.canvas.y]
|
||||||
|
ctx.save()
|
||||||
|
for (const link of renderLinks) {
|
||||||
|
// Prefer Vue slot layout position for accuracy in Vue Nodes mode
|
||||||
|
let fromPoint = link.fromPos
|
||||||
|
const nodeId = (link.node as any)?.id
|
||||||
|
if (typeof nodeId === 'number') {
|
||||||
|
const isInputFrom = link.toType === 'output'
|
||||||
|
const key = getSlotKey(
|
||||||
|
String(nodeId),
|
||||||
|
link.fromSlotIndex,
|
||||||
|
isInputFrom
|
||||||
|
)
|
||||||
|
const layout = layoutStore.getSlotLayout(key)
|
||||||
|
if (layout) fromPoint = [layout.position.x, layout.position.y]
|
||||||
|
}
|
||||||
|
|
||||||
|
const colour = resolveConnectingLinkColor(link.fromSlot.type)
|
||||||
|
const startDir = link.fromDirection ?? LinkDirection.RIGHT
|
||||||
|
const endDir = link.dragDirection ?? LinkDirection.CENTER
|
||||||
|
|
||||||
|
linkRenderer.renderDraggingLink(
|
||||||
|
ctx,
|
||||||
|
fromPoint,
|
||||||
|
to,
|
||||||
|
colour,
|
||||||
|
startDir,
|
||||||
|
endDir,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ctx.restore()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to legacy single-link preview based on composable state
|
||||||
|
const start = source.position
|
||||||
|
const sourceSlot = resolveSourceSlot(canvas, source)
|
||||||
const from: ReadOnlyPoint = [start.x, start.y]
|
const from: ReadOnlyPoint = [start.x, start.y]
|
||||||
const to: ReadOnlyPoint = [pointer.canvas.x, pointer.canvas.y]
|
const to: ReadOnlyPoint = [pointer.canvas.x, pointer.canvas.y]
|
||||||
|
|
||||||
const startDir = source.direction ?? LinkDirection.RIGHT
|
const startDir = source.direction ?? LinkDirection.RIGHT
|
||||||
const endDir = LinkDirection.CENTER
|
const endDir = LinkDirection.CENTER
|
||||||
|
|
||||||
const colour = resolveConnectingLinkColor(sourceSlot?.type)
|
const colour = resolveConnectingLinkColor(sourceSlot?.type)
|
||||||
|
|
||||||
ctx.save()
|
ctx.save()
|
||||||
|
|
||||||
linkRenderer.renderDraggingLink(
|
linkRenderer.renderDraggingLink(
|
||||||
ctx,
|
ctx,
|
||||||
from,
|
from,
|
||||||
@@ -69,7 +107,6 @@ export function attachSlotLinkPreviewRenderer(canvas: LGraphCanvas) {
|
|||||||
endDir,
|
endDir,
|
||||||
context
|
context
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,19 +5,23 @@ import { onBeforeUnmount } from 'vue'
|
|||||||
import { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
|
import { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
|
||||||
import type { LGraph } from '@/lib/litegraph/src/LGraph'
|
import type { LGraph } from '@/lib/litegraph/src/LGraph'
|
||||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||||
import { LLink, type LinkId } from '@/lib/litegraph/src/LLink'
|
import { LLink } from '@/lib/litegraph/src/LLink'
|
||||||
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
|
import type { Reroute } from '@/lib/litegraph/src/Reroute'
|
||||||
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
|
import type { RenderLink } from '@/lib/litegraph/src/canvas/RenderLink'
|
||||||
|
import type {
|
||||||
|
INodeInputSlot,
|
||||||
|
INodeOutputSlot
|
||||||
|
} from '@/lib/litegraph/src/interfaces'
|
||||||
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
|
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
|
||||||
import { evaluateCompatibility } from '@/renderer/core/canvas/links/slotLinkCompatibility'
|
import { createLinkConnectorAdapter } from '@/renderer/core/canvas/links/linkConnectorAdapter'
|
||||||
import type { MovedOutputNormalLink } from '@/renderer/core/canvas/links/slotLinkDragState'
|
import type { LinkConnectorAdapter } from '@/renderer/core/canvas/links/linkConnectorAdapter'
|
||||||
import {
|
import {
|
||||||
type SlotDropCandidate,
|
type SlotDropCandidate,
|
||||||
useSlotLinkDragState
|
useSlotLinkDragState
|
||||||
} from '@/renderer/core/canvas/links/slotLinkDragState'
|
} from '@/renderer/core/canvas/links/slotLinkDragState'
|
||||||
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
||||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||||
import type { Point, SlotLayout } from '@/renderer/core/layout/types'
|
import type { Point } from '@/renderer/core/layout/types'
|
||||||
import { toPoint } from '@/renderer/core/layout/utils/geometry'
|
import { toPoint } from '@/renderer/core/layout/utils/geometry'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
|
|
||||||
@@ -102,10 +106,22 @@ export function useSlotLinkInteraction({
|
|||||||
const candidate: SlotDropCandidate = { layout, compatible: false }
|
const candidate: SlotDropCandidate = { layout, compatible: false }
|
||||||
|
|
||||||
if (state.source) {
|
if (state.source) {
|
||||||
candidate.compatible = evaluateCompatibility(
|
const canvas = app.canvas
|
||||||
state.source,
|
const graph = canvas?.graph
|
||||||
candidate
|
adapter ??= createLinkConnectorAdapter()
|
||||||
).allowable
|
if (graph && adapter) {
|
||||||
|
if (layout.type === 'input') {
|
||||||
|
candidate.compatible = adapter.isInputValidDrop(
|
||||||
|
layout.nodeId,
|
||||||
|
layout.index
|
||||||
|
)
|
||||||
|
} else if (layout.type === 'output') {
|
||||||
|
candidate.compatible = adapter.isOutputValidDrop(
|
||||||
|
layout.nodeId,
|
||||||
|
layout.index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return candidate
|
return candidate
|
||||||
@@ -114,12 +130,73 @@ export function useSlotLinkInteraction({
|
|||||||
const conversion = useSharedCanvasPositionConversion()
|
const conversion = useSharedCanvasPositionConversion()
|
||||||
|
|
||||||
const pointerSession = createPointerSession()
|
const pointerSession = createPointerSession()
|
||||||
|
let adapter: LinkConnectorAdapter | null = null
|
||||||
|
|
||||||
const draggingLinkIds = new Set<LinkId>()
|
function hasCanConnectToReroute(
|
||||||
const draggingRerouteIds = new Set<RerouteId>()
|
link: RenderLink
|
||||||
|
): link is RenderLink & { canConnectToReroute: (r: Reroute) => boolean } {
|
||||||
|
return 'canConnectToReroute' in link
|
||||||
|
}
|
||||||
|
|
||||||
const movedOutputNormalLinks: MovedOutputNormalLink[] = []
|
type InputConnectableLink = RenderLink & {
|
||||||
const movedOutputFloatingLinks: LLink[] = []
|
toType: 'input'
|
||||||
|
canConnectToInput: (node: LGraphNode, input: INodeInputSlot) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutputConnectableLink = RenderLink & {
|
||||||
|
toType: 'output'
|
||||||
|
canConnectToOutput: (node: LGraphNode, output: INodeOutputSlot) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInputConnectableLink(
|
||||||
|
link: RenderLink
|
||||||
|
): link is InputConnectableLink {
|
||||||
|
return (
|
||||||
|
link.toType === 'input' &&
|
||||||
|
typeof (link as { canConnectToInput?: unknown }).canConnectToInput ===
|
||||||
|
'function'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOutputConnectableLink(
|
||||||
|
link: RenderLink
|
||||||
|
): link is OutputConnectableLink {
|
||||||
|
return (
|
||||||
|
link.toType === 'output' &&
|
||||||
|
typeof (link as { canConnectToOutput?: unknown }).canConnectToOutput ===
|
||||||
|
'function'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectLinksToInput(
|
||||||
|
links: ReadonlyArray<RenderLink>,
|
||||||
|
node: LGraphNode,
|
||||||
|
inputSlot: INodeInputSlot
|
||||||
|
): boolean {
|
||||||
|
let didConnect = false
|
||||||
|
for (const link of links) {
|
||||||
|
if (!isInputConnectableLink(link)) continue
|
||||||
|
if (!link.canConnectToInput(node, inputSlot)) continue
|
||||||
|
link.connectToInput(node, inputSlot, adapter?.linkConnector.events)
|
||||||
|
didConnect = true
|
||||||
|
}
|
||||||
|
return didConnect
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectLinksToOutput(
|
||||||
|
links: ReadonlyArray<RenderLink>,
|
||||||
|
node: LGraphNode,
|
||||||
|
outputSlot: INodeOutputSlot
|
||||||
|
): boolean {
|
||||||
|
let didConnect = false
|
||||||
|
for (const link of links) {
|
||||||
|
if (!isOutputConnectableLink(link)) continue
|
||||||
|
if (!link.canConnectToOutput(node, outputSlot)) continue
|
||||||
|
link.connectToOutput(node, outputSlot, adapter?.linkConnector.events)
|
||||||
|
didConnect = true
|
||||||
|
}
|
||||||
|
return didConnect
|
||||||
|
}
|
||||||
|
|
||||||
const resolveLinkOrigin = (
|
const resolveLinkOrigin = (
|
||||||
graph: LGraph,
|
graph: LGraph,
|
||||||
@@ -212,103 +289,10 @@ export function useSlotLinkInteraction({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveInputDragOrigin = (
|
|
||||||
graph: LGraph,
|
|
||||||
sourceNode: LGraphNode,
|
|
||||||
slotIndex: number,
|
|
||||||
linkId: number | undefined
|
|
||||||
) => {
|
|
||||||
const inputSlot = sourceNode.inputs?.[slotIndex]
|
|
||||||
if (!inputSlot) return null
|
|
||||||
|
|
||||||
const mapLinkToOrigin = (link: LLink | undefined | null) => {
|
|
||||||
if (!link) return null
|
|
||||||
|
|
||||||
const originNode = graph.getNodeById(link.origin_id)
|
|
||||||
const originSlot = originNode?.outputs?.[link.origin_slot]
|
|
||||||
if (!originNode || !originSlot) return null
|
|
||||||
|
|
||||||
return {
|
|
||||||
node: originNode,
|
|
||||||
slot: originSlot,
|
|
||||||
slotIndex: link.origin_slot,
|
|
||||||
afterRerouteId: link.parentId ?? null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (linkId != null) {
|
|
||||||
const fromStoredLink = mapLinkToOrigin(graph.getLink(linkId))
|
|
||||||
if (fromStoredLink) return fromStoredLink
|
|
||||||
}
|
|
||||||
|
|
||||||
const fromDirectLink = mapLinkToOrigin(graph.getLink(inputSlot.link))
|
|
||||||
if (fromDirectLink) return fromDirectLink
|
|
||||||
|
|
||||||
const floatingLinkIterator = inputSlot._floatingLinks?.values()
|
|
||||||
const floatingLink = floatingLinkIterator
|
|
||||||
? floatingLinkIterator.next().value
|
|
||||||
: undefined
|
|
||||||
if (!floatingLink || floatingLink.isFloating) return null
|
|
||||||
|
|
||||||
const originNode = graph.getNodeById(floatingLink.origin_id)
|
|
||||||
const originSlot = originNode?.outputs?.[floatingLink.origin_slot]
|
|
||||||
if (!originNode || !originSlot) return null
|
|
||||||
|
|
||||||
return {
|
|
||||||
node: originNode,
|
|
||||||
slot: originSlot,
|
|
||||||
slotIndex: floatingLink.origin_slot,
|
|
||||||
afterRerouteId: floatingLink.parentId ?? null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearDraggingFlags = () => {
|
|
||||||
const canvas = app.canvas
|
|
||||||
const graph = canvas?.graph
|
|
||||||
const source = state.source
|
|
||||||
if (!canvas || !graph) return
|
|
||||||
|
|
||||||
if (source?.linkId != null) {
|
|
||||||
const activeLink = graph.getLink(source.linkId)
|
|
||||||
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 = () => {
|
const cleanupInteraction = () => {
|
||||||
clearDraggingFlags()
|
adapter?.reset()
|
||||||
pointerSession.clear()
|
pointerSession.clear()
|
||||||
endDrag()
|
endDrag()
|
||||||
movedOutputNormalLinks.length = 0
|
|
||||||
movedOutputFloatingLinks.length = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const disconnectSourceLink = (): boolean => {
|
|
||||||
const canvas = app.canvas
|
|
||||||
const graph = canvas?.graph
|
|
||||||
const source = state.source
|
|
||||||
if (!canvas || !graph || !source) return false
|
|
||||||
|
|
||||||
const sourceNode = graph.getNodeById(Number(source.nodeId))
|
|
||||||
if (!sourceNode) return false
|
|
||||||
|
|
||||||
graph.beforeChange()
|
|
||||||
if (source.type === 'input') {
|
|
||||||
return sourceNode.disconnectInput(source.slotIndex, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sourceNode.disconnectOutput(source.slotIndex)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePointerState = (event: PointerEvent) => {
|
const updatePointerState = (event: PointerEvent) => {
|
||||||
@@ -328,121 +312,6 @@ export function useSlotLinkInteraction({
|
|||||||
app.canvas?.setDirty(true)
|
app.canvas?.setDirty(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectSlots = (slotLayout: SlotLayout): boolean => {
|
|
||||||
const canvas = app.canvas
|
|
||||||
const graph = canvas?.graph
|
|
||||||
const source = state.source
|
|
||||||
if (!canvas || !graph || !source) return false
|
|
||||||
|
|
||||||
const sourceNode = graph.getNodeById(Number(source.nodeId))
|
|
||||||
const targetNode = graph.getNodeById(Number(slotLayout.nodeId))
|
|
||||||
if (!sourceNode || !targetNode) return false
|
|
||||||
|
|
||||||
// Output ➝ Output (shift‑drag 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]
|
|
||||||
if (!outputSlot || !inputSlot) return false
|
|
||||||
const existingLink = graph.getLink(inputSlot.link)
|
|
||||||
const afterRerouteId = existingLink?.parentId ?? undefined
|
|
||||||
sourceNode.connectSlots(outputSlot, targetNode, inputSlot, afterRerouteId)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source.type === 'input') {
|
|
||||||
const inputSlot = sourceNode.inputs?.[source.slotIndex]
|
|
||||||
if (!inputSlot) return false
|
|
||||||
|
|
||||||
const origin = resolveInputDragOrigin(
|
|
||||||
graph,
|
|
||||||
sourceNode,
|
|
||||||
source.slotIndex,
|
|
||||||
source.linkId
|
|
||||||
)
|
|
||||||
|
|
||||||
if (slotLayout.type === 'output') {
|
|
||||||
const outputSlot = targetNode.outputs?.[slotLayout.index]
|
|
||||||
if (!outputSlot) return false
|
|
||||||
|
|
||||||
const afterRerouteId =
|
|
||||||
origin &&
|
|
||||||
String(origin.node.id) === slotLayout.nodeId &&
|
|
||||||
origin.slotIndex === slotLayout.index
|
|
||||||
? origin.afterRerouteId ?? undefined
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
targetNode.connectSlots(
|
|
||||||
outputSlot,
|
|
||||||
sourceNode,
|
|
||||||
inputSlot,
|
|
||||||
afterRerouteId
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slotLayout.type === 'input') {
|
|
||||||
if (!origin) return false
|
|
||||||
|
|
||||||
const outputNode = origin.node
|
|
||||||
const outputSlot = origin.slot
|
|
||||||
const newInputSlot = targetNode.inputs?.[slotLayout.index]
|
|
||||||
if (!outputSlot || !newInputSlot) return false
|
|
||||||
sourceNode.disconnectInput(source.slotIndex, true)
|
|
||||||
outputNode.connectSlots(
|
|
||||||
outputSlot,
|
|
||||||
targetNode,
|
|
||||||
newInputSlot,
|
|
||||||
origin.afterRerouteId ?? undefined
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const finishInteraction = (event: PointerEvent) => {
|
const finishInteraction = (event: PointerEvent) => {
|
||||||
if (!pointerSession.matches(event)) return
|
if (!pointerSession.matches(event)) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -451,11 +320,91 @@ export function useSlotLinkInteraction({
|
|||||||
const candidate = candidateFromTarget(event.target)
|
const candidate = candidateFromTarget(event.target)
|
||||||
let connected = false
|
let connected = false
|
||||||
if (candidate?.compatible) {
|
if (candidate?.compatible) {
|
||||||
connected = connectSlots(candidate.layout)
|
const canvas = app.canvas
|
||||||
|
const graph = canvas?.graph
|
||||||
|
if (graph) {
|
||||||
|
adapter ??= createLinkConnectorAdapter()
|
||||||
|
const targetNode = graph.getNodeById(Number(candidate.layout.nodeId))
|
||||||
|
if (adapter && targetNode) {
|
||||||
|
if (candidate.layout.type === 'input') {
|
||||||
|
const inputSlot = targetNode.inputs?.[candidate.layout.index]
|
||||||
|
if (
|
||||||
|
inputSlot &&
|
||||||
|
connectLinksToInput(adapter.renderLinks, targetNode, inputSlot)
|
||||||
|
)
|
||||||
|
connected = true
|
||||||
|
} else if (candidate.layout.type === 'output') {
|
||||||
|
const outputSlot = targetNode.outputs?.[candidate.layout.index]
|
||||||
|
if (
|
||||||
|
outputSlot &&
|
||||||
|
connectLinksToOutput(
|
||||||
|
adapter.renderLinks,
|
||||||
|
targetNode,
|
||||||
|
outputSlot
|
||||||
|
)
|
||||||
|
)
|
||||||
|
connected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try reroute drop when no DOM slot was detected
|
||||||
|
if (!connected) {
|
||||||
|
const rerouteLayout = layoutStore.queryRerouteAtPoint({
|
||||||
|
x: state.pointer.canvas.x,
|
||||||
|
y: state.pointer.canvas.y
|
||||||
|
})
|
||||||
|
const graph = app.canvas?.graph
|
||||||
|
adapter ??= createLinkConnectorAdapter()
|
||||||
|
if (rerouteLayout && graph && adapter) {
|
||||||
|
const reroute = graph.getReroute(rerouteLayout.id)
|
||||||
|
if (reroute && adapter.isRerouteValidDrop(reroute.id)) {
|
||||||
|
const results = reroute.findTargetInputs() ?? []
|
||||||
|
const maybeReroutes = reroute.getReroutes()
|
||||||
|
if (results.length && maybeReroutes !== null) {
|
||||||
|
const originalReroutes = maybeReroutes.slice(0, -1).reverse()
|
||||||
|
for (const link of adapter.renderLinks) {
|
||||||
|
if (!isInputConnectableLink(link)) continue
|
||||||
|
for (const result of results) {
|
||||||
|
link.connectToRerouteInput(
|
||||||
|
reroute,
|
||||||
|
result,
|
||||||
|
adapter.linkConnector.events,
|
||||||
|
originalReroutes
|
||||||
|
)
|
||||||
|
connected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceOutput = reroute.findSourceOutput()
|
||||||
|
if (sourceOutput) {
|
||||||
|
const { node, output } = sourceOutput
|
||||||
|
for (const link of adapter.renderLinks) {
|
||||||
|
if (!isOutputConnectableLink(link)) continue
|
||||||
|
if (
|
||||||
|
hasCanConnectToReroute(link) &&
|
||||||
|
!link.canConnectToReroute(reroute)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
link.connectToRerouteOutput(
|
||||||
|
reroute,
|
||||||
|
node,
|
||||||
|
output,
|
||||||
|
adapter.linkConnector.events
|
||||||
|
)
|
||||||
|
connected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop on canvas: disconnect moving input link(s)
|
||||||
if (!connected && !candidate && state.source.type === 'input') {
|
if (!connected && !candidate && state.source.type === 'input') {
|
||||||
disconnectSourceLink()
|
adapter ??= createLinkConnectorAdapter()
|
||||||
|
adapter?.disconnectMovingLinks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,51 +467,29 @@ export function useSlotLinkInteraction({
|
|||||||
? resolveExistingInputLinkAnchor(graph, inputSlot)
|
? resolveExistingInputLinkAnchor(graph, inputSlot)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
if (!shouldBreakExistingLink && existingLink) {
|
|
||||||
existingLink._dragging = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputSlot =
|
const outputSlot =
|
||||||
type === 'output' ? resolvedNode?.outputs?.[index] : undefined
|
type === 'output' ? resolvedNode?.outputs?.[index] : undefined
|
||||||
const isMultiOutputDrag =
|
const hasExistingOutputLink = Boolean(
|
||||||
type === 'output' &&
|
outputSlot &&
|
||||||
Boolean(
|
((outputSlot.links?.length ?? 0) > 0 ||
|
||||||
outputSlot &&
|
(outputSlot._floatingLinks?.size ?? 0) > 0)
|
||||||
(outputSlot.links?.length || outputSlot._floatingLinks?.size)
|
)
|
||||||
) &&
|
const shouldMoveExistingOutput =
|
||||||
event.shiftKey
|
type === 'output' && event.shiftKey && hasExistingOutputLink
|
||||||
|
|
||||||
if (isMultiOutputDrag && outputSlot) {
|
adapter ??= createLinkConnectorAdapter()
|
||||||
movedOutputNormalLinks.length = 0
|
if (adapter) {
|
||||||
movedOutputFloatingLinks.length = 0
|
if (type === 'output') {
|
||||||
|
adapter.beginFromOutput(Number(nodeId), index, {
|
||||||
if (outputSlot.links?.length) {
|
moveExisting: shouldMoveExistingOutput
|
||||||
for (const linkId of outputSlot.links) {
|
})
|
||||||
const link = graph.getLink(linkId)
|
} else {
|
||||||
if (!link) continue
|
const moveExisting = !!(
|
||||||
|
inputSlot &&
|
||||||
const firstReroute = LLink.getFirstReroute(graph, link)
|
!shouldBreakExistingLink &&
|
||||||
if (firstReroute) {
|
(inputSlot.link != null || inputSlot._floatingLinks?.size)
|
||||||
firstReroute._dragging = true
|
)
|
||||||
draggingRerouteIds.add(firstReroute.id)
|
adapter.beginFromInput(Number(nodeId), index, { moveExisting })
|
||||||
} 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,7 +507,7 @@ export function useSlotLinkInteraction({
|
|||||||
direction,
|
direction,
|
||||||
position: startPosition,
|
position: startPosition,
|
||||||
linkId: !shouldBreakExistingLink ? existingLink?.id : undefined,
|
linkId: !shouldBreakExistingLink ? existingLink?.id : undefined,
|
||||||
multiOutputDrag: isMultiOutputDrag
|
movingExistingOutput: shouldMoveExistingOutput
|
||||||
},
|
},
|
||||||
event.pointerId
|
event.pointerId
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user