112 lines
4.5 KiB
TypeScript
112 lines
4.5 KiB
TypeScript
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
|
|
};
|
|
}
|