feat(vue-nodes): snap link preview; connect on drop (#5780)

## Summary

Snap link preview to the nearest compatible slot while dragging in Vue
Nodes mode, and complete the connection on drop using the snapped
target. Mirrors LiteGraph’s first-compatible-slot logic for node-level
snapping and reuses the computed candidate for performance.

## Changes

- Snap preview end to compatible slot
  - slot under cursor via `data-slot-key` fast-path
  - node under cursor via `findInputByType` / `findOutputByType`
- Render path
- `slotLinkPreviewRenderer.ts` now renders to
`state.candidate.layout.position`
- Complete on drop
  - Prefer `state.candidate` (no re-hit-testing)
  - Fallbacks: DOM slot → node first-compatible → reroute
  - Disconnects moving input link when dropped on canvas

## Review Focus

- UX feel of snapping and drop completion (both directions)
- Performance on large graphs (mousemove path is O(1) with dataset +
single validation)
- Edge cases: reroutes, moving existing links, collapsed nodes

## Screenshots (if applicable)


https://github.com/user-attachments/assets/fbed0ae2-2231-473b-a05a-9aaf68e3f820

https://github.com/Comfy-Org/ComfyUI_frontend/pull/5780 (snapping) <--
https://github.com/Comfy-Org/ComfyUI_frontend/pull/5898 (drop on canvas
+ linkconnectoradapter refactor) <--
https://github.com/Comfy-Org/ComfyUI_frontend/pull/5903 (fix reroute
snapping)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5780-feat-vue-nodes-snap-link-preview-connect-on-drop-27a6d73d365081d89c8cf570e2049c89)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Benjamin Lu
2025-10-04 21:48:59 -07:00
committed by GitHub
parent 4e7e6e2bf3
commit cd7310cb8c
21 changed files with 365 additions and 36 deletions

View File

@@ -17,19 +17,17 @@ import {
isSizeEqual
} from '@/renderer/core/layout/utils/geometry'
import { useNodeSlotRegistryStore } from '@/renderer/extensions/vueNodes/stores/nodeSlotRegistryStore'
import { createRafBatch } from '@/utils/rafBatch'
// RAF batching
const pendingNodes = new Set<string>()
let rafId: number | null = null
const raf = createRafBatch(() => {
flushScheduledSlotLayoutSync()
})
function scheduleSlotLayoutSync(nodeId: string) {
pendingNodes.add(nodeId)
if (rafId == null) {
rafId = requestAnimationFrame(() => {
rafId = null
flushScheduledSlotLayoutSync()
})
}
raf.schedule()
}
function flushScheduledSlotLayoutSync() {