import type { ProjectionMode } from './config.js'; type CameraControlsCallbacks = { hideControls: () => void; isEnabled: () => boolean; showControls: () => void; }; const MOUSE_SENSITIVITY = 0.002; const TOUCH_SENSITIVITY = 0.003; const MOMENTUM_DAMPING = 0.8; const MAX_PITCH = Math.PI * (45 / 180); const MAX_YAW = Math.PI * (45 / 180); export class FallbackCameraControls { private camera: any; private readonly callbacks: CameraControlsCallbacks; private cameraRotation = { yaw: 0, pitch: 0 }; private cameraVelocity = { yaw: 0, pitch: 0 }; private dragging = false; private lastMouseX = 0; private lastMouseY = 0; private lastTouchX = 0; private lastTouchY = 0; constructor(camera: any, callbacks: CameraControlsCallbacks) { this.camera = camera; this.callbacks = callbacks; } get isDragging(): boolean { return this.dragging; } reset(): void { this.cameraRotation = { yaw: 0, pitch: 0 }; this.cameraVelocity = { yaw: 0, pitch: 0 }; this.dragging = false; if (this.camera) { this.camera.rotation.set(0, 0, 0); } } updateCameraRotation(): void { if (!this.camera) return; this.cameraRotation.yaw += this.cameraVelocity.yaw; this.cameraRotation.pitch += this.cameraVelocity.pitch; this.cameraRotation.pitch = Math.max(-MAX_PITCH, Math.min(MAX_PITCH, this.cameraRotation.pitch)); this.cameraRotation.yaw = Math.max(-MAX_YAW, Math.min(MAX_YAW, this.cameraRotation.yaw)); this.cameraVelocity.yaw *= MOMENTUM_DAMPING; this.cameraVelocity.pitch *= MOMENTUM_DAMPING; if (Math.abs(this.cameraVelocity.yaw) < 0.001) this.cameraVelocity.yaw = 0; if (Math.abs(this.cameraVelocity.pitch) < 0.001) this.cameraVelocity.pitch = 0; this.camera.rotation.set(this.cameraRotation.pitch, this.cameraRotation.yaw, 0); } addEventListeners(canvas: HTMLElement, projectionMode: ProjectionMode): void { canvas.addEventListener('mousemove', this.onCanvasMouseMove); if (projectionMode === 'vr180') { canvas.addEventListener('mousedown', this.onMouseDown); canvas.addEventListener('mousemove', this.onMouseMove); canvas.addEventListener('mouseup', this.onMouseUp); canvas.addEventListener('touchstart', this.onTouchStart, { passive: false }); canvas.addEventListener('touchmove', this.onTouchMove, { passive: false }); canvas.addEventListener('touchend', this.onTouchEnd, { passive: false }); } else { canvas.addEventListener('touchstart', this.onCanvasTouchStart, { passive: true }); } } removeEventListeners(canvas: HTMLElement): void { canvas.removeEventListener('mousemove', this.onCanvasMouseMove); canvas.removeEventListener('mousedown', this.onMouseDown); canvas.removeEventListener('mousemove', this.onMouseMove); canvas.removeEventListener('mouseup', this.onMouseUp); canvas.removeEventListener('touchstart', this.onTouchStart); canvas.removeEventListener('touchmove', this.onTouchMove); canvas.removeEventListener('touchend', this.onTouchEnd); canvas.removeEventListener('touchstart', this.onCanvasTouchStart); } private readonly onCanvasMouseMove = (): void => { if (this.callbacks.isEnabled() && !this.dragging) { this.callbacks.showControls(); } }; private readonly onCanvasTouchStart = (): void => { if (this.callbacks.isEnabled()) { this.callbacks.showControls(); } }; private readonly onMouseDown = (event: MouseEvent): void => { if (!this.callbacks.isEnabled()) return; this.dragging = true; this.lastMouseX = event.clientX; this.lastMouseY = event.clientY; this.cameraVelocity.yaw = 0; this.cameraVelocity.pitch = 0; this.callbacks.hideControls(); }; private readonly onMouseMove = (event: MouseEvent): void => { if (!this.callbacks.isEnabled() || !this.dragging) return; const deltaX = event.clientX - this.lastMouseX; const deltaY = event.clientY - this.lastMouseY; this.cameraVelocity.yaw = deltaX * MOUSE_SENSITIVITY; this.cameraVelocity.pitch = deltaY * MOUSE_SENSITIVITY; this.lastMouseX = event.clientX; this.lastMouseY = event.clientY; }; private readonly onMouseUp = (): void => { if (!this.callbacks.isEnabled()) return; this.dragging = false; this.callbacks.showControls(); }; private readonly onTouchStart = (event: TouchEvent): void => { if (!this.callbacks.isEnabled()) return; event.preventDefault(); if (event.touches.length === 1) { this.dragging = true; this.lastTouchX = event.touches[0].clientX; this.lastTouchY = event.touches[0].clientY; this.cameraVelocity.yaw = 0; this.cameraVelocity.pitch = 0; this.callbacks.hideControls(); } }; private readonly onTouchMove = (event: TouchEvent): void => { if (!this.callbacks.isEnabled() || !this.dragging) return; event.preventDefault(); if (event.touches.length === 1) { const deltaX = event.touches[0].clientX - this.lastTouchX; const deltaY = event.touches[0].clientY - this.lastTouchY; this.cameraVelocity.yaw = deltaX * TOUCH_SENSITIVITY; this.cameraVelocity.pitch = deltaY * TOUCH_SENSITIVITY; this.lastTouchX = event.touches[0].clientX; this.lastTouchY = event.touches[0].clientY; } }; private readonly onTouchEnd = (event: TouchEvent): void => { if (!this.callbacks.isEnabled()) return; event.preventDefault(); this.dragging = false; this.callbacks.showControls(); }; }