Files
manyfold/app/javascript/src/orbit_controls.js
T

1251 lines
34 KiB
JavaScript

import {
EventDispatcher,
MOUSE,
Quaternion,
Spherical,
TOUCH,
Vector2,
Vector3,
Plane,
Ray,
MathUtils
} from 'three'
// OrbitControls performs orbiting, dollying (zooming), and panning.
// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
//
// Orbit - left mouse / touch: one-finger move
// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
// Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move
const _changeEvent = { type: 'change' }
const _startEvent = { type: 'start' }
const _endEvent = { type: 'end' }
const _ray = new Ray()
const _plane = new Plane()
const TILT_LIMIT = Math.cos(70 * MathUtils.DEG2RAD)
class OrbitControls extends EventDispatcher {
constructor (object, domElement) {
super()
this.object = object
this.domElement = domElement
this.domElement.style.touchAction = 'none' // disable touch scroll
// Set to false to disable this control
this.enabled = true
// "target" sets the location of focus, where the object orbits around
this.target = new Vector3()
// Sets the 3D cursor (similar to Blender), from which the maxTargetRadius takes effect
this.cursor = new Vector3()
// How far you can dolly in and out ( PerspectiveCamera only )
this.minDistance = 0
this.maxDistance = Infinity
// How far you can zoom in and out ( OrthographicCamera only )
this.minZoom = 0
this.maxZoom = Infinity
// Limit camera target within a spherical area around the cursor
this.minTargetRadius = 0
this.maxTargetRadius = Infinity
// How far you can orbit vertically, upper and lower limits.
// Range is 0 to Math.PI radians.
this.minPolarAngle = 0 // radians
this.maxPolarAngle = Math.PI // radians
// How far you can orbit horizontally, upper and lower limits.
// If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI )
this.minAzimuthAngle = -Infinity // radians
this.maxAzimuthAngle = Infinity // radians
// Set to true to enable damping (inertia)
// If damping is enabled, you must call controls.update() in your animation loop
this.enableDamping = false
this.dampingFactor = 0.05
// This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
// Set to false to disable zooming
this.enableZoom = true
this.zoomSpeed = 1.0
this.keyZoomSpeed = 100.0
// Set to false to disable rotating
this.enableRotate = true
this.rotateSpeed = 1.0
this.keyRotateFactor = 1.0
// Set to false to disable panning
this.enablePan = true
this.panSpeed = 1.0
this.screenSpacePanning = true // if false, pan orthogonal to world-space direction camera.up
this.keyPanSpeed = 7.0 // pixels moved per arrow key push
this.zoomToCursor = false
// Set to true to automatically rotate around the target
// If auto-rotate is enabled, you must call controls.update() in your animation loop
this.autoRotate = false
this.autoRotateSpeed = 2.0 // 30 seconds per orbit when fps is 60
// The four arrow keys
this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown', IN: 'Equal', OUT: 'Minus' }
// Mouse buttons
this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }
// Touch fingers
this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }
// for reset
this.target0 = this.target.clone()
this.position0 = this.object.position.clone()
this.zoom0 = this.object.zoom
// the target DOM element for key events
this._domElementKeyEvents = null
//
// public methods
//
this.getPolarAngle = function () {
return spherical.phi
}
this.getAzimuthalAngle = function () {
return spherical.theta
}
this.getDistance = function () {
return this.object.position.distanceTo(this.target)
}
this.listenToKeyEvents = function (domElement) {
domElement.addEventListener('keydown', onKeyDown)
this._domElementKeyEvents = domElement
}
this.stopListenToKeyEvents = function () {
this._domElementKeyEvents.removeEventListener('keydown', onKeyDown)
this._domElementKeyEvents = null
}
this.saveState = function () {
scope.target0.copy(scope.target)
scope.position0.copy(scope.object.position)
scope.zoom0 = scope.object.zoom
}
this.reset = function () {
scope.target.copy(scope.target0)
scope.object.position.copy(scope.position0)
scope.object.zoom = scope.zoom0
scope.object.updateProjectionMatrix()
scope.dispatchEvent(_changeEvent)
scope.update()
state = STATE.NONE
}
// this method is exposed, but perhaps it would be better if we can make it private...
this.update = (function () {
const offset = new Vector3()
// so camera.up is the orbit axis
const quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0))
const quatInverse = quat.clone().invert()
const lastPosition = new Vector3()
const lastQuaternion = new Quaternion()
const lastTargetPosition = new Vector3()
const twoPI = 2 * Math.PI
return function update (deltaTime = null) {
const position = scope.object.position
offset.copy(position).sub(scope.target)
// rotate offset to "y-axis-is-up" space
offset.applyQuaternion(quat)
// angle from z-axis around y-axis
spherical.setFromVector3(offset)
if (scope.autoRotate && state === STATE.NONE) {
rotateLeft(getAutoRotationAngle(deltaTime))
}
if (scope.enableDamping) {
spherical.theta += sphericalDelta.theta * scope.dampingFactor
spherical.phi += sphericalDelta.phi * scope.dampingFactor
} else {
spherical.theta += sphericalDelta.theta
spherical.phi += sphericalDelta.phi
}
// restrict theta to be between desired limits
let min = scope.minAzimuthAngle
let max = scope.maxAzimuthAngle
if (isFinite(min) && isFinite(max)) {
if (min < -Math.PI) min += twoPI; else if (min > Math.PI) min -= twoPI
if (max < -Math.PI) max += twoPI; else if (max > Math.PI) max -= twoPI
if (min <= max) {
spherical.theta = Math.max(min, Math.min(max, spherical.theta))
} else {
spherical.theta = (spherical.theta > (min + max) / 2)
? Math.max(min, spherical.theta)
: Math.min(max, spherical.theta)
}
}
// restrict phi to be between desired limits
spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi))
spherical.makeSafe()
// move target to panned location
if (scope.enableDamping === true) {
scope.target.addScaledVector(panOffset, scope.dampingFactor)
} else {
scope.target.add(panOffset)
}
// Limit the target distance from the cursor to create a sphere around the center of interest
scope.target.sub(scope.cursor)
scope.target.clampLength(scope.minTargetRadius, scope.maxTargetRadius)
scope.target.add(scope.cursor)
let zoomChanged = false
// adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera
// we adjust zoom later in these cases
if ((scope.zoomToCursor && performCursorZoom) || scope.object.isOrthographicCamera) {
spherical.radius = clampDistance(spherical.radius)
} else {
const prevRadius = spherical.radius
spherical.radius = clampDistance(spherical.radius * scale)
zoomChanged = prevRadius !== spherical.radius
}
offset.setFromSpherical(spherical)
// rotate offset back to "camera-up-vector-is-up" space
offset.applyQuaternion(quatInverse)
position.copy(scope.target).add(offset)
scope.object.lookAt(scope.target)
if (scope.enableDamping === true) {
sphericalDelta.theta *= (1 - scope.dampingFactor)
sphericalDelta.phi *= (1 - scope.dampingFactor)
panOffset.multiplyScalar(1 - scope.dampingFactor)
} else {
sphericalDelta.set(0, 0, 0)
panOffset.set(0, 0, 0)
}
// adjust camera position
if (scope.zoomToCursor && performCursorZoom) {
let newRadius = null
if (scope.object.isPerspectiveCamera) {
// move the camera down the pointer ray
// this method avoids floating point error
const prevRadius = offset.length()
newRadius = clampDistance(prevRadius * scale)
const radiusDelta = prevRadius - newRadius
scope.object.position.addScaledVector(dollyDirection, radiusDelta)
scope.object.updateMatrixWorld()
zoomChanged = !!radiusDelta
} else if (scope.object.isOrthographicCamera) {
// adjust the ortho camera position based on zoom changes
const mouseBefore = new Vector3(mouse.x, mouse.y, 0)
mouseBefore.unproject(scope.object)
const prevZoom = scope.object.zoom
scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / scale))
scope.object.updateProjectionMatrix()
zoomChanged = prevZoom !== scope.object.zoom
const mouseAfter = new Vector3(mouse.x, mouse.y, 0)
mouseAfter.unproject(scope.object)
scope.object.position.sub(mouseAfter).add(mouseBefore)
scope.object.updateMatrixWorld()
newRadius = offset.length()
} else {
console.warn('WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.')
scope.zoomToCursor = false
}
// handle the placement of the target
if (newRadius !== null) {
if (this.screenSpacePanning) {
// position the orbit target in front of the new camera position
scope.target.set(0, 0, -1)
.transformDirection(scope.object.matrix)
.multiplyScalar(newRadius)
.add(scope.object.position)
} else {
// get the ray and translation plane to compute target
_ray.origin.copy(scope.object.position)
_ray.direction.set(0, 0, -1).transformDirection(scope.object.matrix)
// if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid
// extremely large values
if (Math.abs(scope.object.up.dot(_ray.direction)) < TILT_LIMIT) {
object.lookAt(scope.target)
} else {
_plane.setFromNormalAndCoplanarPoint(scope.object.up, scope.target)
_ray.intersectPlane(_plane, scope.target)
}
}
}
} else if (scope.object.isOrthographicCamera) {
const prevZoom = scope.object.zoom
scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / scale))
if (prevZoom !== scope.object.zoom) {
scope.object.updateProjectionMatrix()
zoomChanged = true
}
}
scale = 1
performCursorZoom = false
// update condition is:
// min(camera displacement, camera rotation in radians)^2 > EPS
// using small-angle approximation cos(x/2) = 1 - x^2 / 8
if (zoomChanged ||
lastPosition.distanceToSquared(scope.object.position) > EPS ||
8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS ||
lastTargetPosition.distanceToSquared(scope.target) > EPS) {
scope.dispatchEvent(_changeEvent)
lastPosition.copy(scope.object.position)
lastQuaternion.copy(scope.object.quaternion)
lastTargetPosition.copy(scope.target)
return true
}
return false
}
}())
this.dispose = function () {
scope.domElement.removeEventListener('contextmenu', onContextMenu)
scope.domElement.removeEventListener('pointerdown', onPointerDown)
scope.domElement.removeEventListener('pointercancel', onPointerUp)
scope.domElement.removeEventListener('wheel', onMouseWheel)
scope.domElement.removeEventListener('pointermove', onPointerMove)
scope.domElement.removeEventListener('pointerup', onPointerUp)
const document = scope.domElement.getRootNode() // offscreen canvas compatibility
document.removeEventListener('keydown', interceptControlDown, { capture: true })
if (scope._domElementKeyEvents !== null) {
scope._domElementKeyEvents.removeEventListener('keydown', onKeyDown)
scope._domElementKeyEvents = null
}
// scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
}
//
// internals
//
const scope = this
const STATE = {
NONE: -1,
ROTATE: 0,
DOLLY: 1,
PAN: 2,
TOUCH_ROTATE: 3,
TOUCH_PAN: 4,
TOUCH_DOLLY_PAN: 5,
TOUCH_DOLLY_ROTATE: 6
}
let state = STATE.NONE
const EPS = 0.000001
// current position in spherical coordinates
const spherical = new Spherical()
const sphericalDelta = new Spherical()
let scale = 1
const panOffset = new Vector3()
const rotateStart = new Vector2()
const rotateEnd = new Vector2()
const rotateDelta = new Vector2()
const panStart = new Vector2()
const panEnd = new Vector2()
const panDelta = new Vector2()
const dollyStart = new Vector2()
const dollyEnd = new Vector2()
const dollyDelta = new Vector2()
const dollyDirection = new Vector3()
const mouse = new Vector2()
let performCursorZoom = false
const pointers = []
const pointerPositions = {}
let controlActive = false
function getAutoRotationAngle (deltaTime) {
if (deltaTime !== null) {
return (2 * Math.PI / 60 * scope.autoRotateSpeed) * deltaTime
} else {
return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed
}
}
function getZoomScale (delta) {
const normalizedDelta = Math.abs(delta * 0.01)
return Math.pow(0.95, scope.zoomSpeed * normalizedDelta)
}
function rotateLeft (angle) {
sphericalDelta.theta -= angle
}
function rotateUp (angle) {
sphericalDelta.phi -= angle
}
const panLeft = (function () {
const v = new Vector3()
return function panLeft (distance, objectMatrix) {
v.setFromMatrixColumn(objectMatrix, 0) // get X column of objectMatrix
v.multiplyScalar(-distance)
panOffset.add(v)
}
}())
const panUp = (function () {
const v = new Vector3()
return function panUp (distance, objectMatrix) {
if (scope.screenSpacePanning === true) {
v.setFromMatrixColumn(objectMatrix, 1)
} else {
v.setFromMatrixColumn(objectMatrix, 0)
v.crossVectors(scope.object.up, v)
}
v.multiplyScalar(distance)
panOffset.add(v)
}
}())
// deltaX and deltaY are in pixels; right and down are positive
const pan = (function () {
const offset = new Vector3()
return function pan (deltaX, deltaY) {
const element = scope.domElement
if (scope.object.isPerspectiveCamera) {
// perspective
const position = scope.object.position
offset.copy(position).sub(scope.target)
let targetDistance = offset.length()
// half of the fov is center to top of screen
targetDistance *= Math.tan((scope.object.fov / 2) * Math.PI / 180.0)
// we use only clientHeight here so aspect ratio does not distort speed
panLeft(2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix)
panUp(2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix)
} else if (scope.object.isOrthographicCamera) {
// orthographic
panLeft(deltaX * (scope.object.right - scope.object.left) / scope.object.zoom / element.clientWidth, scope.object.matrix)
panUp(deltaY * (scope.object.top - scope.object.bottom) / scope.object.zoom / element.clientHeight, scope.object.matrix)
} else {
// camera neither orthographic nor perspective
console.warn('WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.')
scope.enablePan = false
}
}
}())
function dollyOut (dollyScale) {
if (scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera) {
scale /= dollyScale
} else {
console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.')
scope.enableZoom = false
}
}
function dollyIn (dollyScale) {
if (scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera) {
scale *= dollyScale
} else {
console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.')
scope.enableZoom = false
}
}
function updateZoomParameters (x, y) {
if (!scope.zoomToCursor) {
return
}
performCursorZoom = true
const rect = scope.domElement.getBoundingClientRect()
const dx = x - rect.left
const dy = y - rect.top
const w = rect.width
const h = rect.height
mouse.x = (dx / w) * 2 - 1
mouse.y = -(dy / h) * 2 + 1
dollyDirection.set(mouse.x, mouse.y, 1).unproject(scope.object).sub(scope.object.position).normalize()
}
function clampDistance (dist) {
return Math.max(scope.minDistance, Math.min(scope.maxDistance, dist))
}
//
// event callbacks - update the object state
//
function handleMouseDownRotate (event) {
rotateStart.set(event.clientX, event.clientY)
}
function handleMouseDownDolly (event) {
updateZoomParameters(event.clientX, event.clientX)
dollyStart.set(event.clientX, event.clientY)
}
function handleMouseDownPan (event) {
panStart.set(event.clientX, event.clientY)
}
function handleMouseMoveRotate (event) {
rotateEnd.set(event.clientX, event.clientY)
rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed)
const element = scope.domElement
rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight) // yes, height
rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight)
rotateStart.copy(rotateEnd)
scope.update()
}
function handleMouseMoveDolly (event) {
dollyEnd.set(event.clientX, event.clientY)
dollyDelta.subVectors(dollyEnd, dollyStart)
if (dollyDelta.y > 0) {
dollyOut(getZoomScale(dollyDelta.y))
} else if (dollyDelta.y < 0) {
dollyIn(getZoomScale(dollyDelta.y))
}
dollyStart.copy(dollyEnd)
scope.update()
}
function handleMouseMovePan (event) {
panEnd.set(event.clientX, event.clientY)
panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed)
pan(panDelta.x, panDelta.y)
panStart.copy(panEnd)
scope.update()
}
function handleMouseWheel (event) {
updateZoomParameters(event.clientX, event.clientY)
if (event.deltaY < 0) {
dollyIn(getZoomScale(event.deltaY))
} else if (event.deltaY > 0) {
dollyOut(getZoomScale(event.deltaY))
}
scope.update()
}
function handleKeyDown (event) {
let needsUpdate = false
switch (event.code) {
case scope.keys.UP:
if (scope.enablePan && (event.ctrlKey || event.metaKey || event.shiftKey)) {
pan(0, scope.keyPanSpeed)
} else {
rotateUp(-2 * Math.PI * scope.keyRotateFactor * scope.rotateSpeed / scope.domElement.clientHeight)
}
needsUpdate = true
break
case scope.keys.BOTTOM:
if (scope.enablePan && (event.ctrlKey || event.metaKey || event.shiftKey)) {
pan(0, -scope.keyPanSpeed)
} else {
rotateUp(2 * Math.PI * scope.keyRotateFactor * scope.rotateSpeed / scope.domElement.clientHeight)
}
needsUpdate = true
break
case scope.keys.LEFT:
if (scope.enablePan && (event.ctrlKey || event.metaKey || event.shiftKey)) {
pan(scope.keyPanSpeed, 0)
} else {
rotateLeft(-2 * Math.PI * scope.keyRotateFactor * scope.rotateSpeed / scope.domElement.clientHeight)
}
needsUpdate = true
break
case scope.keys.RIGHT:
if (scope.enablePan && (event.ctrlKey || event.metaKey || event.shiftKey)) {
pan(-scope.keyPanSpeed, 0)
} else {
rotateLeft(2 * Math.PI * scope.keyRotateFactor * scope.rotateSpeed / scope.domElement.clientHeight)
}
needsUpdate = true
break
case scope.keys.IN:
if (scope.enableZoom) {
dollyIn(getZoomScale(scope.keyZoomSpeed))
needsUpdate = true
}
break
case scope.keys.OUT:
if (scope.enableZoom) {
dollyOut(getZoomScale(scope.keyZoomSpeed))
needsUpdate = true
}
break
}
if (needsUpdate) {
// prevent the browser from scrolling on cursor keys
event.preventDefault()
scope.update()
}
}
function handleTouchStartRotate (event) {
if (pointers.length === 1) {
rotateStart.set(event.pageX, event.pageY)
} else {
const position = getSecondPointerPosition(event)
const x = 0.5 * (event.pageX + position.x)
const y = 0.5 * (event.pageY + position.y)
rotateStart.set(x, y)
}
}
function handleTouchStartPan (event) {
if (pointers.length === 1) {
panStart.set(event.pageX, event.pageY)
} else {
const position = getSecondPointerPosition(event)
const x = 0.5 * (event.pageX + position.x)
const y = 0.5 * (event.pageY + position.y)
panStart.set(x, y)
}
}
function handleTouchStartDolly (event) {
const position = getSecondPointerPosition(event)
const dx = event.pageX - position.x
const dy = event.pageY - position.y
const distance = Math.sqrt(dx * dx + dy * dy)
dollyStart.set(0, distance)
}
function handleTouchStartDollyPan (event) {
if (scope.enableZoom) handleTouchStartDolly(event)
if (scope.enablePan) handleTouchStartPan(event)
}
function handleTouchStartDollyRotate (event) {
if (scope.enableZoom) handleTouchStartDolly(event)
if (scope.enableRotate) handleTouchStartRotate(event)
}
function handleTouchMoveRotate (event) {
if (pointers.length === 1) {
rotateEnd.set(event.pageX, event.pageY)
} else {
const position = getSecondPointerPosition(event)
const x = 0.5 * (event.pageX + position.x)
const y = 0.5 * (event.pageY + position.y)
rotateEnd.set(x, y)
}
rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed)
const element = scope.domElement
rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight) // yes, height
rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight)
rotateStart.copy(rotateEnd)
}
function handleTouchMovePan (event) {
if (pointers.length === 1) {
panEnd.set(event.pageX, event.pageY)
} else {
const position = getSecondPointerPosition(event)
const x = 0.5 * (event.pageX + position.x)
const y = 0.5 * (event.pageY + position.y)
panEnd.set(x, y)
}
panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed)
pan(panDelta.x, panDelta.y)
panStart.copy(panEnd)
}
function handleTouchMoveDolly (event) {
const position = getSecondPointerPosition(event)
const dx = event.pageX - position.x
const dy = event.pageY - position.y
const distance = Math.sqrt(dx * dx + dy * dy)
dollyEnd.set(0, distance)
dollyDelta.set(0, Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed))
dollyOut(dollyDelta.y)
dollyStart.copy(dollyEnd)
const centerX = (event.pageX + position.x) * 0.5
const centerY = (event.pageY + position.y) * 0.5
updateZoomParameters(centerX, centerY)
}
function handleTouchMoveDollyPan (event) {
if (scope.enableZoom) handleTouchMoveDolly(event)
if (scope.enablePan) handleTouchMovePan(event)
}
function handleTouchMoveDollyRotate (event) {
if (scope.enableZoom) handleTouchMoveDolly(event)
if (scope.enableRotate) handleTouchMoveRotate(event)
}
//
// event handlers - FSM: listen for events and reset state
//
function onPointerDown (event) {
if (scope.enabled === false) return
if (pointers.length === 0) {
scope.domElement.setPointerCapture(event.pointerId)
scope.domElement.addEventListener('pointermove', onPointerMove)
scope.domElement.addEventListener('pointerup', onPointerUp)
}
//
if (isTrackingPointer(event)) return
//
addPointer(event)
if (event.pointerType === 'touch') {
onTouchStart(event)
} else {
onMouseDown(event)
}
}
function onPointerMove (event) {
if (scope.enabled === false) return
if (event.pointerType === 'touch') {
onTouchMove(event)
} else {
onMouseMove(event)
}
}
function onPointerUp (event) {
removePointer(event)
let pointerId = null
let position = null
switch (pointers.length) {
case 0:
scope.domElement.releasePointerCapture(event.pointerId)
scope.domElement.removeEventListener('pointermove', onPointerMove)
scope.domElement.removeEventListener('pointerup', onPointerUp)
scope.dispatchEvent(_endEvent)
state = STATE.NONE
break
case 1:
pointerId = pointers[0]
position = pointerPositions[pointerId]
// minimal placeholder event - allows state correction on pointer-up
onTouchStart({ pointerId, pageX: position.x, pageY: position.y })
break
}
}
function onMouseDown (event) {
let mouseAction
switch (event.button) {
case 0:
mouseAction = scope.mouseButtons.LEFT
break
case 1:
mouseAction = scope.mouseButtons.MIDDLE
break
case 2:
mouseAction = scope.mouseButtons.RIGHT
break
default:
mouseAction = -1
}
switch (mouseAction) {
case MOUSE.DOLLY:
if (scope.enableZoom === false) return
handleMouseDownDolly(event)
state = STATE.DOLLY
break
case MOUSE.ROTATE:
if (event.ctrlKey || event.metaKey || event.shiftKey) {
if (scope.enablePan === false) return
handleMouseDownPan(event)
state = STATE.PAN
} else {
if (scope.enableRotate === false) return
handleMouseDownRotate(event)
state = STATE.ROTATE
}
break
case MOUSE.PAN:
if (event.ctrlKey || event.metaKey || event.shiftKey) {
if (scope.enableRotate === false) return
handleMouseDownRotate(event)
state = STATE.ROTATE
} else {
if (scope.enablePan === false) return
handleMouseDownPan(event)
state = STATE.PAN
}
break
default:
state = STATE.NONE
}
if (state !== STATE.NONE) {
scope.dispatchEvent(_startEvent)
}
}
function onMouseMove (event) {
switch (state) {
case STATE.ROTATE:
if (scope.enableRotate === false) return
handleMouseMoveRotate(event)
break
case STATE.DOLLY:
if (scope.enableZoom === false) return
handleMouseMoveDolly(event)
break
case STATE.PAN:
if (scope.enablePan === false) return
handleMouseMovePan(event)
break
}
}
function onMouseWheel (event) {
if (scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE) return
event.preventDefault()
scope.dispatchEvent(_startEvent)
handleMouseWheel(customWheelEvent(event))
scope.dispatchEvent(_endEvent)
}
function customWheelEvent (event) {
const mode = event.deltaMode
// minimal wheel event altered to meet delta-zoom demand
const newEvent = {
clientX: event.clientX,
clientY: event.clientY,
deltaY: event.deltaY
}
switch (mode) {
case 1: // LINE_MODE
newEvent.deltaY *= 16
break
case 2: // PAGE_MODE
newEvent.deltaY *= 100
break
}
// detect if event was triggered by pinching
if (event.ctrlKey && !controlActive) {
newEvent.deltaY *= 10
}
return newEvent
}
function interceptControlDown (event) {
if (event.key === 'Control') {
controlActive = true
const document = scope.domElement.getRootNode() // offscreen canvas compatibility
document.addEventListener('keyup', interceptControlUp, { passive: true, capture: true })
}
}
function interceptControlUp (event) {
if (event.key === 'Control') {
controlActive = false
const document = scope.domElement.getRootNode() // offscreen canvas compatibility
document.removeEventListener('keyup', interceptControlUp, { passive: true, capture: true })
}
}
function onKeyDown (event) {
if (scope.enabled === false) return
handleKeyDown(event)
}
function onTouchStart (event) {
trackPointer(event)
switch (pointers.length) {
case 1:
switch (scope.touches.ONE) {
case TOUCH.ROTATE:
if (scope.enableRotate === false) return
handleTouchStartRotate(event)
state = STATE.TOUCH_ROTATE
break
case TOUCH.PAN:
if (scope.enablePan === false) return
handleTouchStartPan(event)
state = STATE.TOUCH_PAN
break
default:
state = STATE.NONE
}
break
case 2:
switch (scope.touches.TWO) {
case TOUCH.DOLLY_PAN:
if (scope.enableZoom === false && scope.enablePan === false) return
handleTouchStartDollyPan(event)
state = STATE.TOUCH_DOLLY_PAN
break
case TOUCH.DOLLY_ROTATE:
if (scope.enableZoom === false && scope.enableRotate === false) return
handleTouchStartDollyRotate(event)
state = STATE.TOUCH_DOLLY_ROTATE
break
default:
state = STATE.NONE
}
break
default:
state = STATE.NONE
}
if (state !== STATE.NONE) {
scope.dispatchEvent(_startEvent)
}
}
function onTouchMove (event) {
trackPointer(event)
switch (state) {
case STATE.TOUCH_ROTATE:
if (scope.enableRotate === false) return
handleTouchMoveRotate(event)
scope.update()
break
case STATE.TOUCH_PAN:
if (scope.enablePan === false) return
handleTouchMovePan(event)
scope.update()
break
case STATE.TOUCH_DOLLY_PAN:
if (scope.enableZoom === false && scope.enablePan === false) return
handleTouchMoveDollyPan(event)
scope.update()
break
case STATE.TOUCH_DOLLY_ROTATE:
if (scope.enableZoom === false && scope.enableRotate === false) return
handleTouchMoveDollyRotate(event)
scope.update()
break
default:
state = STATE.NONE
}
}
function onContextMenu (event) {
if (scope.enabled === false) return
event.preventDefault()
}
function addPointer (event) {
pointers.push(event.pointerId)
}
function removePointer (event) {
delete pointerPositions[event.pointerId]
for (let i = 0; i < pointers.length; i++) {
if (pointers[i] === event.pointerId) {
pointers.splice(i, 1)
return
}
}
}
function isTrackingPointer (event) {
for (let i = 0; i < pointers.length; i++) {
if (pointers[i] === event.pointerId) return true
}
return false
}
function trackPointer (event) {
let position = pointerPositions[event.pointerId]
if (position === undefined) {
position = new Vector2()
pointerPositions[event.pointerId] = position
}
position.set(event.pageX, event.pageY)
}
function getSecondPointerPosition (event) {
const pointerId = (event.pointerId === pointers[0]) ? pointers[1] : pointers[0]
return pointerPositions[pointerId]
}
//
scope.domElement.addEventListener('contextmenu', onContextMenu)
scope.domElement.addEventListener('pointerdown', onPointerDown)
scope.domElement.addEventListener('pointercancel', onPointerUp)
scope.domElement.addEventListener('wheel', onMouseWheel, { passive: false })
const document = scope.domElement.getRootNode() // offscreen canvas compatibility
document.addEventListener('keydown', interceptControlDown, { passive: true, capture: true })
// force an update at start
this.update()
}
}
export { OrbitControls }