import * as THREE from 'three' export interface CameraControls { enabled: boolean target: THREE.Vector3 dampingFactor: number object: THREE.Camera update(): void dispose(): void } export function createCameraControls( camera: THREE.Camera, domElement: HTMLElement ): CameraControls { // State let enabled = false const target = new THREE.Vector3() let dampingFactor = 0.05 // Internal state let isDragging = false let dragButton = -1 const pointerStart = new THREE.Vector2() const pointerCurrent = new THREE.Vector2() // Spherical coordinates for orbit const spherical = new THREE.Spherical() const sphericalDelta = new THREE.Spherical() // Pan offset const panOffset = new THREE.Vector3() // Damping velocities const orbitVelocity = new THREE.Spherical() const panVelocity = new THREE.Vector3() // Temp vectors const tempVec = new THREE.Vector3() // Sensitivity const rotateSpeed = 0.005 const panSpeed = 0.002 function getSphericalFromCamera() { tempVec.copy(camera.position).sub(target) spherical.setFromVector3(tempVec) } function applySpericalToCamera() { tempVec.setFromSpherical(spherical) camera.position.copy(target).add(tempVec) camera.lookAt(target) } function onPointerDown(event: PointerEvent) { if (!enabled) return isDragging = true dragButton = event.button pointerStart.set(event.clientX, event.clientY) pointerCurrent.copy(pointerStart) // Capture pointer for tracking outside element domElement.setPointerCapture(event.pointerId) } function onPointerMove(event: PointerEvent) { if (!enabled || !isDragging) return pointerCurrent.set(event.clientX, event.clientY) const deltaX = pointerCurrent.x - pointerStart.x const deltaY = pointerCurrent.y - pointerStart.y if (dragButton === 0) { // Left button: orbit sphericalDelta.theta = -deltaX * rotateSpeed sphericalDelta.phi = -deltaY * rotateSpeed // Store velocity for damping orbitVelocity.theta = sphericalDelta.theta orbitVelocity.phi = sphericalDelta.phi } else { // Right/middle button: pan panInScreenSpace(deltaX, deltaY) } pointerStart.copy(pointerCurrent) } function onPointerUp(event: PointerEvent) { if (!enabled) return isDragging = false dragButton = -1 // Release pointer capture domElement.releasePointerCapture(event.pointerId) } function panInScreenSpace(deltaX: number, deltaY: number) { // Get camera's local axes const right = new THREE.Vector3() const up = new THREE.Vector3() // Extract right and up vectors from camera's world matrix camera.matrixWorld.extractBasis(right, up, tempVec) // Calculate distance to target for consistent pan speed const distance = camera.position.distanceTo(target) const panFactor = distance * panSpeed // Pan in screen space panOffset.set(0, 0, 0) panOffset.addScaledVector(right, -deltaX * panFactor) panOffset.addScaledVector(up, deltaY * panFactor) // Store velocity for damping panVelocity.copy(panOffset) } function update() { // Get current spherical from camera position getSphericalFromCamera() if (isDragging) { // Apply orbit delta spherical.theta += sphericalDelta.theta spherical.phi += sphericalDelta.phi // Clamp phi to avoid flipping spherical.phi = Math.max(0.01, Math.min(Math.PI - 0.01, spherical.phi)) // Apply pan target.add(panOffset) camera.position.add(panOffset) // Reset deltas sphericalDelta.set(0, 0, 0) panOffset.set(0, 0, 0) } else { // Apply damping when not dragging if (Math.abs(orbitVelocity.theta) > 0.0001 || Math.abs(orbitVelocity.phi) > 0.0001) { spherical.theta += orbitVelocity.theta spherical.phi += orbitVelocity.phi spherical.phi = Math.max(0.01, Math.min(Math.PI - 0.01, spherical.phi)) orbitVelocity.theta *= 1 - dampingFactor orbitVelocity.phi *= 1 - dampingFactor } if (panVelocity.lengthSq() > 0.0000001) { target.add(panVelocity) camera.position.add(panVelocity) panVelocity.multiplyScalar(1 - dampingFactor) } } // Apply spherical to camera applySpericalToCamera() } function dispose() { domElement.removeEventListener('pointerdown', onPointerDown) domElement.removeEventListener('pointermove', onPointerMove) domElement.removeEventListener('pointerup', onPointerUp) domElement.removeEventListener('pointercancel', onPointerUp) } // Setup event listeners domElement.addEventListener('pointerdown', onPointerDown) domElement.addEventListener('pointermove', onPointerMove) domElement.addEventListener('pointerup', onPointerUp) domElement.addEventListener('pointercancel', onPointerUp) // Return controls object with getters/setters return { get enabled() { return enabled }, set enabled(value: boolean) { enabled = value if (!value) { // Stop any ongoing drag isDragging = false dragButton = -1 } }, target, get dampingFactor() { return dampingFactor }, set dampingFactor(value: number) { dampingFactor = value }, get object() { return camera }, set object(value: THREE.Camera) { // Update camera reference (for ortho/perspective switching) ;(camera as THREE.Camera) = value }, update, dispose, } }