mattercontrol/src/composables/useCameraControls.ts
2026-01-29 23:54:34 -08:00

208 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,
}
}