import { useEffect, type RefObject } from "react"; import * as THREE from "three"; import { normalizeScreenPosition } from "../normalizeScreenPosition"; const CLICK_THRESHOLD = 2; // px export type InteractionMouseEventArgs = { screenPosition: THREE.Vector2Like, position: THREE.Vector2Like, screenSize: THREE.Vector2Like, pixelSize: THREE.Vector2Like, }; export type UseInteractionOptions = { onMouseClick?: (e: InteractionMouseEventArgs) => void, onMouseMove?: (e: InteractionMouseEventArgs) => void, onCameraChange?: () => void, } export function useInteraction( targetRef: RefObject, cameraRef: RefObject, options: UseInteractionOptions, ): void { useEffect( () => { const target = targetRef.current!; const camera = cameraRef.current!; const onWheel = (e: WheelEvent) => { radius = Math.max(2, Math.min(20, radius + e.deltaY * 0.02)); updateCamera(); e.preventDefault(); }; // --- Orbit Controls (manual implementation, no OrbitControls import needed) --- let isDragging = false; let isRightDrag = false; let startX = 0, startY = 0; let lastX = 0, lastY = 0; let azimuth = -0.8, elevation = 0.45, radius = 10; let targetPoint = new THREE.Vector3(0.5, 0.5, 0); let rotationSpeed = 0.005; // prevent flip at poles const epsilon = 0.0001; function updateCamera() { const cosEl = Math.cos(elevation); const x = targetPoint.x + radius * cosEl * Math.cos(azimuth); const y = targetPoint.y + radius * cosEl * Math.sin(azimuth); const z = targetPoint.z + radius * Math.sin(elevation); camera.position.set(x, y, z) camera.lookAt(targetPoint); options.onCameraChange?.(); } updateCamera(); const onMouseDown = (e: MouseEvent) => { isDragging = true; isRightDrag = e.button === 2; startX = e.clientX; startY = e.clientY; lastX = e.clientX; lastY = e.clientY; e.preventDefault(); }; const onMouseUp = (e: MouseEvent) => { isDragging = false; if ((e.clientX - startX < CLICK_THRESHOLD) && (e.clientY - startY < CLICK_THRESHOLD)) { const screenPosition = { x: e.clientX, y: e.clientY }; options.onMouseClick?.({ ...normalizeScreenPosition(screenPosition, target), screenPosition, }); } }; const onMouseMove = (e: MouseEvent) => { if (!isDragging) return; const dx = e.clientX - lastX; const dy = e.clientY - lastY; lastX = e.clientX; lastY = e.clientY; if (isRightDrag) { targetPoint.x -= dx * 0.01; targetPoint.y += dy * 0.01; } else { azimuth = azimuth - dx * rotationSpeed; elevation = elevation + dy * rotationSpeed; elevation = Math.max(-Math.PI / 2 + epsilon, Math.min(Math.PI / 2 - epsilon, elevation + dy * rotationSpeed)); } updateCamera(); }; const onHover = (e: MouseEvent) => { const screenPosition = { x: e.clientX, y: e.clientY }; options.onMouseMove?.({ ...normalizeScreenPosition(screenPosition, target), screenPosition, }); }; const onContextMenu = (e: Event) => e.preventDefault(); target.addEventListener("mousedown", onMouseDown); target.addEventListener("contextmenu", onContextMenu); target.addEventListener("wheel", onWheel, { passive: false }); target.addEventListener("mousemove", onHover); window.addEventListener("mousemove", onMouseMove); window.addEventListener("mouseup", onMouseUp); return () => { target.removeEventListener("mousedown", onMouseDown); target.removeEventListener("contextmenu", onContextMenu); target.removeEventListener("wheel", onWheel); target.removeEventListener("mousemove", onHover); window.removeEventListener("mousemove", onMouseMove); window.removeEventListener("mouseup", onMouseUp); } }); }