diff --git a/src/renderer/core/canvas/links/slotLinkPreviewRenderer.ts b/src/renderer/core/canvas/links/slotLinkPreviewRenderer.ts index c8e513779..39e030458 100644 --- a/src/renderer/core/canvas/links/slotLinkPreviewRenderer.ts +++ b/src/renderer/core/canvas/links/slotLinkPreviewRenderer.ts @@ -40,7 +40,7 @@ export function attachSlotLinkPreviewRenderer(canvas: LGraphCanvas) { if (canvas.linkConnector?.isConnecting) return if (!state.active || !state.source) return - const { pointer } = state + const { pointer, candidate } = state const linkRenderer = canvas.linkRenderer if (!linkRenderer) return @@ -50,7 +50,9 @@ export function attachSlotLinkPreviewRenderer(canvas: LGraphCanvas) { const renderLinks = adapter?.renderLinks if (!adapter || !renderLinks || renderLinks.length === 0) return - const to: ReadOnlyPoint = [pointer.canvas.x, pointer.canvas.y] + const to: ReadOnlyPoint = candidate?.compatible + ? [candidate.layout.position.x, candidate.layout.position.y] + : [pointer.canvas.x, pointer.canvas.y] ctx.save() for (const link of renderLinks) { const startDir = link.fromDirection ?? LinkDirection.RIGHT diff --git a/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts b/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts index 37817ed5f..0845f5ac3 100644 --- a/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts +++ b/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts @@ -87,14 +87,15 @@ export function useSlotLinkInteraction({ } } - const { state, beginDrag, endDrag, updatePointerPosition } = + const { state, beginDrag, endDrag, updatePointerPosition, setCandidate } = useSlotLinkDragState() function candidateFromTarget( target: EventTarget | null ): SlotDropCandidate | null { if (!(target instanceof HTMLElement)) return null - const key = target.dataset['slotKey'] + const elWithKey = target.closest('[data-slot-key]') + const key = elWithKey?.dataset['slotKey'] if (!key) return null const layout = layoutStore.getSlotLayout(key) @@ -102,28 +103,70 @@ export function useSlotLinkInteraction({ const candidate: SlotDropCandidate = { layout, compatible: false } - if (state.source) { - const canvas = app.canvas - const graph = canvas?.graph - const adapter = ensureActiveAdapter() - 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 - ) - } + const graph = app.canvas?.graph + const adapter = ensureActiveAdapter() + 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 } + function candidateFromNodeTarget( + target: EventTarget | null + ): SlotDropCandidate | null { + if (!(target instanceof HTMLElement)) return null + const elWithNode = target.closest('[data-node-id]') + const nodeIdStr = elWithNode?.dataset['nodeId'] + if (!nodeIdStr) return null + + const adapter = ensureActiveAdapter() + const graph = app.canvas?.graph + if (!adapter || !graph) return null + + const nodeId = Number(nodeIdStr) + const node = graph.getNodeById(nodeId) + if (!node) return null + + const firstLink = adapter.renderLinks[0] + if (!firstLink) return null + const connectingTo = adapter.linkConnector.state.connectingTo + + if (connectingTo === 'input') { + const res = node.findInputByType(firstLink.fromSlot.type) + const index = res?.index + if (index == null) return null + const key = getSlotKey(String(nodeId), index, true) + const layout = layoutStore.getSlotLayout(key) + if (!layout) return null + const compatible = adapter.isInputValidDrop(nodeId, index) + if (!compatible) return null + return { layout, compatible: true } + } else if (connectingTo === 'output') { + const res = node.findOutputByType(firstLink.fromSlot.type) + const index = res?.index + if (index == null) return null + const key = getSlotKey(String(nodeId), index, false) + const layout = layoutStore.getSlotLayout(key) + if (!layout) return null + const compatible = adapter.isOutputValidDrop(nodeId, index) + if (!compatible) return null + return { layout, compatible: true } + } + + return null + } + const conversion = useSharedCanvasPositionConversion() const pointerSession = createPointerSession() @@ -359,6 +402,22 @@ export function useSlotLinkInteraction({ const handlePointerMove = (event: PointerEvent) => { if (!pointerSession.matches(event)) return updatePointerState(event) + + const adapter = ensureActiveAdapter() + // Resolve a candidate from slot under cursor, else from node + const slotCandidate = candidateFromTarget(event.target) + const nodeCandidate = slotCandidate + ? null + : candidateFromNodeTarget(event.target) + const candidate = slotCandidate ?? nodeCandidate + + // Update drag-state candidate; Vue preview renderer reads this + if (candidate?.compatible && adapter) { + setCandidate(candidate) + } else { + setCandidate(null) + } + app.canvas?.setDirty(true) // Debug: Log hovered slot/node IDs using event.target.dataset for review