import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { useSimulation } from '../useSimulation'; export function useCameraControls(camera: THREE.Camera, renderer: THREE.WebGLRenderer) { let controls: OrbitControls; // Camera Motion State let moveAnimating = false; let moveStart: THREE.Vector3 | null = null; let moveEnd: THREE.Vector3 | null = null; let contStart: THREE.Vector3 | null = null; let contEnd: THREE.Vector3 | null = null; let moveProgress = 0; const moveDuration = 0.2; let moveStartTime = 0; const { state } = useSimulation(); // Access global state const initControls = () => { controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = false; controls.dampingFactor = 0.05; controls.enableRotate = false; controls.enablePan = true; controls.enableZoom = false; controls.screenSpacePanning = false; // Fix: Pan on XZ plane only // Strictly match legacy controls.js controls.mouseButtons = { LEFT: THREE.MOUSE.PAN } as any; // Cast to allow partial object if TS complains, or just because we are overriding defaults controls.target.set(0, 0, 0); // Scroll Listeners controls.addEventListener('change', () => { if (moveAnimating) return; controls.target.y = 0; camera.position.y = Math.min(camera.position.y, state.value.maxZoom || 120); // This 'change' listener might fight with the zoom logic if not careful. // But let's just stick to the requested change: Zoom Out limit. }); renderer.domElement.addEventListener('wheel', (event: WheelEvent) => { event.preventDefault(); if (!moveAnimating) { const zoomAmount = state.value.zoomSensitivity || 5; const maxZoom = state.value.maxZoom || 120; // Default fallback // Calculate direction from target to camera (view vector) const dir = new THREE.Vector3().subVectors(camera.position, controls.target).normalize(); let targetPos: THREE.Vector3 | null = null; if (event.deltaY < 0) { // Zoom Out: Move away from target targetPos = camera.position.clone().add(dir.multiplyScalar(zoomAmount)); // Check Max Limit if (targetPos.y > maxZoom) { // Clamp to max zoom height, but keep angle (roughly, or just stop) // Simple stop: targetPos = null; // Or precise clamping (complex math to find point on vector where y=maxZoom): // y = y0 + t * dir.y => maxZoom = y0 + t * dir.y => t = (maxZoom - y0) / dir.y // But simply ignoring the input if it exceeds is safer/smoother for now. } } else if (event.deltaY > 0) { // Zoom In: Move towards target targetPos = camera.position.clone().sub(dir.multiplyScalar(zoomAmount)); // Check Min Limit if (targetPos.y < 10) { targetPos = null; } } if (targetPos) { moveStart = camera.position.clone(); moveEnd = targetPos; contStart = controls.target.clone(); contEnd = controls.target.clone(); // Target stays same moveProgress = 0; moveStartTime = performance.now() / 1000; moveAnimating = true; } } }, { passive: false }); }; const updateControls = () => { // Animation if (moveAnimating && moveStart && moveEnd && contStart && contEnd) { const now = performance.now() / 1000; moveProgress = Math.min((now - moveStartTime) / moveDuration, 1); camera.position.lerpVectors(moveStart, moveEnd, moveProgress); controls.target.lerpVectors(contStart, contEnd, moveProgress); if (moveProgress >= 1) { moveAnimating = false; } controls.update(); } if (controls) controls.update(); }; return { initControls, updateControls, getControls: () => controls }; }