209 lines
5.4 KiB
TypeScript
209 lines
5.4 KiB
TypeScript
|
|
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,
|
||
|
|
}
|
||
|
|
}
|