Skip to content

Commit 4b95ef9

Browse files
committed
Implement caching and rAF
1 parent 18b4f56 commit 4b95ef9

File tree

4 files changed

+187
-41
lines changed

4 files changed

+187
-41
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { SlotLayout } from '@/renderer/core/layout/types'
2+
3+
export interface PendingMoveData {
4+
clientX: number
5+
clientY: number
6+
target: EventTarget | null
7+
}
8+
9+
export interface SlotLinkDragSession {
10+
compatCache: Map<string, boolean>
11+
nodePreferred: Map<
12+
number,
13+
{ index: number; key: string; layout: SlotLayout } | null
14+
>
15+
lastHoverSlotKey: string | null
16+
lastHoverNodeId: number | null
17+
lastCandidateKey: string | null
18+
pendingMove: PendingMoveData | null
19+
reset: () => void
20+
dispose: () => void
21+
}
22+
23+
export function createSlotLinkDragSession(): SlotLinkDragSession {
24+
const state: SlotLinkDragSession = {
25+
compatCache: new Map(),
26+
nodePreferred: new Map(),
27+
lastHoverSlotKey: null,
28+
lastHoverNodeId: null,
29+
lastCandidateKey: null,
30+
pendingMove: null,
31+
reset: () => {
32+
state.compatCache = new Map()
33+
state.nodePreferred = new Map()
34+
state.lastHoverSlotKey = null
35+
state.lastHoverNodeId = null
36+
state.lastCandidateKey = null
37+
state.pendingMove = null
38+
},
39+
dispose: () => {
40+
state.reset()
41+
}
42+
}
43+
44+
return state
45+
}

src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,17 @@ import {
1717
isSizeEqual
1818
} from '@/renderer/core/layout/utils/geometry'
1919
import { useNodeSlotRegistryStore } from '@/renderer/extensions/vueNodes/stores/nodeSlotRegistryStore'
20+
import { createRafBatch } from '@/utils/rafBatch'
2021

2122
// RAF batching
2223
const pendingNodes = new Set<string>()
23-
let rafId: number | null = null
24+
const raf = createRafBatch(() => {
25+
flushScheduledSlotLayoutSync()
26+
})
2427

2528
function scheduleSlotLayoutSync(nodeId: string) {
2629
pendingNodes.add(nodeId)
27-
if (rafId == null) {
28-
rafId = requestAnimationFrame(() => {
29-
rafId = null
30-
flushScheduledSlotLayoutSync()
31-
})
32-
}
30+
raf.schedule()
3331
}
3432

3533
function flushScheduledSlotLayoutSync() {

src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts

Lines changed: 108 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
2222
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
2323
import type { Point } from '@/renderer/core/layout/types'
2424
import { toPoint } from '@/renderer/core/layout/utils/geometry'
25+
import { createSlotLinkDragSession } from '@/renderer/extensions/vueNodes/composables/slotLinkDragSession'
2526
import { app } from '@/scripts/app'
27+
import { createRafBatch } from '@/utils/rafBatch'
2628

2729
interface SlotInteractionOptions {
2830
nodeId: string
@@ -89,6 +91,12 @@ export function useSlotLinkInteraction({
8991

9092
const { state, beginDrag, endDrag, updatePointerPosition, setCandidate } =
9193
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()
92100

93101
function candidateFromTarget(
94102
target: EventTarget | null
@@ -106,16 +114,16 @@ export function useSlotLinkInteraction({
106114
const graph = app.canvas?.graph
107115
const adapter = ensureActiveAdapter()
108116
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
119127
}
120128
}
121129

@@ -135,6 +143,15 @@ export function useSlotLinkInteraction({
135143
if (!adapter || !graph) return null
136144

137145
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+
138155
const node = graph.getNodeById(nodeId)
139156
if (!node) return null
140157

@@ -162,16 +179,18 @@ export function useSlotLinkInteraction({
162179
? adapter.isInputValidDrop(nodeId, index)
163180
: adapter.isOutputValidDrop(nodeId, index)
164181

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+
}
168192
}
169193

170-
const conversion = useSharedCanvasPositionConversion()
171-
172-
const pointerSession = createPointerSession()
173-
let activeAdapter: LinkConnectorAdapter | null = null
174-
175194
const ensureActiveAdapter = (): LinkConnectorAdapter | null => {
176195
if (!activeAdapter) activeAdapter = createLinkConnectorAdapter()
177196
return activeAdapter
@@ -302,6 +321,8 @@ export function useSlotLinkInteraction({
302321
pointerSession.clear()
303322
endDrag()
304323
activeAdapter = null
324+
raf.cancel()
325+
dragSession.dispose()
305326
}
306327

307328
const updatePointerState = (event: PointerEvent) => {
@@ -315,27 +336,74 @@ export function useSlotLinkInteraction({
315336
updatePointerPosition(clientX, clientY, canvasX, canvasY)
316337
}
317338

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
321343

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
335392
}
336393

337394
app.canvas?.setDirty(true)
338395
}
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+
}
339407

340408
// Attempt to finalize by connecting to a DOM slot candidate
341409
const tryConnectToCandidate = (
@@ -426,6 +494,8 @@ export function useSlotLinkInteraction({
426494
if (!pointerSession.matches(event)) return
427495
event.preventDefault()
428496

497+
raf.flush()
498+
429499
if (!state.source) {
430500
cleanupInteraction()
431501
app.canvas?.setDirty(true)
@@ -467,6 +537,8 @@ export function useSlotLinkInteraction({
467537

468538
const handlePointerCancel = (event: PointerEvent) => {
469539
if (!pointerSession.matches(event)) return
540+
541+
raf.flush()
470542
cleanupInteraction()
471543
app.canvas?.setDirty(true)
472544
}
@@ -481,6 +553,8 @@ export function useSlotLinkInteraction({
481553
if (!canvas || !graph) return
482554

483555
ensureActiveAdapter()
556+
raf.cancel()
557+
dragSession.reset()
484558

485559
const layout = layoutStore.getSlotLayout(
486560
getSlotKey(nodeId, index, type === 'input')

src/utils/rafBatch.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export function createRafBatch(run: () => void) {
2+
let rafId: number | null = null
3+
4+
const schedule = () => {
5+
if (rafId != null) return
6+
rafId = requestAnimationFrame(() => {
7+
rafId = null
8+
run()
9+
})
10+
}
11+
12+
const cancel = () => {
13+
if (rafId != null) {
14+
cancelAnimationFrame(rafId)
15+
rafId = null
16+
}
17+
}
18+
19+
const flush = () => {
20+
if (rafId == null) return
21+
cancelAnimationFrame(rafId)
22+
rafId = null
23+
run()
24+
}
25+
26+
const isScheduled = () => rafId != null
27+
28+
return { schedule, cancel, flush, isScheduled }
29+
}

0 commit comments

Comments
 (0)