OpenGL 与 3D精校教程
从零实现 Orbit Controller
从相机坐标系、球坐标和输入状态机出发,实现旋转、平移、缩放与惯性更新。
参考xeokit实现的控制器
math
import { Matrix4, Vector3 } from 'three';
const lookAtMat = new Matrix4()
const _vec3 = new Vector3()
export function lookAtMat4v(pos, target, up, dest = lookAtMat) {
const posx = pos.x;
const posy = pos.y;
const posz = pos.z;
const upx = up.x;
const upy = up.y;
const upz = up.z;
const targetx = target.x;
const targety = target.y;
const targetz = target.z;
if (posx === targetx && posy === targety && posz === targetz) {
return new Matrix4();
}
let z0;
let z1;
let z2;
let x0;
let x1;
let x2;
let y0;
let y1;
let y2;
let len;
//vec3.direction(eye, center, z);
z0 = posx - targetx;
z1 = posy - targety;
z2 = posz - targetz;
// normalize (no check needed for 0 because of early return)
len = 1 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2);
z0 *= len;
z1 *= len;
z2 *= len;
//vec3.normalize(vec3.cross(up, z, x));
x0 = upy * z2 - upz * z1;
x1 = upz * z0 - upx * z2;
x2 = upx * z1 - upy * z0;
len = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2);
if (!len) {
x0 = 0;
x1 = 0;
x2 = 0;
} else {
len = 1 / len;
x0 *= len;
x1 *= len;
x2 *= len;
}
y0 = z1 * x2 - z2 * x1;
y1 = z2 * x0 - z0 * x2;
y2 = z0 * x1 - z1 * x0;
len = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2);
if (!len) {
y0 = 0;
y1 = 0;
y2 = 0;
} else {
len = 1 / len;
y0 *= len;
y1 *= len;
y2 *= len;
}
const e = dest.elements;
e[0] = x0;
e[1] = y0;
e[2] = z0;
e[3] = 0;
e[4] = x1;
e[5] = y1;
e[6] = z1;
e[7] = 0;
e[8] = x2;
e[9] = y2;
e[10] = z2;
e[11] = 0;
e[12] = -(x0 * posx + x1 * posy + x2 * posz);
e[13] = -(y0 * posx + y1 * posy + y2 * posz);
e[14] = -(z0 * posx + z1 * posy + z2 * posz);
e[15] = 1;
return dest;
}
export function transformVec3(m, v, dest = _vec3) {
const v0 = v.x;
const v1 = v.y;
const v2 = v.z;
const e = m.elements;
dest.x = (e[0] * v0) + (e[4] * v1) + (e[8] * v2);
dest.y = (e[1] * v0) + (e[5] * v1) + (e[9] * v2);
dest.z = (e[2] * v0) + (e[6] * v1) + (e[10] * v2);
return dest;
}
controller
import { EventDispatcher, MOUSE, Spherical, Vector2, Vector3,Matrix3,Matrix4 } from 'three'
import { lookAtMat4v } from './exokit-math.js'
// OrbitControls performs orbiting, dollying (zooming), and panning.
// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
const _changeEvent = { type: 'change' }
const _startEvent = { type: 'start' }
const _endEvent = { type: 'end' }
const EPSILON = 0.000001
const maxElapsed = 1 / 20
const minElapsed = 1 / 60
const intersectPoint = new Vector3()
const tempVec3 = new Vector3()
const tempVec3b = new Vector3()
const tempMatrix3 = new Matrix3()
const tempDiff = new Vector3()
const tempMatrix4 = new Matrix4()
/**
* 轨道控制器 封装优化思路
* 1. 抽象出相机坐标系下的操作,
* 1.1如pan,dolly,rotate,细分出panLeft,panUp,dollyIn,dollyOut,rotateLeft,rotateUp
* 1.2 实现触摸、鼠标、快捷键分别控制细分操作
*
*/
class OrbitControls extends EventDispatcher {
constructor(object, domElement,three) {
super()
this.object = object
this.domElement = domElement
this.domElement.style.touchAction = 'none' // disable touch scroll
this.three = three
// 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
// 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.keyDollySpeed = 2.0
this.shiftSpeed = 4.0
// Set to false to disable rotating
this.enableRotate = true
this.rotateSpeed = 1.0
// Set to false to disable panning
this.enablePan = true
this.panSpeed = 1
this.keyPanSpeed = 7.0 // pixels moved per arrow key push
// 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'], ZOOM_IN: ['ArrowUp'], ZOOM_OUT: ['ArrowDown'] }
// Mouse buttons
this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.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
this.pivot = new Vector3()
//
// 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 update(deltaTime = null) {
if (scope.autoRotate && state === STATE.NONE) {
rotateLeft(getAutoRotationAngle(deltaTime))
}
scope.object.lookAt(scope.target)
}
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)
scope.domElement.removeEventListener('mousemove', handleMouseMove)
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
//panning
const panOffset = new Vector3()
const panStart = new Vector2()
const panEnd = new Vector2()
const panDelta = new Vector3()
const panTargrt = new Vector3()
//dollying
let scale = 1
const dollyStart = new Vector2()
const dollyEnd = new Vector2()
const dollyDelta = new Vector2()
const pointers = []
const pointerPositions = {}
//rotating
const cameraOffset = new Vector3() // 相机偏移量
const rotateStart = new Vector2()
const rotateEnd = new Vector2()
const rotateDelta = new Vector2()
const spherical = new Spherical()
let secsNowLast = null;
let dollyDelta1 = 0
let dollyDistFactor = 1.0
const mouseWheelDollyRate = 100
const dollyProximityThreshold = 30.0
const dollyMinSpeed = 0.04
const dollyMaxSpeed = 20
const dollyState = {
isPass:false,
changePos:true
}
let controlActive = false
camera-actions
function getAutoRotationAngle(deltaTime) {
if (deltaTime !== null) {
return ((2 * Math.PI) / 60) * scope.autoRotateSpeed * deltaTime
} else {
return ((2 * Math.PI) / 60 / 60) * scope.autoRotateSpeed
}
}
function rotateLeft(angle) {
spherical.theta -= angle * 0.6
}
function rotateUp(angle) {
spherical.phi -= angle * 0.6
}
//实际dolly的函数
function dollyToCanvasPos(optionalTargetWorldPos, targetCanvasPos, dollyDelta) {
const camera = scope.object;
let dolliedThroughSurface = false
if (optionalTargetWorldPos) {
const eyeToWorldPosVec = new Vector3().subVectors(optionalTargetWorldPos, camera.position);
const eyeWorldPosDist = eyeToWorldPosVec.length();
dolliedThroughSurface = (eyeWorldPosDist < dollyDelta);
}
if (camera.type === "PerspectiveCamera") {
const unprojectedWorldPos= _unproject(targetCanvasPos)
const offset = new Vector3().subVectors(unprojectedWorldPos, camera.position);
const moveVec = offset.clone().normalize().multiplyScalar( -dollyDelta);
camera.position.sub(moveVec)
scope.target.sub(moveVec)
//if (optionalTargetWorldPos) {
// const eyeTargetVec = new Vector3().subVectors(optionalTargetWorldPos, camera.position);
// const lenEyeTargetVec = eyeTargetVec.length();
//const eyeLookVec = eyeTargetVec.normalize().multiplyScalar(lenEyeTargetVec);
//scope.target.add(eyeLookVec)
//}
}
return dolliedThroughSurface
}
//将画布坐标转换为世界坐标
function _unproject(canvasPos, worldPos = new Vector3()) {
const camera = scope.object;
// 获取投影矩阵
const projMatrix = camera.projectionMatrix;
// 获取转置矩阵的第三行和第四行
// Three.js中矩阵是列主序4x4矩阵,存储在Float32Array中
const Pt3 = [
projMatrix.elements[2], // m20
projMatrix.elements[6], // m21
projMatrix.elements[10], // m22
projMatrix.elements[14] // m23
];
const Pt4 = [
projMatrix.elements[3], // m30
projMatrix.elements[7], // m31
projMatrix.elements[11], // m32
projMatrix.elements[15] // m33
];
const D = [0, 0, -1.0, 1];
// 计算screenZ
const screenZ = (
D[0] * Pt3[0] + D[1] * Pt3[1] + D[2] * Pt3[2] + D[3] * Pt3[3]
) / (
D[0] * Pt4[0] + D[1] * Pt4[1] + D[2] * Pt4[2] + D[3] * Pt4[3]
);
// 将画布坐标转换为标准化设备坐标 (NDC)
const ndcX = (canvasPos.x / scope.domElement.clientWidth) * 2 - 1;
const ndcY = -(canvasPos.y / scope.domElement.clientHeight) * 2 + 1;
// 创建NDC坐标
const ndcPos = new Vector3(ndcX, ndcY, screenZ);
// 创建投影矩阵和视图矩阵的逆矩阵
const projInverse = camera.projectionMatrix.clone().invert();
const viewInverse = camera.matrixWorld; // 相机的世界矩阵就是视图矩阵的逆
// 应用逆变换
ndcPos.applyMatrix4(projInverse);
ndcPos.applyMatrix4(viewInverse);
// 将结果复制到worldPos
worldPos.set(ndcPos.x, ndcPos.y, ndcPos.z);
return worldPos;
}
//
// event callbacks - update the object state
//
function handleMouseDownDolly(event) {
dollyStart.set(event.clientX, event.clientY)
}
/**
* 鼠标点击平移
* @param {*} event
*/
function handleMouseDownPan(event) {
panStart.set(event.clientX, event.clientY)
const mouseDownPicked = getIntersectPoint(event)
dollyState.changePos = false
if(mouseDownPicked){
panTargrt.copy(intersectPoint)
} else {
panTargrt.copy(scope.target)
}
}
/**
* 获取鼠标点击的射线与场景的交点
* @param {*} event
* @returns
*/
function getIntersectPoint(event) {
const intersects = scope.three.setMouseRayCaster(event.clientX, event.clientY)
if (intersects.length) {
intersectPoint.copy(intersects[0].point)
return true
}
return false
}
function genViewMat4(pos, target, up,dest=tempMatrix4){
const zAxis = new Vector3().subVectors(pos, target).normalize()
const xAxis = new Vector3().crossVectors(up, zAxis).normalize()
const yAxis = new Vector3().crossVectors(zAxis, xAxis).normalize()
//得到一个正交矩阵,这个矩阵是相机坐标系下的lookat矩阵
dest.makeBasis(xAxis, yAxis, zAxis, dest)
//求逆矩阵,得到view矩阵,但是由于正交矩阵的逆矩阵就是其转置矩阵,所以直接转置即可,避免计算逆矩阵
dest.transpose()
dest.elements[12] = -pos.dot(xAxis)
dest.elements[13] = -pos.dot(yAxis)
dest.elements[14] = -pos.dot(zAxis)
return dest
}
/**
* 鼠标点击旋转
* @param {*} event
*/
function handleMouseDownRotate(event) {
rotateStart.set(event.clientX, event.clientY)
const position = scope.object.position
const hasIntersectPoint = getIntersectPoint(event)
if(hasIntersectPoint){
scope.pivot.copy(intersectPoint)
}
// 相机的lookat矩阵,即相机的视图矩阵
let lookat = scope.object.matrixWorldInverse.clone()
// 将pivot点转换到相机坐标系下
cameraOffset.copy(scope.pivot).applyMatrix4(lookat)
// 计算相机位置到pivot点的距离
const distance = position.distanceTo(scope.pivot);
// 将距离加到相机偏移量的z轴上
cameraOffset.z += distance;
tempMatrix3.setFromMatrix4(scope.object.matrixWorld)
// 将pivot点在相机坐标系下的偏移后的坐标转换到世界坐标系下
const offset = cameraOffset.clone().applyMatrix3(tempMatrix3)
tempDiff.subVectors(position, scope.pivot).add(offset);
spherical.setFromVector3(tempDiff)
}
// 鼠标移动旋转
function handleMouseMoveRotate(event) {
rotateEnd.set(event.clientX, event.clientY)
rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed)
const element = scope.domElement
const rotateX = (2 * Math.PI * rotateDelta.x) / element.clientHeight // yes, height
const rotateY = (2 * Math.PI * rotateDelta.y) / element.clientHeight
rotateLeft(rotateX)
rotateUp(rotateY)
// 确保球面坐标系中的角度在安全范围内
spherical.makeSafe()
// pos是pivotPos的坐标
const pos = new Vector3().setFromSpherical(spherical);
// 计算当前相机视点(eye)到观察点(look)的距离,这个距离在旋转过程中需要保持不变
const eyeLookLen = scope.object.position.distanceTo(scope.target);
// 获取当前的pivot点位置(旋转中心)
const pivotPos = scope.pivot;
// 将之前计算的球面坐标pos(相对于pivot点的偏移)加上pivot点的位置,得到新的相机位置
pos.add(pivotPos);
// 使用新的相机位置(pos)、pivot点位置和世界坐标系的up向量创建视图矩阵
// 这个矩阵定义了相机的新姿态
let lookat = lookAtMat4v(pos, pivotPos, new Vector3(0, 1, 0));
// 将视图矩阵求逆,得到相机的世界变换矩阵
// 这个矩阵包含了相机的位置和方向信息
lookat.invert()
// 将相机偏移量从相机空间转换到世界空间
const offset = cameraOffset.clone().applyMatrix3(tempMatrix3.setFromMatrix4(lookat))
// 应用偏移量,调整相机的最终位置
lookat.setPosition(lookat.elements[12] - offset.x, lookat.elements[13] - offset.y, lookat.elements[14] - offset.z)
// 从lookat矩阵中提取相机的前向方向(z轴),也就是相机空间的z轴,在世界空间下的表示
const zAxis = new Vector3().setFromMatrixColumn(lookat, 2).normalize()
// 设置新的相机位置(position)
const position = new Vector3().setFromMatrixPosition(lookat)
scope.object.position.copy(position);
// 计算新的观察点(look)位置
// 通过从相机位置沿着视线方向(zAxis)前进eyeLookLen距离
scope.target.subVectors(scope.object.position, zAxis.multiplyScalar(eyeLookLen));
// 设置相机的up向量
scope.object.up.set(lookat.elements[4], lookat.elements[5], lookat.elements[6])
rotateStart.set(event.clientX, event.clientY)
}
function handleMouseMoveDolly(event) {
dollyEnd.set(event.clientX, event.clientY)
dollyDelta.subVectors(dollyEnd, dollyStart)
if (dollyDelta.y > 0) {
calculatePanDelta(-1,event)
} else if (dollyDelta.y < 0) {
calculatePanDelta(1,event)
}
dollyStart.copy(dollyEnd)
}
/**
* 新的pan函数,平移
* @param {*} pan
*/
function newPan(pan) {
const eye2 = tempVec3.subVectors(scope.object.position, scope.target);
const vec = new Vector3()
if (pan.x !== 0) {
const left = tempVec3b.crossVectors(eye2.normalize(), scope.object.up.normalize());
vec.add(left.multiplyScalar(pan.x))
}
if (pan.y !== 0) {
vec.add(tempVec3b.copy(scope.object.up).multiplyScalar(pan.y))
}
if (pan.z !== 0) {
vec.add(tempVec3b.copy(eye2).multiplyScalar(pan.z))
}
scope.object.position.add(vec)
scope.target.add(vec)
}
function panLeftUp(panOffset) {
const panDepth = scope.object.position.distanceTo(panTargrt)
const targetDistance = panDepth * Math.tan((scope.object.fov / 2) * Math.PI / 180.0);
panDelta.x += (panOffset.x * targetDistance / scope.domElement.clientHeight);
panDelta.y += ( panOffset.y * targetDistance / scope.domElement.clientHeight);
if (Math.abs(panDelta.x) < EPSILON) {
panDelta.x = 0;
}
if (Math.abs(panDelta.y) < EPSILON) {
panDelta.y = 0;
}
if (Math.abs(panDelta.z) < EPSILON) {
panDelta.z = 0;
}
newPan(panDelta)
panDelta.set(0,0,0)
}
// 鼠标移动平移
function handleMouseMovePan(event) {
panEnd.set(event.clientX, event.clientY)
panOffset.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed)
panOffset.z = 0
panLeftUp(panOffset)
panStart.set(event.clientX, event.clientY)
}
// 响应dolly操作,delta为放大或缩小
function calculatePanDelta(delta,event){
const secsNow = performance.now() / 1000.0;
let secsElapsed = (secsNowLast !== null) ? (secsNow - secsNowLast) : 0;
secsNowLast = secsNow;
if (secsElapsed > maxElapsed) {
secsElapsed = maxElapsed;
}
if (secsElapsed < minElapsed) {
secsElapsed = minElapsed;
}
if (Math.abs(dollyDelta1) < EPSILON) {
dollyDelta1 = 0;
}
if (delta === 0) {
return;
}
const normalizedDelta = delta / Math.abs(delta);
dollyDelta1 += -normalizedDelta * secsElapsed * mouseWheelDollyRate;
let target = null
if(dollyState.changePos ){
const hasIntersectPoint = getIntersectPoint(event)
if(hasIntersectPoint){
target = intersectPoint
dollyState.changePos = false
} else {
dollyDistFactor = 1.0
target = null
}
}
if(target){
const dist = Math.abs(scope.object.position.distanceTo(target));
dollyDistFactor = dist / dollyProximityThreshold;
}
dollyDistFactor = Math.max(Math.min(dollyDistFactor, dollyMaxSpeed), dollyMinSpeed)
let dollyDelta = dollyDelta1 * dollyDistFactor
const isPass = dollyToCanvasPos(target, new Vector2(event.clientX, event.clientY), -dollyDelta)
if(isPass){
dollyState.changePos = true
}
dollyDelta1 = 0
}
function handleMouseWheel(event) {
//放大delta >0 缩小delta <0
const delta = Math.max(-1, Math.min(1, -event.deltaY * 40));
calculatePanDelta(delta,event)
}
function handleKeyDown(event) {
let needsUpdate = false
let speed = 1
if(event.shiftKey){
//加速
speed *= scope.shiftSpeed
}
const filterRes = Object.entries(scope.keys).filter(([_, value]) => value.includes(event.code))
if (filterRes.length) {
const [aspect] = filterRes[0]
switch(aspect){
case 'UP':
panLeftUp(new Vector3(0, -speed*scope.keyPanSpeed, 0))
break
case 'BOTTOM':
panLeftUp(new Vector3(0, speed*scope.keyPanSpeed, 0))
break
case 'LEFT':
panLeftUp(new Vector3(-speed*scope.keyPanSpeed,0, 0))
break
case 'RIGHT':
panLeftUp(new Vector3(speed*scope.keyPanSpeed,0, 0))
break
case 'ZOOM_IN':
dollyToCanvasPos(null, new Vector2(scope.domElement.clientWidth/2, scope.domElement.clientHeight/2), speed*scope.keyDollySpeed)
break
case 'ZOOM_OUT':
dollyToCanvasPos(null, new Vector2(scope.domElement.clientWidth/2, scope.domElement.clientHeight/2), -speed*scope.keyDollySpeed)
break
}
needsUpdate = true
}
if (needsUpdate) {
// prevent the browser from scrolling on cursor keys
event.preventDefault()
}
}
//
// event handlers - FSM: listen for events and reset state
//
input-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') {
} else {
onMouseDown(event)
}
}
function onPointerMove(event) {
dollyState.changePos = true
if (scope.enabled === false) return
if (event.pointerType === 'touch') {
} else {
onMouseMove(event)
}
}
function onPointerUp(event) {
removePointer(event)
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:
break
}
}
function onMouseDown(event) {
let mouseAction
dollyState.changePos = true
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))
// handleMouseWheel(event)
scope.dispatchEvent(_endEvent)
}
function handleMouseMove(event){
dollyState.changePos = true
}
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 || scope.enablePan === false || event.target.nodeName == 'INPUT') return
handleKeyDown(event)
}
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
}
//
scope.domElement.addEventListener('contextmenu', onContextMenu)
scope.domElement.addEventListener('mousemove', handleMouseMove)
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 default OrbitControls