@@ -22,7 +22,9 @@ import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
22
22
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
23
23
import type { Point } from '@/renderer/core/layout/types'
24
24
import { toPoint } from '@/renderer/core/layout/utils/geometry'
25
+ import { createSlotLinkDragSession } from '@/renderer/extensions/vueNodes/composables/slotLinkDragSession'
25
26
import { app } from '@/scripts/app'
27
+ import { createRafBatch } from '@/utils/rafBatch'
26
28
27
29
interface SlotInteractionOptions {
28
30
nodeId : string
@@ -89,6 +91,12 @@ export function useSlotLinkInteraction({
89
91
90
92
const { state, beginDrag, endDrag, updatePointerPosition, setCandidate } =
91
93
useSlotLinkDragState ( )
94
+ const conversion = useSharedCanvasPositionConversion ( )
95
+ const pointerSession = createPointerSession ( )
96
+ let activeAdapter : LinkConnectorAdapter | null = null
97
+
98
+ // Per-drag drag-state cache
99
+ const dragSession = createSlotLinkDragSession ( )
92
100
93
101
function candidateFromTarget (
94
102
target : EventTarget | null
@@ -106,16 +114,16 @@ export function useSlotLinkInteraction({
106
114
const graph = app . canvas ?. graph
107
115
const adapter = ensureActiveAdapter ( )
108
116
if ( graph && adapter ) {
109
- if ( layout . type === 'input' ) {
110
- candidate . compatible = adapter . isInputValidDrop (
111
- layout . nodeId ,
112
- layout . index
113
- )
114
- } else if ( layout . type === 'output' ) {
115
- candidate . compatible = adapter . isOutputValidDrop (
116
- layout . nodeId ,
117
- layout . index
118
- )
117
+ const cached = dragSession . compatCache . get ( key )
118
+ if ( cached != null ) {
119
+ candidate . compatible = cached
120
+ } else {
121
+ const compatible =
122
+ layout . type === 'input'
123
+ ? adapter . isInputValidDrop ( layout . nodeId , layout . index )
124
+ : adapter . isOutputValidDrop ( layout . nodeId , layout . index )
125
+ dragSession . compatCache . set ( key , compatible )
126
+ candidate . compatible = compatible
119
127
}
120
128
}
121
129
@@ -135,6 +143,15 @@ export function useSlotLinkInteraction({
135
143
if ( ! adapter || ! graph ) return null
136
144
137
145
const nodeId = Number ( nodeIdStr )
146
+
147
+ // Cached preferred slot for this node within this drag
148
+ const cachedPreferred = dragSession . nodePreferred . get ( nodeId )
149
+ if ( cachedPreferred !== undefined ) {
150
+ return cachedPreferred
151
+ ? { layout : cachedPreferred . layout , compatible : true }
152
+ : null
153
+ }
154
+
138
155
const node = graph . getNodeById ( nodeId )
139
156
if ( ! node ) return null
140
157
@@ -162,16 +179,18 @@ export function useSlotLinkInteraction({
162
179
? adapter . isInputValidDrop ( nodeId , index )
163
180
: adapter . isOutputValidDrop ( nodeId , index )
164
181
165
- return compatible ? { layout, compatible : true } : null
166
-
167
- return null
182
+ if ( compatible ) {
183
+ dragSession . compatCache . set ( key , true )
184
+ const preferred = { index, key, layout }
185
+ dragSession . nodePreferred . set ( nodeId , preferred )
186
+ return { layout, compatible : true }
187
+ } else {
188
+ dragSession . compatCache . set ( key , false )
189
+ dragSession . nodePreferred . set ( nodeId , null )
190
+ return null
191
+ }
168
192
}
169
193
170
- const conversion = useSharedCanvasPositionConversion ( )
171
-
172
- const pointerSession = createPointerSession ( )
173
- let activeAdapter : LinkConnectorAdapter | null = null
174
-
175
194
const ensureActiveAdapter = ( ) : LinkConnectorAdapter | null => {
176
195
if ( ! activeAdapter ) activeAdapter = createLinkConnectorAdapter ( )
177
196
return activeAdapter
@@ -302,6 +321,8 @@ export function useSlotLinkInteraction({
302
321
pointerSession . clear ( )
303
322
endDrag ( )
304
323
activeAdapter = null
324
+ raf . cancel ( )
325
+ dragSession . dispose ( )
305
326
}
306
327
307
328
const updatePointerState = ( event : PointerEvent ) => {
@@ -315,27 +336,74 @@ export function useSlotLinkInteraction({
315
336
updatePointerPosition ( clientX , clientY , canvasX , canvasY )
316
337
}
317
338
318
- const handlePointerMove = ( event : PointerEvent ) => {
319
- if ( ! pointerSession . matches ( event ) ) return
320
- updatePointerState ( event )
339
+ const processPointerMoveFrame = ( ) => {
340
+ const data = dragSession . pendingMove
341
+ if ( ! data ) return
342
+ dragSession . pendingMove = null
321
343
322
- const adapter = ensureActiveAdapter ( )
323
- // Resolve a candidate from slot under cursor, else from node
324
- const slotCandidate = candidateFromTarget ( event . target )
325
- const nodeCandidate = slotCandidate
326
- ? null
327
- : candidateFromNodeTarget ( event . target )
328
- const candidate = slotCandidate ?? nodeCandidate
329
-
330
- // Update drag-state candidate; Vue preview renderer reads this
331
- if ( candidate ?. compatible && adapter ) {
332
- setCandidate ( candidate )
333
- } else {
334
- setCandidate ( null )
344
+ const [ canvasX , canvasY ] = conversion . clientPosToCanvasPos ( [
345
+ data . clientX ,
346
+ data . clientY
347
+ ] )
348
+ updatePointerPosition ( data . clientX , data . clientY , canvasX , canvasY )
349
+
350
+ let hoveredSlotKey : string | null = null
351
+ let hoveredNodeId : number | null = null
352
+ const target = data . target
353
+ if ( target instanceof HTMLElement ) {
354
+ hoveredSlotKey =
355
+ target . closest < HTMLElement > ( '[data-slot-key]' ) ?. dataset [ 'slotKey' ] ??
356
+ null
357
+ if ( ! hoveredSlotKey ) {
358
+ const nodeIdStr =
359
+ target . closest < HTMLElement > ( '[data-node-id]' ) ?. dataset [ 'nodeId' ]
360
+ hoveredNodeId = nodeIdStr != null ? Number ( nodeIdStr ) : null
361
+ }
362
+ }
363
+
364
+ const hoverChanged =
365
+ hoveredSlotKey !== dragSession . lastHoverSlotKey ||
366
+ hoveredNodeId !== dragSession . lastHoverNodeId
367
+
368
+ let candidate : SlotDropCandidate | null = state . candidate
369
+
370
+ if ( hoverChanged ) {
371
+ const slotCandidate = candidateFromTarget ( target )
372
+ const nodeCandidate = slotCandidate
373
+ ? null
374
+ : candidateFromNodeTarget ( target )
375
+ candidate = slotCandidate ?? nodeCandidate
376
+ dragSession . lastHoverSlotKey = hoveredSlotKey
377
+ dragSession . lastHoverNodeId = hoveredNodeId
378
+ }
379
+
380
+ const newCandidate = candidate ?. compatible ? candidate : null
381
+ const newCandidateKey = newCandidate
382
+ ? getSlotKey (
383
+ newCandidate . layout . nodeId ,
384
+ newCandidate . layout . index ,
385
+ newCandidate . layout . type === 'input'
386
+ )
387
+ : null
388
+
389
+ if ( newCandidateKey !== dragSession . lastCandidateKey ) {
390
+ setCandidate ( newCandidate )
391
+ dragSession . lastCandidateKey = newCandidateKey
335
392
}
336
393
337
394
app . canvas ?. setDirty ( true )
338
395
}
396
+ const raf = createRafBatch ( processPointerMoveFrame )
397
+
398
+ const handlePointerMove = ( event : PointerEvent ) => {
399
+ if ( ! pointerSession . matches ( event ) ) return
400
+ dragSession . pendingMove = {
401
+ clientX : event . clientX ,
402
+ clientY : event . clientY ,
403
+ target : event . target
404
+ }
405
+ raf . schedule ( )
406
+ }
339
407
340
408
// Attempt to finalize by connecting to a DOM slot candidate
341
409
const tryConnectToCandidate = (
@@ -426,6 +494,8 @@ export function useSlotLinkInteraction({
426
494
if ( ! pointerSession . matches ( event ) ) return
427
495
event . preventDefault ( )
428
496
497
+ raf . flush ( )
498
+
429
499
if ( ! state . source ) {
430
500
cleanupInteraction ( )
431
501
app . canvas ?. setDirty ( true )
@@ -467,6 +537,8 @@ export function useSlotLinkInteraction({
467
537
468
538
const handlePointerCancel = ( event : PointerEvent ) => {
469
539
if ( ! pointerSession . matches ( event ) ) return
540
+
541
+ raf . flush ( )
470
542
cleanupInteraction ( )
471
543
app . canvas ?. setDirty ( true )
472
544
}
@@ -481,6 +553,8 @@ export function useSlotLinkInteraction({
481
553
if ( ! canvas || ! graph ) return
482
554
483
555
ensureActiveAdapter ( )
556
+ raf . cancel ( )
557
+ dragSession . reset ( )
484
558
485
559
const layout = layoutStore . getSlotLayout (
486
560
getSlotKey ( nodeId , index , type === 'input' )
0 commit comments