Comfy-Org / Comfy-Org/ComfyUI_frontend
Vue node drag applies nothing for gestures with fewer than three pointermove events
- Dominant language
- TypeScript
- Stars
- 2k
- Forks
- 704
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 512
Description
## What happens
A Vue-node drag applies **nothing** unless the gesture carries at least three `pointermove` events. One or two moves leave the node at its original position — not a partial move, zero — even if the pointer button stays held for hundreds of milliseconds afterwards.
Measured on the cloud build (1280x720, `Comfy.VueNodes.Enabled: true`, template `image_krea2_turbo_t2i`, node 30, node box 327x608, grab point 60px into its title bar):
| gesture | `page.mouse.move(..., { steps })` | node moved |
| --- | --- | --- |
| +120,+80 | 1 | `0, 0` |
| +120,+80 | 2 | `0, 0` |
| +120,+80 | 3 | `120, 80` |
| +220,+140 | 1 | `0, 0` |
| +220,+140 | 12 | `220, 140` |
Holding the button down after the single move does not rescue it:
```
down → move(+120,+80) → wait 400ms → node moved 0,0 → up → wait 400ms → node moved 0,0
```
Both destinations are inside the node box, so this is not "the pointer left the element".
## Where it is not
This was first suspected to be the first-run tour's coachmark scrim swallowing the rest of a drag once the pointer left the spotlight cutout (`coach-hit-region` in `src/platform/onboarding/TourOverlay.vue` is `pointer-events-auto` outside the hole). **It is not.** The same measurements with the tour dismissed (Escape) are identical:
```
tour open stepped +220,+140 → moved 220,140 jump +220,+140 → moved 0,0
tour closed stepped +220,+140 → moved 220,140 jump +220,+140 → moved 0,0
```
Once the drag engages, it survives the pointer leaving the cutout intact, because `useNodeDrag.handleDrag` takes pointer capture on the first move it sees.
## Leads
`src/renderer/extensions/vueNodes/layout/useNodeDrag.ts`:
- `handleDrag` batches the position update into `requestAnimationFrame` and early-returns while `rafId !== null`, so several moves inside one frame collapse into one scheduled update.
- `resetDragState`, which `endDrag` calls, does `cancelAnimationFrame(rafId)` — a scheduled update that has not run yet is dropped rather than flushed.
That pair explains a gesture that ends before its frame, but not the held case above, so the mechanism is not fully pinned.
## Impact
No known user-facing impact: a real pointer emits many moves across many frames, so a human drag always clears the threshold. It matters for synthetic input — Playwright's `page.mouse.move` defaults to `steps: 1`, so a raw drag in an e2e test silently no-ops and asserts nothing (the in-repo `ComfyMouse` helpers already pass a step count and are unaffected), and likewise for extensions that dispatch pointer events directly.
Contributor guide
Assessment
This issue has not been assessed yet.