import { useCallback, useEffect, useRef } from "react"; import { useFrame, useThree } from "@react-three/fiber"; import type { RapierRigidBody } from "@react-three/rapier"; import * as THREE from "three"; import type GUI from "lil-gui"; import type { RefObject } from "react"; import { INTERACTION_DEBUG_SPHERE_COLOR, INTERACTION_DEBUG_SPHERE_OPACITY, INTERACTION_DEBUG_SPHERE_SEGMENTS, } from "@/data/debug/debugConfig"; import { Debug } from "@/utils/debug/Debug"; import { useDebugFolder } from "@/hooks/debug/useDebugFolder"; import { InteractionManager } from "@/managers/InteractionManager"; import { INTERACTION_RADIUS } from "@/data/interaction/interactionConfig"; import type { Vector3Tuple } from "@/types/three"; import type { InteractableHandle } from "@/types/interaction"; interface InteractableObjectBaseProps { label: string; position: Vector3Tuple; bodyRef?: RefObject; onPress: () => void; children: React.ReactNode; } interface TriggerInteractableObjectProps extends InteractableObjectBaseProps { kind: "trigger"; } interface GrabInteractableObjectProps extends InteractableObjectBaseProps { kind: "grab"; onRelease: () => void; } type InteractableObjectProps = | TriggerInteractableObjectProps | GrabInteractableObjectProps; const _cameraPos = new THREE.Vector3(); const _cameraDir = new THREE.Vector3(); const _objectPos = new THREE.Vector3(); const _raycaster = new THREE.Raycaster(); function createInteractableHandle( props: InteractableObjectProps, ): InteractableHandle { if (props.kind === "grab") { return { kind: props.kind, label: props.label, onPress: props.onPress, onRelease: props.onRelease, }; } return { kind: props.kind, label: props.label, onPress: props.onPress, }; } export function InteractableObject( props: InteractableObjectProps, ): React.JSX.Element { const { kind, label, position, bodyRef, onPress, children } = props; const onRelease = props.kind === "grab" ? props.onRelease : null; const camera = useThree((state) => state.camera); const groupRef = useRef(null); const debugSphereRef = useRef(null); const handle = useRef(createInteractableHandle(props)); useEffect(() => { const currentHandle = handle.current; if (currentHandle.kind === kind) { currentHandle.label = label; currentHandle.onPress = onPress; if (currentHandle.kind === "grab") { if (!onRelease) return; currentHandle.onRelease = onRelease; } return; } if (kind === "grab") { if (!onRelease) return; handle.current = { kind, label, onPress, onRelease }; } else { handle.current = { kind, label, onPress }; } const manager = InteractionManager.getInstance(); if (manager.getState().focused === currentHandle) { manager.setFocused(handle.current); } }, [kind, label, onPress, onRelease]); const setupInteractionDebugFolder = useCallback((folder: GUI) => { folder .add({ radius: INTERACTION_RADIUS }, "radius") .name("Interaction radius") .disable(); }, []); useDebugFolder("Interaction", setupInteractionDebugFolder); useFrame(() => { const group = groupRef.current; const debug = Debug.getInstance(); const manager = InteractionManager.getInstance(); if (debugSphereRef.current) { debugSphereRef.current.visible = debug.active && debug.getShowInteractionSpheres(); } if (bodyRef?.current) { const t = bodyRef.current.translation(); _objectPos.set(t.x, t.y, t.z); } else { _objectPos.set(...position); } camera.getWorldPosition(_cameraPos); const dist = _cameraPos.distanceTo(_objectPos); if (dist > INTERACTION_RADIUS) { if (manager.getState().focused === handle.current) { manager.setFocused(null); } return; } camera.getWorldDirection(_cameraDir); _raycaster.set(_cameraPos, _cameraDir); _raycaster.far = INTERACTION_RADIUS; const hits = group ? _raycaster.intersectObject(group, true) : []; const validHit = hits.find((h) => h.object !== debugSphereRef.current); if (validHit) { manager.setFocused(handle.current); } else if (manager.getState().focused === handle.current) { manager.setFocused(null); } }); return ( {children} ); }