import { useKeyboardEvent, useRafState } from '@react-hookz/web'; import { useThree } from '@react-three/fiber'; import type { ReactElement } from 'react'; import { useCallback, useState } from 'react'; import type { Vector2 } from 'three'; import { useAxisSystemContext } from '../vis/shared/AxisSystemProvider'; import { useCanvasEvents, useInteraction, useModifierKeyPressed, } from './hooks'; import type { CanvasEvent, CommonInteractionProps, Selection } from './models'; import { MouseButton } from './models'; import { boundPointToFOV, getModifierKeyArray } from './utils'; interface Props extends CommonInteractionProps { id?: string; onSelectionStart?: () => void; onSelectionChange?: (points: Selection) => void; onSelectionEnd?: (points: Selection) => void; children: (points: Selection) => ReactElement; } // eslint-disable-next-line sonarjs/cognitive-complexity function SelectionTool(props: Props) { const { id = 'Selection', modifierKey, disabled, onSelectionStart, onSelectionChange, onSelectionEnd, children, } = props; const camera = useThree((state) => state.camera); const { dataToWorld, worldToData } = useAxisSystemContext(); const modifierKeys = getModifierKeyArray(modifierKey); const shouldInteract = useInteraction(id, { button: MouseButton.Left, modifierKeys, disabled, }); const [startPoint, setStartPoint] = useState(); const [endPoint, setEndPoint] = useRafState(undefined); const isModifierKeyPressed = useModifierKeyPressed(modifierKeys); const onPointerDown = useCallback( (evt: CanvasEvent) => { const { dataPt, sourceEvent } = evt; if (!shouldInteract(sourceEvent)) { return; } const { target, pointerId } = sourceEvent; (target as Element).setPointerCapture(pointerId); setStartPoint(dataPt); if (onSelectionStart) { onSelectionStart(); } }, [onSelectionStart, shouldInteract] ); const onPointerMove = useCallback( (evt: CanvasEvent) => { if (!startPoint) { return; } const { worldPt } = evt; const dataEndPoint = worldToData(boundPointToFOV(worldPt, camera)); setEndPoint(dataEndPoint); const worldEndPoint = dataToWorld(dataEndPoint); const worldStartPoint = dataToWorld(startPoint); if (onSelectionChange && isModifierKeyPressed) { onSelectionChange({ startPoint, endPoint: dataEndPoint, worldStartPoint, worldEndPoint, }); } }, [ startPoint, worldToData, camera, setEndPoint, dataToWorld, onSelectionChange, isModifierKeyPressed, ] ); const onPointerUp = useCallback( (evt: CanvasEvent) => { if (!startPoint) { return; } const { sourceEvent, worldPt } = evt; const { target, pointerId } = sourceEvent; (target as Element).releasePointerCapture(pointerId); setStartPoint(undefined); setEndPoint(undefined); if (onSelectionEnd && shouldInteract(sourceEvent)) { const dataEndPoint = worldToData(boundPointToFOV(worldPt, camera)); const worldEndPoint = dataToWorld(dataEndPoint); const worldStartPoint = dataToWorld(startPoint); onSelectionEnd({ startPoint, endPoint: dataEndPoint, worldStartPoint, worldEndPoint, }); } }, [ startPoint, setEndPoint, onSelectionEnd, shouldInteract, worldToData, camera, dataToWorld, ] ); useCanvasEvents({ onPointerDown, onPointerMove, onPointerUp }); useKeyboardEvent( 'Escape', () => { setStartPoint(undefined); setEndPoint(undefined); }, [], { event: 'keydown' } ); if (!startPoint || !endPoint || !isModifierKeyPressed) { return null; } const worldEndPoint = dataToWorld(endPoint); const worldStartPoint = dataToWorld(startPoint); return children({ startPoint, endPoint, worldStartPoint, worldEndPoint, }); } export type { Props as SelectionProps }; export { SelectionTool as default };