-
Notifications
You must be signed in to change notification settings - Fork 673
Expand file tree
/
Copy pathuseNodeDrag.ts
More file actions
322 lines (273 loc) · 9.95 KB
/
Copy pathuseNodeDrag.ts
File metadata and controls
322 lines (273 loc) · 9.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
import { createSharedComposable, whenever } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { toValue } from 'vue'
import type { Positionable } from '@/lib/litegraph/src/interfaces'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { AutoPanController } from '@/renderer/core/canvas/useAutoPan'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { LayoutSource } from '@/renderer/core/layout/types'
import type { NodeBoundsUpdate, Point } from '@/renderer/core/layout/types'
import type { NodeId } from '@/types/nodeId'
import { useNodeSnap } from '@/renderer/extensions/vueNodes/composables/useNodeSnap'
import { useShiftKeySync } from '@/renderer/extensions/vueNodes/composables/useShiftKeySync'
import { useTransformState } from '@/renderer/core/layout/transform/useTransformState'
import { isLGraphNode } from '@/utils/litegraphUtil'
export const useNodeDrag = createSharedComposable(useNodeDragIndividual)
function useNodeDragIndividual() {
const mutations = useLayoutMutations()
const { selectedNodeIds, selectedItems } = storeToRefs(useCanvasStore())
// Get transform utilities from TransformPane if available
const transformState = useTransformState()
// Snap-to-grid functionality
const { shouldSnap, applySnapToPosition } = useNodeSnap()
// Shift key sync for LiteGraph canvas preview
const { trackShiftKey } = useShiftKeySync()
const canvasStore = useCanvasStore()
// Drag state
let dragStartPos: Point | null = null
let dragStartMouse: Point | null = null
let otherSelectedNodesStartPositions: Map<NodeId, Point> | null = null
let rafId: number | null = null
let stopShiftSync: (() => void) | null = null
// For groups: track the last applied canvas delta to compute frame delta
let lastCanvasDelta: Point | null = null
let selectedNonNode: Positionable[] | null = null
// Auto-pan state
let autoPan: AutoPanController | null = null
let lastPointerX = 0
let lastPointerY = 0
function startDrag(event: PointerEvent, nodeId: NodeId) {
const layout = toValue(layoutStore.getNodeLayoutRef(nodeId))
if (!layout) return
const position = layout.position ?? { x: 0, y: 0 }
// Track shift key state and sync to canvas for snap preview
stopShiftSync = trackShiftKey(event)
dragStartPos = { ...position }
dragStartMouse = { x: event.clientX, y: event.clientY }
lastPointerX = event.clientX
lastPointerY = event.clientY
const selectedNodes = toValue(selectedNodeIds)
// capture the starting positions of all other selected nodes
// Only move other selected items if the dragged node is part of the selection
const isDraggedNodeInSelection = selectedNodes?.has(nodeId)
if (isDraggedNodeInSelection && selectedNodes.size > 1) {
otherSelectedNodesStartPositions = new Map()
for (const id of selectedNodes) {
// Skip the current node being dragged
if (id === nodeId) continue
const nodeLayout = layoutStore.getNodeLayoutRef(id).value
if (nodeLayout) {
otherSelectedNodesStartPositions.set(id, { ...nodeLayout.position })
}
}
} else {
otherSelectedNodesStartPositions = null
}
// Capture selected groups only if the dragged node is part of the selection
// This prevents groups from moving when dragging an unrelated node
if (isDraggedNodeInSelection) {
selectedNonNode = toValue(selectedItems).filter((i) => !isLGraphNode(i))
lastCanvasDelta = { x: 0, y: 0 }
} else {
selectedNonNode = null
lastCanvasDelta = null
}
mutations.setSource(LayoutSource.Vue)
}
function startAutoPan(event: PointerEvent, nodeId: NodeId) {
if (autoPan) {
autoPan.updatePointer(event.clientX, event.clientY)
return
}
const lgCanvas = canvasStore.canvas
if (!lgCanvas?.ds) return
autoPan = new AutoPanController({
canvas: lgCanvas.canvas,
ds: lgCanvas.ds,
maxPanSpeed: lgCanvas.auto_pan_speed,
onPan: (panX, panY) => {
if (dragStartPos) {
dragStartPos.x += panX
dragStartPos.y += panY
}
if (otherSelectedNodesStartPositions) {
for (const pos of otherSelectedNodesStartPositions.values()) {
pos.x += panX
pos.y += panY
}
}
if (selectedNonNode) {
for (const group of selectedNonNode) {
group.move(panX, panY, true)
}
}
updateNodePositions(nodeId)
}
})
autoPan.updatePointer(event.clientX, event.clientY)
autoPan.start()
}
/**
* Recalculates all dragged node positions based on the current mouse
* position and canvas transform.
*/
function updateNodePositions(nodeId: NodeId) {
if (!dragStartPos || !dragStartMouse) return
const mouseDelta = {
x: lastPointerX - dragStartMouse.x,
y: lastPointerY - dragStartMouse.y
}
const canvasOrigin = transformState.screenToCanvas({ x: 0, y: 0 })
const canvasWithDelta = transformState.screenToCanvas(mouseDelta)
const canvasDelta = {
x: canvasWithDelta.x - canvasOrigin.x,
y: canvasWithDelta.y - canvasOrigin.y
}
// Move drag updates in one transaction to avoid per-node notify fan-out.
const updates = [
{
nodeId,
position: {
x: dragStartPos.x + canvasDelta.x,
y: dragStartPos.y + canvasDelta.y
}
}
]
if (
otherSelectedNodesStartPositions &&
otherSelectedNodesStartPositions.size > 0
) {
for (const [otherNodeId, startPos] of otherSelectedNodesStartPositions) {
updates.push({
nodeId: otherNodeId,
position: {
x: startPos.x + canvasDelta.x,
y: startPos.y + canvasDelta.y
}
})
}
}
mutations.batchMoveNodes(updates)
if (selectedNonNode && selectedNonNode.length > 0 && lastCanvasDelta) {
const frameDelta = {
x: canvasDelta.x - lastCanvasDelta.x,
y: canvasDelta.y - lastCanvasDelta.y
}
for (const group of selectedNonNode) {
group.move(frameDelta.x, frameDelta.y, true)
}
}
lastCanvasDelta = canvasDelta
}
function handleDrag(event: PointerEvent, nodeId: NodeId) {
if (!dragStartPos || !dragStartMouse) {
return
}
if (canvasStore.isReadOnly) {
const canvas = canvasStore.getCanvas()
const delta = [event.clientX - lastPointerX, event.clientY - lastPointerY]
canvas.ds.offset[0] += delta[0] / canvas.ds.scale
canvas.ds.offset[1] += delta[1] / canvas.ds.scale
canvas.setDirty(true, true)
lastPointerX = event.clientX
lastPointerY = event.clientY
dragStartMouse.x += delta[0]
dragStartMouse.y += delta[1]
return
}
// Throttle position updates using requestAnimationFrame for better performance
if (rafId !== null) return // Skip if frame already scheduled
const { target, pointerId } = event
if (target instanceof HTMLElement && !target.hasPointerCapture(pointerId)) {
// Delay capture to drag to allow for the Node cloning
target.setPointerCapture(pointerId)
}
lastPointerX = event.clientX
lastPointerY = event.clientY
startAutoPan(event, nodeId)
rafId = requestAnimationFrame(() => {
rafId = null
updateNodePositions(nodeId)
})
}
function endDrag(event: PointerEvent, nodeId: NodeId | undefined) {
// Apply snap to final position if snap was active (matches LiteGraph behavior)
if (shouldSnap(event) && nodeId) {
const boundsUpdates: NodeBoundsUpdate[] = []
// Snap main node
const currentLayout = toValue(layoutStore.getNodeLayoutRef(nodeId))
if (currentLayout) {
const currentPos = currentLayout.position
const snappedPos = applySnapToPosition({ ...currentPos })
// Only add update if position actually changed
if (snappedPos.x !== currentPos.x || snappedPos.y !== currentPos.y) {
boundsUpdates.push({
nodeId,
bounds: {
x: snappedPos.x,
y: snappedPos.y,
width: currentLayout.size.width,
height: currentLayout.size.height
}
})
}
}
// Also snap other selected nodes
// Capture all positions at the start to ensure consistent state
if (
otherSelectedNodesStartPositions &&
otherSelectedNodesStartPositions.size > 0
) {
for (const otherNodeId of otherSelectedNodesStartPositions.keys()) {
const nodeLayout = layoutStore.getNodeLayoutRef(otherNodeId).value
if (nodeLayout) {
const currentPos = { ...nodeLayout.position }
const snappedPos = applySnapToPosition(currentPos)
// Only add update if position actually changed
if (
snappedPos.x !== currentPos.x ||
snappedPos.y !== currentPos.y
) {
boundsUpdates.push({
nodeId: otherNodeId,
bounds: {
x: snappedPos.x,
y: snappedPos.y,
width: nodeLayout.size.width,
height: nodeLayout.size.height
}
})
}
}
}
}
// Apply all snap updates in a single batched transaction
if (boundsUpdates.length > 0) {
layoutStore.batchUpdateNodeBounds(boundsUpdates)
}
}
resetDragState()
}
function resetDragState() {
dragStartPos = null
dragStartMouse = null
otherSelectedNodesStartPositions = null
selectedNonNode = null
lastCanvasDelta = null
autoPan?.stop()
autoPan = null
stopShiftSync?.()
stopShiftSync = null
if (rafId !== null) {
cancelAnimationFrame(rafId)
rafId = null
}
}
whenever(() => !layoutStore.isDraggingVueNodes.value, resetDragState)
return {
startDrag,
handleDrag,
endDrag
}
}