@tldiagram/core-ui 1.92.0 → 1.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.d.ts +13 -1
- package/dist/components/ElementNode.d.ts +9 -0
- package/dist/config/runtime-vscode.d.ts +1 -0
- package/dist/config/runtime.d.ts +1 -0
- package/dist/index.js +10344 -9294
- package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +2 -1
- package/dist/pages/ViewEditor/hooks/useViewData.d.ts +20 -21
- package/dist/shims/empty-node-module.d.ts +2 -0
- package/dist/store/useStore.d.ts +78 -0
- package/dist/store/useStore.test.d.ts +1 -0
- package/package.json +7 -4
- package/src/api/client.ts +39 -1
- package/src/components/ElementNode.tsx +11 -58
- package/src/components/ElementPanel.tsx +2 -2
- package/src/components/LayoutSection.tsx +68 -93
- package/src/components/ViewGridNode.tsx +1 -4
- package/src/components/ZUI/renderer.ts +166 -66
- package/src/components/ZUI/useZUIInteraction.ts +235 -81
- package/src/config/runtime-vscode.ts +6 -0
- package/src/config/runtime.ts +4 -0
- package/src/main.tsx +26 -14
- package/src/pages/ViewEditor/context.tsx +12 -4
- package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +172 -121
- package/src/pages/ViewEditor/hooks/useViewData.ts +455 -253
- package/src/pages/ViewEditor/index.tsx +45 -32
- package/src/shims/empty-node-module.ts +1 -0
- package/src/store/useStore.test.ts +272 -0
- package/src/store/useStore.ts +285 -0
|
@@ -34,8 +34,100 @@ import {
|
|
|
34
34
|
ensureVisualHandleId,
|
|
35
35
|
getLogicalHandleId,
|
|
36
36
|
} from '../../../utils/edgeDistribution'
|
|
37
|
+
import { useStore } from '../../../store/useStore'
|
|
37
38
|
|
|
38
39
|
const SNAP_RADIUS = 75
|
|
40
|
+
const CONNECTOR_DRAG_UPDATE_INTERVAL_MS = 25
|
|
41
|
+
|
|
42
|
+
type HandleTarget = {
|
|
43
|
+
nodeId?: string
|
|
44
|
+
handleId: string
|
|
45
|
+
x: number
|
|
46
|
+
y: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function collectHandleTargets(excludeNodeId?: string): HandleTarget[] {
|
|
50
|
+
const handles = document.querySelectorAll('.react-flow__handle')
|
|
51
|
+
const targets: HandleTarget[] = []
|
|
52
|
+
|
|
53
|
+
for (const handle of handles) {
|
|
54
|
+
const nodeId = handle.closest('.react-flow__node')?.getAttribute('data-id') || undefined
|
|
55
|
+
if (excludeNodeId && nodeId === excludeNodeId) continue
|
|
56
|
+
|
|
57
|
+
const rect = handle.getBoundingClientRect()
|
|
58
|
+
targets.push({
|
|
59
|
+
nodeId,
|
|
60
|
+
handleId: handle.getAttribute('data-handleid') || handle.id,
|
|
61
|
+
x: rect.left + rect.width / 2,
|
|
62
|
+
y: rect.top + rect.height / 2,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return targets
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function findNearestHandleTargetInCache(targets: HandleTarget[], clientX: number, clientY: number) {
|
|
70
|
+
let hoveredHandleId: string | undefined
|
|
71
|
+
let hoveredNodeId: string | undefined
|
|
72
|
+
let snapPos = { x: clientX, y: clientY }
|
|
73
|
+
let nearestDistance = Infinity
|
|
74
|
+
|
|
75
|
+
for (const target of targets) {
|
|
76
|
+
const dist = Math.hypot(clientX - target.x, clientY - target.y)
|
|
77
|
+
if (dist < 36 && dist < nearestDistance) {
|
|
78
|
+
nearestDistance = dist
|
|
79
|
+
snapPos = { x: target.x, y: target.y }
|
|
80
|
+
hoveredHandleId = target.handleId
|
|
81
|
+
hoveredNodeId = target.nodeId
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
nearHandle: hoveredHandleId !== undefined,
|
|
87
|
+
snapPos,
|
|
88
|
+
hoveredHandleId,
|
|
89
|
+
hoveredNodeId,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function applyNodeChangesWithStructuralSharing(changes: NodeChange[], nodes: RFNode[]) {
|
|
94
|
+
if (changes.length === 0) return nodes
|
|
95
|
+
|
|
96
|
+
const canFastPath = changes.every((change) => change.type === 'position' && 'id' in change)
|
|
97
|
+
if (!canFastPath) return applyNodeChanges(changes, nodes)
|
|
98
|
+
|
|
99
|
+
const changesById = new Map(changes.map((change) => [change.id, change]))
|
|
100
|
+
let didChange = false
|
|
101
|
+
|
|
102
|
+
const nextNodes = nodes.map((node) => {
|
|
103
|
+
const change = changesById.get(node.id)
|
|
104
|
+
if (!change || change.type !== 'position') return node
|
|
105
|
+
|
|
106
|
+
const position = change.position ?? node.position
|
|
107
|
+
const positionAbsolute = change.positionAbsolute ?? node.positionAbsolute
|
|
108
|
+
const dragging = change.dragging ?? node.dragging
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
node.position.x === position.x &&
|
|
112
|
+
node.position.y === position.y &&
|
|
113
|
+
node.positionAbsolute?.x === positionAbsolute?.x &&
|
|
114
|
+
node.positionAbsolute?.y === positionAbsolute?.y &&
|
|
115
|
+
node.dragging === dragging
|
|
116
|
+
) {
|
|
117
|
+
return node
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
didChange = true
|
|
121
|
+
return {
|
|
122
|
+
...node,
|
|
123
|
+
position,
|
|
124
|
+
positionAbsolute,
|
|
125
|
+
dragging,
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
return didChange ? nextNodes : nodes
|
|
130
|
+
}
|
|
39
131
|
|
|
40
132
|
interface CanvasInteractionOptions {
|
|
41
133
|
viewId: number | null
|
|
@@ -127,8 +219,8 @@ export function useCanvasInteractions({
|
|
|
127
219
|
interactionSourceIdRef,
|
|
128
220
|
hoveredZoomRef,
|
|
129
221
|
hoverPanLockedUntilRef,
|
|
130
|
-
setViewElements,
|
|
131
|
-
setConnectors,
|
|
222
|
+
setViewElements: _setViewElements,
|
|
223
|
+
setConnectors: _setConnectors,
|
|
132
224
|
setRfNodes,
|
|
133
225
|
setRfEdges,
|
|
134
226
|
setLinksMap,
|
|
@@ -163,6 +255,10 @@ export function useCanvasInteractions({
|
|
|
163
255
|
onMoveStateChange,
|
|
164
256
|
}: CanvasInteractionOptions) {
|
|
165
257
|
const { screenToFlowPosition, setViewport, getViewport, zoomIn, zoomOut } = useReactFlow()
|
|
258
|
+
const updateElementPosition = useStore((state) => state.updateElementPosition)
|
|
259
|
+
const removeElementPlacement = useStore((state) => state.removeElementPlacement)
|
|
260
|
+
const upsertConnector = useStore((state) => state.upsertConnector)
|
|
261
|
+
const replaceConnector = useStore((state) => state.replaceConnector)
|
|
166
262
|
const screenToFlowPositionRef = useRef(screenToFlowPosition)
|
|
167
263
|
screenToFlowPositionRef.current = screenToFlowPosition
|
|
168
264
|
|
|
@@ -176,6 +272,7 @@ export function useCanvasInteractions({
|
|
|
176
272
|
const [reconnectPicking, setReconnectPicking] = useState<{ edgeId: number; endpoint: 'source' | 'target' } | null>(null)
|
|
177
273
|
const [handleReconnectDrag, setHandleReconnectDrag] = useState<HandleReconnectDragState | null>(null)
|
|
178
274
|
const [connectorLongPressMenu, setConnectorLongPressMenu] = useState<{ edgeId: number; x: number; y: number } | null>(null)
|
|
275
|
+
const isMovingRef = useRef(false)
|
|
179
276
|
|
|
180
277
|
interactionSourceIdRef.current = interactionSourceId
|
|
181
278
|
|
|
@@ -185,6 +282,7 @@ export function useCanvasInteractions({
|
|
|
185
282
|
const connectingSourceRef = useRef<string | null>(null)
|
|
186
283
|
const connectWasValidRef = useRef(false)
|
|
187
284
|
const connectGhostListenerRef = useRef<((e: MouseEvent) => void) | null>(null)
|
|
285
|
+
const connectorDragLastUpdateRef = useRef(0)
|
|
188
286
|
const isReconnectingRef = useRef(false)
|
|
189
287
|
const suppressNextConnectorClickRef = useRef(false)
|
|
190
288
|
const suppressNextPaneClickRef = useRef(false)
|
|
@@ -225,36 +323,6 @@ export function useCanvasInteractions({
|
|
|
225
323
|
isReconnectingRef.current = false
|
|
226
324
|
}, [clearHandleReconnectListeners])
|
|
227
325
|
|
|
228
|
-
const findNearestHandleTarget = useCallback((clientX: number, clientY: number, excludeNodeId?: string) => {
|
|
229
|
-
const handles = document.querySelectorAll('.react-flow__handle')
|
|
230
|
-
let hoveredHandleId: string | undefined
|
|
231
|
-
let hoveredNodeId: string | undefined
|
|
232
|
-
let snapPos = { x: clientX, y: clientY }
|
|
233
|
-
let nearestDistance = Infinity
|
|
234
|
-
|
|
235
|
-
for (const handle of handles) {
|
|
236
|
-
const nodeId = handle.closest('.react-flow__node')?.getAttribute('data-id') || undefined
|
|
237
|
-
if (excludeNodeId && nodeId === excludeNodeId) continue
|
|
238
|
-
const rect = handle.getBoundingClientRect()
|
|
239
|
-
const cx = rect.left + rect.width / 2
|
|
240
|
-
const cy = rect.top + rect.height / 2
|
|
241
|
-
const dist = Math.hypot(clientX - cx, clientY - cy)
|
|
242
|
-
if (dist < 36 && dist < nearestDistance) {
|
|
243
|
-
nearestDistance = dist
|
|
244
|
-
snapPos = { x: cx, y: cy }
|
|
245
|
-
hoveredHandleId = handle.getAttribute('data-handleid') || handle.id
|
|
246
|
-
hoveredNodeId = nodeId
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return {
|
|
251
|
-
nearHandle: hoveredHandleId !== undefined,
|
|
252
|
-
snapPos,
|
|
253
|
-
hoveredHandleId,
|
|
254
|
-
hoveredNodeId,
|
|
255
|
-
}
|
|
256
|
-
}, [])
|
|
257
|
-
|
|
258
326
|
// ── Ref-forwarded callbacks ────────────────────────────────────────────────
|
|
259
327
|
const openConnectorPanelRef = useRef(openConnectorPanel)
|
|
260
328
|
openConnectorPanelRef.current = openConnectorPanel
|
|
@@ -321,11 +389,12 @@ export function useCanvasInteractions({
|
|
|
321
389
|
source_element_id: sourceId, target_element_id: obj.id,
|
|
322
390
|
source_handle: sourceHandle, target_handle: targetHandle, direction: 'forward',
|
|
323
391
|
})
|
|
324
|
-
|
|
325
|
-
|
|
392
|
+
const connector = connectorToConnector(newConnector)
|
|
393
|
+
upsertConnectorGraphSnapshot(connector)
|
|
394
|
+
upsertConnector(connector)
|
|
326
395
|
}
|
|
327
396
|
} catch { /* intentionally empty */ }
|
|
328
|
-
}, [canEdit, viewId, addingElementAt, refreshElements, rfNodesRef,
|
|
397
|
+
}, [canEdit, viewId, addingElementAt, refreshElements, rfNodesRef, upsertConnector, viewElementsRef])
|
|
329
398
|
|
|
330
399
|
const handleConfirmExistingElement = useCallback(async (obj: LibraryElement) => {
|
|
331
400
|
if (!canEdit || viewId === null || !addingElementAt || addingElementAt.mode !== 'add') return
|
|
@@ -352,11 +421,12 @@ export function useCanvasInteractions({
|
|
|
352
421
|
source_element_id: sourceId, target_element_id: obj.id,
|
|
353
422
|
source_handle: sourceHandle, target_handle: targetHandle, direction: 'forward',
|
|
354
423
|
})
|
|
355
|
-
|
|
356
|
-
|
|
424
|
+
const connector = connectorToConnector(newConnector)
|
|
425
|
+
upsertConnectorGraphSnapshot(connector)
|
|
426
|
+
upsertConnector(connector)
|
|
357
427
|
}
|
|
358
428
|
} catch { /* intentionally empty */ }
|
|
359
|
-
}, [canEdit, viewId, addingElementAt, existingElementIds, refreshElements, rfNodesRef,
|
|
429
|
+
}, [canEdit, viewId, addingElementAt, existingElementIds, refreshElements, rfNodesRef, upsertConnector, viewElementsRef])
|
|
360
430
|
|
|
361
431
|
const handleConfirmConnectExistingElement = useCallback(async (obj: LibraryElement) => {
|
|
362
432
|
if (!canEdit || viewId === null || !addingElementAt || addingElementAt.mode !== 'connect') return
|
|
@@ -377,10 +447,11 @@ export function useCanvasInteractions({
|
|
|
377
447
|
target_handle: targetHandle,
|
|
378
448
|
direction: 'forward',
|
|
379
449
|
})
|
|
380
|
-
|
|
381
|
-
|
|
450
|
+
const connector = connectorToConnector(newConnector)
|
|
451
|
+
upsertConnectorGraphSnapshot(connector)
|
|
452
|
+
upsertConnector(connector)
|
|
382
453
|
} catch { /* intentionally empty */ }
|
|
383
|
-
}, [addingElementAt, canEdit, rfNodesRef,
|
|
454
|
+
}, [addingElementAt, canEdit, rfNodesRef, upsertConnector, viewId])
|
|
384
455
|
|
|
385
456
|
// ── Zoom-in / zoom-out stable callbacks ───────────────────────────────────
|
|
386
457
|
const stableOnZoomIn = useCallback(async (elementId: number) => {
|
|
@@ -467,20 +538,21 @@ export function useCanvasInteractions({
|
|
|
467
538
|
try {
|
|
468
539
|
await api.workspace.views.placements.remove(viewId, elementId)
|
|
469
540
|
removePlacementGraphSnapshot(viewId, elementId)
|
|
541
|
+
removeElementPlacement(elementId)
|
|
470
542
|
handleElementDeleted(elementId)
|
|
471
543
|
setInteractionSourceId(null)
|
|
472
544
|
} catch { /* intentionally empty */ }
|
|
473
|
-
}, [canEdit, viewId, handleElementDeleted])
|
|
545
|
+
}, [canEdit, viewId, removeElementPlacement, handleElementDeleted])
|
|
474
546
|
|
|
475
547
|
// ── Node/connector changes ─────────────────────────────────────────────────────
|
|
476
548
|
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
|
477
549
|
if (!canEdit) {
|
|
478
550
|
const nonMutating = changes.filter((c) => c.type !== 'position')
|
|
479
551
|
if (nonMutating.length === 0) return
|
|
480
|
-
setRfNodes((nds) =>
|
|
552
|
+
setRfNodes((nds) => applyNodeChangesWithStructuralSharing(nonMutating, nds))
|
|
481
553
|
return
|
|
482
554
|
}
|
|
483
|
-
setRfNodes((nds) =>
|
|
555
|
+
setRfNodes((nds) => applyNodeChangesWithStructuralSharing(changes, nds))
|
|
484
556
|
}, [canEdit, setRfNodes])
|
|
485
557
|
|
|
486
558
|
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
|
|
@@ -494,18 +566,11 @@ export function useCanvasInteractions({
|
|
|
494
566
|
dragStartPositionsRef.current[node.id] = { x: node.position.x, y: node.position.y }
|
|
495
567
|
}, [canEdit, viewId])
|
|
496
568
|
|
|
497
|
-
const onNodeDrag: NodeDragHandler = useCallback((
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
prev.map((element) =>
|
|
503
|
-
element.element_id === elementId
|
|
504
|
-
? { ...element, position_x: node.position.x, position_y: node.position.y }
|
|
505
|
-
: element,
|
|
506
|
-
),
|
|
507
|
-
)
|
|
508
|
-
}, [canEdit, setViewElements, viewId])
|
|
569
|
+
const onNodeDrag: NodeDragHandler = useCallback(() => {
|
|
570
|
+
// React Flow already updates rfNodes via onNodesChange while dragging.
|
|
571
|
+
// Mirroring into viewElements here forces every derived edge/node to rebuild
|
|
572
|
+
// on each pointer frame, so persist to app state only on drag stop.
|
|
573
|
+
}, [])
|
|
509
574
|
|
|
510
575
|
const positionTimers = useRef<Record<string, ReturnType<typeof setTimeout>>>({})
|
|
511
576
|
const dragStartPositionsRef = useRef<Record<string, { x: number; y: number }>>({})
|
|
@@ -522,13 +587,7 @@ export function useCanvasInteractions({
|
|
|
522
587
|
return
|
|
523
588
|
}
|
|
524
589
|
|
|
525
|
-
|
|
526
|
-
prev.map((element) =>
|
|
527
|
-
element.element_id === elementId
|
|
528
|
-
? { ...element, position_x: node.position.x, position_y: node.position.y }
|
|
529
|
-
: element,
|
|
530
|
-
),
|
|
531
|
-
)
|
|
590
|
+
updateElementPosition(elementId, node.position.x, node.position.y)
|
|
532
591
|
clearTimeout(positionTimers.current[node.id])
|
|
533
592
|
positionTimers.current[node.id] = setTimeout(() => {
|
|
534
593
|
api.workspace.views.placements
|
|
@@ -536,7 +595,7 @@ export function useCanvasInteractions({
|
|
|
536
595
|
.catch(() => { /* intentionally empty */ })
|
|
537
596
|
}, 400)
|
|
538
597
|
delete dragStartPositionsRef.current[node.id]
|
|
539
|
-
}, [canEdit,
|
|
598
|
+
}, [canEdit, updateElementPosition, viewId, viewElementsRef])
|
|
540
599
|
|
|
541
600
|
// ── Connections ────────────────────────────────────────────────────────────
|
|
542
601
|
const onConnect: OnConnect = useCallback(async (params: Connection) => {
|
|
@@ -554,25 +613,24 @@ export function useCanvasInteractions({
|
|
|
554
613
|
source_handle: sourceHandle, target_handle: targetHandle,
|
|
555
614
|
direction: 'forward', style: 'bezier',
|
|
556
615
|
})
|
|
557
|
-
|
|
558
|
-
|
|
616
|
+
const connector = connectorToConnector(newConnector)
|
|
617
|
+
upsertConnectorGraphSnapshot(connector)
|
|
618
|
+
upsertConnector(connector)
|
|
559
619
|
} catch { /* intentionally empty */ }
|
|
560
|
-
}, [canEdit,
|
|
620
|
+
}, [canEdit, upsertConnector, viewId])
|
|
561
621
|
|
|
562
622
|
const onConnectStart = useCallback((_: React.MouseEvent | React.TouchEvent, { nodeId }: OnConnectStartParams) => {
|
|
563
623
|
if (!canEdit || isReconnectingRef.current) return
|
|
564
624
|
connectingSourceRef.current = nodeId
|
|
565
625
|
connectWasValidRef.current = false
|
|
626
|
+
const handleTargets = collectHandleTargets(nodeId ?? undefined)
|
|
627
|
+
connectorDragLastUpdateRef.current = 0
|
|
566
628
|
const listener = (e: MouseEvent) => {
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
nearHandle = true; break
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
setConnectGhostPos(nearHandle ? null : { x: e.clientX, y: e.clientY })
|
|
629
|
+
const now = performance.now()
|
|
630
|
+
if (now - connectorDragLastUpdateRef.current < CONNECTOR_DRAG_UPDATE_INTERVAL_MS) return
|
|
631
|
+
connectorDragLastUpdateRef.current = now
|
|
632
|
+
const hit = findNearestHandleTargetInCache(handleTargets, e.clientX, e.clientY)
|
|
633
|
+
setConnectGhostPos(hit.nearHandle ? null : { x: e.clientX, y: e.clientY })
|
|
576
634
|
}
|
|
577
635
|
connectGhostListenerRef.current = listener
|
|
578
636
|
document.addEventListener('mousemove', listener)
|
|
@@ -618,15 +676,16 @@ export function useCanvasInteractions({
|
|
|
618
676
|
source_element_id: sourceElementId, target_element_id: targetElementId,
|
|
619
677
|
source_handle: sourceHandle, target_handle: targetHandle, direction: 'forward',
|
|
620
678
|
}).then((connector) => {
|
|
621
|
-
|
|
622
|
-
|
|
679
|
+
const next = connectorToConnector(connector)
|
|
680
|
+
upsertConnectorGraphSnapshot(next)
|
|
681
|
+
upsertConnector(next)
|
|
623
682
|
}).catch(() => { /* intentionally empty */ })
|
|
624
683
|
} else {
|
|
625
684
|
setPendingConnectionSource(sourceElementId)
|
|
626
685
|
suppressNextPaneClickRef.current = true
|
|
627
686
|
showAddingElementAt(clientX, clientY, true, 'connect', 'shiftKey' in event && event.shiftKey)
|
|
628
687
|
}
|
|
629
|
-
}, [canEdit,
|
|
688
|
+
}, [canEdit, upsertConnector, showAddingElementAt, rfNodesRef, viewIdRef])
|
|
630
689
|
|
|
631
690
|
// ── Reconnect ──────────────────────────────────────────────────────────────
|
|
632
691
|
const performReconnect = useCallback(async (oldConnector: RFEdge, newConnection: Connection) => {
|
|
@@ -650,12 +709,11 @@ export function useCanvasInteractions({
|
|
|
650
709
|
style: existingData?.style === 'default' ? 'bezier' : (existingData?.style ?? 'bezier'),
|
|
651
710
|
url: existingData?.url ?? undefined, relationship: existingData?.relationship ?? undefined,
|
|
652
711
|
})
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
)
|
|
712
|
+
const connector = connectorToConnector(updated)
|
|
713
|
+
upsertConnectorGraphSnapshot(connector)
|
|
714
|
+
replaceConnector(connector)
|
|
657
715
|
} catch { /* intentionally empty */ }
|
|
658
|
-
}, [canEdit,
|
|
716
|
+
}, [canEdit, replaceConnector, viewId, setRfEdges, setSelectedEdgeId])
|
|
659
717
|
const onReconnect = useCallback(async (oldConnector: RFEdge, newConnection: Connection) => {
|
|
660
718
|
await performReconnect(oldConnector, newConnection)
|
|
661
719
|
}, [performReconnect])
|
|
@@ -685,6 +743,8 @@ export function useCanvasInteractions({
|
|
|
685
743
|
setConnectGhostPos(null)
|
|
686
744
|
clearHandleReconnectListeners()
|
|
687
745
|
isReconnectingRef.current = true
|
|
746
|
+
const handleTargets = collectHandleTargets(fixedNodeId)
|
|
747
|
+
connectorDragLastUpdateRef.current = 0
|
|
688
748
|
syncHandleReconnectDrag({
|
|
689
749
|
edgeId: args.edgeId,
|
|
690
750
|
endpoint: args.endpoint,
|
|
@@ -695,7 +755,10 @@ export function useCanvasInteractions({
|
|
|
695
755
|
})
|
|
696
756
|
|
|
697
757
|
const move = (event: PointerEvent) => {
|
|
698
|
-
const
|
|
758
|
+
const now = performance.now()
|
|
759
|
+
if (now - connectorDragLastUpdateRef.current < CONNECTOR_DRAG_UPDATE_INTERVAL_MS) return
|
|
760
|
+
connectorDragLastUpdateRef.current = now
|
|
761
|
+
const hit = findNearestHandleTargetInCache(handleTargets, event.clientX, event.clientY)
|
|
699
762
|
const current = handleReconnectDragRef.current
|
|
700
763
|
if (!current) return
|
|
701
764
|
syncHandleReconnectDrag({
|
|
@@ -776,7 +839,7 @@ export function useCanvasInteractions({
|
|
|
776
839
|
document.addEventListener('pointermove', move)
|
|
777
840
|
document.addEventListener('pointerup', up)
|
|
778
841
|
document.addEventListener('pointercancel', up)
|
|
779
|
-
}, [canEdit, clearHandleReconnectListeners,
|
|
842
|
+
}, [canEdit, clearHandleReconnectListeners, performReconnect, rfNodesRef, _rfEdgesRef, syncHandleReconnectDrag])
|
|
780
843
|
|
|
781
844
|
// ── Click-connect ghost cursor tracking ────────────────────────────────────
|
|
782
845
|
useEffect(() => {
|
|
@@ -785,43 +848,29 @@ export function useCanvasInteractions({
|
|
|
785
848
|
setConnectGhostPos(null)
|
|
786
849
|
return
|
|
787
850
|
}
|
|
851
|
+
const handleTargets = collectHandleTargets(clickConnectMode.sourceNodeId)
|
|
852
|
+
const sourceHandleTargets = collectHandleTargets().filter((target) => target.nodeId === clickConnectMode.sourceNodeId)
|
|
853
|
+
connectorDragLastUpdateRef.current = 0
|
|
788
854
|
const listener = (e: MouseEvent) => {
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
if (handle.closest(`[data-id="${clickConnectMode.sourceNodeId}"]`)) continue
|
|
796
|
-
const rect = handle.getBoundingClientRect()
|
|
797
|
-
const cx = rect.left + rect.width / 2
|
|
798
|
-
const cy = rect.top + rect.height / 2
|
|
799
|
-
const dist = Math.hypot(e.clientX - cx, e.clientY - cy)
|
|
800
|
-
if (dist < 36 && dist < nearestTargetHandleDistance) {
|
|
801
|
-
nearestTargetHandleDistance = dist
|
|
802
|
-
nearHandle = true
|
|
803
|
-
snapPos = { x: cx, y: cy }
|
|
804
|
-
hoveredTargetHandleId = handle.getAttribute('data-handleid') || handle.id
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
setClickConnectCursorPos(snapPos)
|
|
808
|
-
setConnectGhostPos(nearHandle ? null : { x: e.clientX, y: e.clientY })
|
|
855
|
+
const now = performance.now()
|
|
856
|
+
if (now - connectorDragLastUpdateRef.current < CONNECTOR_DRAG_UPDATE_INTERVAL_MS) return
|
|
857
|
+
connectorDragLastUpdateRef.current = now
|
|
858
|
+
const hit = findNearestHandleTargetInCache(handleTargets, e.clientX, e.clientY)
|
|
859
|
+
setClickConnectCursorPos(hit.snapPos)
|
|
860
|
+
setConnectGhostPos(hit.nearHandle ? null : { x: e.clientX, y: e.clientY })
|
|
809
861
|
setClickConnectMode((prev) => {
|
|
810
862
|
if (!prev) return null
|
|
811
863
|
let bestHandle = prev.sourceHandle
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
const cx = rect.left + rect.width / 2; const cy = rect.top + rect.height / 2
|
|
819
|
-
const dist = Math.hypot(e.clientX - cx, e.clientY - cy)
|
|
820
|
-
if (dist < minDist) { minDist = dist; bestHandle = h.getAttribute('data-handleid') || h.id }
|
|
864
|
+
let minDist = Infinity
|
|
865
|
+
for (const target of sourceHandleTargets) {
|
|
866
|
+
const dist = Math.hypot(e.clientX - target.x, e.clientY - target.y)
|
|
867
|
+
if (dist < minDist) {
|
|
868
|
+
minDist = dist
|
|
869
|
+
bestHandle = target.handleId
|
|
821
870
|
}
|
|
822
871
|
}
|
|
823
|
-
if (prev.sourceHandle !== bestHandle || prev.targetHandle !==
|
|
824
|
-
return { ...prev, sourceHandle: bestHandle, targetHandle:
|
|
872
|
+
if (prev.sourceHandle !== bestHandle || prev.targetHandle !== hit.hoveredHandleId) {
|
|
873
|
+
return { ...prev, sourceHandle: bestHandle, targetHandle: hit.hoveredHandleId }
|
|
825
874
|
}
|
|
826
875
|
return prev
|
|
827
876
|
})
|
|
@@ -930,21 +979,23 @@ export function useCanvasInteractions({
|
|
|
930
979
|
}, [])
|
|
931
980
|
|
|
932
981
|
const onMoveStart = useCallback(() => {
|
|
982
|
+
if (isMovingRef.current) return
|
|
983
|
+
isMovingRef.current = true
|
|
933
984
|
setCanvasMenu(null)
|
|
934
985
|
setConnectorLongPressMenu(null)
|
|
935
986
|
setAddingElementAt(null)
|
|
936
|
-
setRfNodes((nds) => nds.map((n) => ({ ...n, data: { ...n.data, isCanvasMoving: true } })))
|
|
937
987
|
onMoveStateChange?.(true)
|
|
938
|
-
}, [
|
|
988
|
+
}, [onMoveStateChange])
|
|
939
989
|
|
|
940
990
|
const onMove = useCallback((_: unknown, viewport: { x: number; y: number; zoom: number }) => {
|
|
941
991
|
drawingCanvasRef.current?.notifyViewportChange(viewport)
|
|
942
992
|
}, [drawingCanvasRef])
|
|
943
993
|
|
|
944
994
|
const onMoveEnd = useCallback(() => {
|
|
945
|
-
|
|
995
|
+
if (!isMovingRef.current) return
|
|
996
|
+
isMovingRef.current = false
|
|
946
997
|
onMoveStateChange?.(false)
|
|
947
|
-
}, [
|
|
998
|
+
}, [onMoveStateChange])
|
|
948
999
|
|
|
949
1000
|
// ── Touch & long-press ────────────────────────────────────────────────────
|
|
950
1001
|
function getTouchDistance(touches: Map<number, { x: number; y: number }>): number {
|