import { useRef, type PointerEvent as ReactPointerEvent } from "react"; import { useSchedulesStore } from "../state/schedulesStore"; import type { ScheduleNode } from "../../../services/api"; type DragState = { nodeId: string; pointerId: number; startClientX: number; startClientY: number; originX: number; originY: number; moved: boolean; }; /** * 画布节点拖拽编排。内部持有瞬时 dragRef,避免每次拖拽都触发 store 重渲染。 * 拖拽过程中直接改 schedule.nodes 的 position;松手时把 rounded 后的位置写到 store 的 * setPositionDraft 模块级 Map,刷新不丢。 */ export function useCanvasNodeDrag({ busy, linkSourceId, }: { busy: string | null | undefined; linkSourceId: string | null; }): { startDrag: (event: ReactPointerEvent, node: ScheduleNode) => void; moveDrag: (event: ReactPointerEvent) => void; finishDrag: (event: ReactPointerEvent) => void; } { const dragRef = useRef(null); const setSchedule = useSchedulesStore((s) => s.setSchedule); const setSelectedNodeId = useSchedulesStore((s) => s.setSelectedNodeId); const setSelectedEdgeId = useSchedulesStore((s) => s.setSelectedEdgeId); const startDrag = ( event: ReactPointerEvent, node: ScheduleNode, ): void => { if (event.button !== 0 || busy || linkSourceId) return; const target = event.target as HTMLElement; if (target.closest("button")) return; event.currentTarget.setPointerCapture(event.pointerId); dragRef.current = { nodeId: node.node_id, pointerId: event.pointerId, startClientX: event.clientX, startClientY: event.clientY, originX: node.position_x, originY: node.position_y, moved: false, }; setSelectedNodeId(node.node_id); setSelectedEdgeId(null); }; const moveDrag = (event: ReactPointerEvent): void => { const drag = dragRef.current; if (!drag || drag.pointerId !== event.pointerId) return; const deltaX = event.clientX - drag.startClientX; const deltaY = event.clientY - drag.startClientY; if (Math.abs(deltaX) + Math.abs(deltaY) > 3) drag.moved = true; setSchedule((current) => { if (!current) return current; return { ...current, nodes: current.nodes.map((item) => item.node_id === drag.nodeId ? { ...item, position_x: Math.max(10, drag.originX + deltaX), position_y: Math.max(10, drag.originY + deltaY), } : item, ), }; }); }; const finishDrag = (event: ReactPointerEvent): void => { const drag = dragRef.current; if (!drag || drag.pointerId !== event.pointerId) return; dragRef.current = null; if (!drag.moved) return; const current = useSchedulesStore.getState().schedule; const node = current?.nodes.find((item) => item.node_id === drag.nodeId); if (!current || !node) return; const position = { position_x: Math.round(node.position_x), position_y: Math.round(node.position_y), }; useSchedulesStore.getState().setPositionDraft(node.node_id, position); setSchedule((value) => { if (!value) return value; return { ...value, nodes: value.nodes.map((item) => item.node_id === node.node_id ? { ...item, ...position } : item, ), }; }); }; return { startDrag, moveDrag, finishDrag }; }