diff --git a/src/renderer/core/canvas/links/slotLinkDragState.ts b/src/renderer/core/canvas/links/slotLinkDragState.ts index 0c83c70cc2..33c47f0f50 100644 --- a/src/renderer/core/canvas/links/slotLinkDragState.ts +++ b/src/renderer/core/canvas/links/slotLinkDragState.ts @@ -7,7 +7,7 @@ import type { Point, SlotLayout } from '@/renderer/core/layout/types' type SlotDragType = 'input' | 'output' -export interface SlotDragSource { +interface SlotDragSource { nodeId: string slotIndex: number type: SlotDragType diff --git a/src/renderer/core/canvas/links/slotLinkPreviewRenderer.ts b/src/renderer/core/canvas/links/slotLinkPreviewRenderer.ts index 9f5e1d6ae2..2c63eca208 100644 --- a/src/renderer/core/canvas/links/slotLinkPreviewRenderer.ts +++ b/src/renderer/core/canvas/links/slotLinkPreviewRenderer.ts @@ -1,16 +1,10 @@ import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' -import type { - INodeInputSlot, - INodeOutputSlot, - ReadOnlyPoint -} from '@/lib/litegraph/src/interfaces' +import type { RenderLink } from '@/lib/litegraph/src/canvas/RenderLink' +import type { ReadOnlyPoint } from '@/lib/litegraph/src/interfaces' import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' import { resolveConnectingLinkColor } from '@/lib/litegraph/src/utils/linkColors' import { createLinkConnectorAdapter } from '@/renderer/core/canvas/links/linkConnectorAdapter' -import { - type SlotDragSource, - useSlotLinkDragState -} from '@/renderer/core/canvas/links/slotLinkDragState' +import { useSlotLinkDragState } from '@/renderer/core/canvas/links/slotLinkDragState' 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' @@ -44,7 +38,7 @@ export function attachSlotLinkPreviewRenderer(canvas: LGraphCanvas) { const { state } = useSlotLinkDragState() if (!state.active || !state.source) return - const { pointer, source } = state + const { pointer } = state const linkRenderer = canvas.linkRenderer if (!linkRenderer) return @@ -53,80 +47,123 @@ export function attachSlotLinkPreviewRenderer(canvas: LGraphCanvas) { // 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] - } + if (!adapter || !renderLinks || renderLinks.length === 0) return - const colour = resolveConnectingLinkColor(link.fromSlot.type) - const startDir = link.fromDirection ?? LinkDirection.RIGHT - const endDir = link.dragDirection ?? LinkDirection.CENTER + const uniqueLinks = dedupeRenderLinks(renderLinks) - 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 to: ReadOnlyPoint = [pointer.canvas.x, pointer.canvas.y] - const startDir = source.direction ?? LinkDirection.RIGHT - const endDir = LinkDirection.CENTER - const colour = resolveConnectingLinkColor(sourceSlot?.type) ctx.save() - linkRenderer.renderDraggingLink( - ctx, - from, - to, - colour, - startDir, - endDir, - context - ) + for (const link of uniqueLinks) { + const startDir = link.fromDirection ?? LinkDirection.RIGHT + const endDir = link.dragDirection ?? LinkDirection.CENTER + const colour = resolveConnectingLinkColor(link.fromSlot.type) + + const fromPoint = resolveRenderLinkOrigin(link) + + linkRenderer.renderDraggingLink( + ctx, + fromPoint, + to, + colour, + startDir, + endDir, + context + ) + } ctx.restore() } canvas.onDrawForeground = patched } -function resolveSourceSlot( - canvas: LGraphCanvas, - source: SlotDragSource -): INodeInputSlot | INodeOutputSlot | undefined { - const graph = canvas.graph - if (!graph) return undefined +function dedupeRenderLinks(links: ReadonlyArray): RenderLink[] { + const uniqueByKey = new Map() + const fallback: RenderLink[] = [] - const nodeId = Number(source.nodeId) - if (!Number.isFinite(nodeId)) return undefined + for (const link of links) { + const key = getRenderLinkKey(link) + if (!key) { + fallback.push(link) + continue + } - const node = graph.getNodeById(nodeId) - if (!node) return undefined + const existing = uniqueByKey.get(key) + if (!existing) { + uniqueByKey.set(key, link) + continue + } - return source.type === 'output' - ? node.outputs?.[source.slotIndex] - : node.inputs?.[source.slotIndex] + // Prefer links that originate from reroutes to keep anchors stable + if (!existing.fromReroute && link.fromReroute) { + uniqueByKey.set(key, link) + } else if (existing.fromReroute === link.fromReroute) { + // Prefer the one with an explicit drag direction when both share the same origin + if ( + (!existing.dragDirection || + existing.dragDirection === LinkDirection.CENTER) && + link.dragDirection && + link.dragDirection !== LinkDirection.CENTER + ) { + uniqueByKey.set(key, link) + } + } + } + + return [...uniqueByKey.values(), ...fallback] +} + +function resolveRenderLinkOrigin(link: RenderLink): ReadOnlyPoint { + if (link.fromReroute) { + const rerouteLayout = layoutStore.getRerouteLayout(link.fromReroute.id) + if (rerouteLayout) { + return [rerouteLayout.position.x, rerouteLayout.position.y] + } + + const [x, y] = link.fromReroute.pos + return [x, y] + } + + const nodeId = getRenderLinkNodeId(link) + if (nodeId != null) { + const isInputFrom = link.toType === 'output' + const key = getSlotKey(String(nodeId), link.fromSlotIndex, isInputFrom) + const layout = layoutStore.getSlotLayout(key) + if (layout) { + return [layout.position.x, layout.position.y] + } + } + + return link.fromPos +} + +function getRenderLinkNodeId(link: RenderLink): number | null { + const node = link.node + if (typeof node === 'object' && node !== null && 'id' in node) { + const maybeId = node.id + if (typeof maybeId === 'number') return maybeId + } + return null +} + +function getRenderLinkKey(link: RenderLink): string | null { + const linkId = getUnderlyingLinkId(link) + if (linkId != null) return `link:${linkId}` + + const rerouteId = link.fromReroute?.id + if (typeof rerouteId === 'number') { + return `reroute:${rerouteId}` + } + + const nodeId = getRenderLinkNodeId(link) + if (nodeId != null) { + return `node:${nodeId}:slot:${link.fromSlotIndex}:to:${link.toType}` + } + + return null +} + +function getUnderlyingLinkId(link: RenderLink): number | null { + const maybeLink = (link as { link?: { id?: unknown } }).link + const maybeId = maybeLink?.id + return typeof maybeId === 'number' ? maybeId : null }