/** * @file Hook for handling node drag interactions. */ import * as React from "react"; import { flushSync } from "react-dom"; import { useCanvasInteractionActions, useCanvasInteractionDragState } from "../../../contexts/composed/canvas/interaction/context"; import { useNodeCanvasGridSettings, useNodeCanvasViewportScale } from "../../../contexts/composed/canvas/viewport/context"; import { useNodeDefinitionList } from "../../../contexts/node-definitions/hooks/useNodeDefinitionList"; import { useNodeEditorApi } from "../../../contexts/composed/node-editor/context"; import { snapMultipleToGrid } from "../../../contexts/composed/node-editor/utils/gridSnap"; import { calculateNewPositions, handleGroupMovement } from "../../../contexts/composed/node-editor/utils/nodeDragHelpers"; import type { UseGroupManagementResult } from "../../../contexts/composed/node-editor/hooks/useGroupManagement"; import { useRafThrottledCallback } from "../../../hooks/useRafThrottledCallback"; import type { DragState, Position } from "../../../types/core"; import { useLatestRef } from "../../../hooks/useLatestRef"; export const useNodeLayerDrag = (moveGroupWithChildren: UseGroupManagementResult["moveGroupWithChildren"]) => { const dragState = useCanvasInteractionDragState(); const { actions: interactionActions } = useCanvasInteractionActions(); const { actions: nodeEditorActions, getState: getNodeEditorState } = useNodeEditorApi(); const viewportScale = useNodeCanvasViewportScale(); const gridSettings = useNodeCanvasGridSettings(); const nodeDefinitions = useNodeDefinitionList(); const dragStateRef = useLatestRef(dragState); const viewportScaleRef = useLatestRef(viewportScale); const gridSettingsRef = useLatestRef(gridSettings); const getNodeEditorStateRef = useLatestRef(getNodeEditorState); const nodeDefinitionsRef = useLatestRef(nodeDefinitions); const moveGroupWithChildrenRef = useLatestRef(moveGroupWithChildren); const nodeEditorActionsRef = useLatestRef(nodeEditorActions); const interactionActionsRef = useLatestRef(interactionActions); const updateDragOffset = React.useCallback((offset: Position) => { interactionActionsRef.current.updateNodeDrag(offset); }, []); const { schedule: scheduleDragOffset, cancel: cancelDragOffset } = useRafThrottledCallback(updateDragOffset); const scheduleDragOffsetRef = useLatestRef(scheduleDragOffset); const cancelDragOffsetRef = useLatestRef(cancelDragOffset); const handlePointerMove = React.useCallback((event: PointerEvent) => { const dragState = dragStateRef.current; if (!dragState) { return; } const scale = viewportScaleRef.current; const deltaX = (event.clientX - dragState.startPosition.x) / scale; const deltaY = (event.clientY - dragState.startPosition.y) / scale; scheduleDragOffsetRef.current({ x: deltaX, y: deltaY }); }, []); const handlePointerUp = React.useCallback(() => { const dragState = dragStateRef.current; if (!dragState) { return; } cancelDragOffsetRef.current(); const { nodeIds, initialPositions, offset } = dragState; const newPositions = calculateNewPositions(nodeIds, initialPositions, offset); const currentGridSettings = gridSettingsRef.current; const snappedPositions = currentGridSettings.snapToGrid ? snapMultipleToGrid(newPositions, currentGridSettings, nodeIds[0]) : newPositions; const finalPositions = handleGroupMovement( nodeIds, getNodeEditorStateRef.current().nodes, snappedPositions, initialPositions, moveGroupWithChildrenRef.current, nodeDefinitionsRef.current, ); flushSync(() => { if (Object.keys(finalPositions).length > 0) { nodeEditorActionsRef.current.moveNodes(finalPositions); } interactionActionsRef.current.endNodeDrag(); }); }, []); React.useEffect(() => { if (!dragState) { return; } window.addEventListener("pointermove", handlePointerMove, { passive: true }); window.addEventListener("pointerup", handlePointerUp, { once: true }); return () => { cancelDragOffsetRef.current(); window.removeEventListener("pointermove", handlePointerMove); window.removeEventListener("pointerup", handlePointerUp); }; }, [dragState ? true : false, handlePointerMove, handlePointerUp]); };