three-stdlib
Version:
stand-alone library of threejs examples
1 lines • 142 kB
Source Map (JSON)
{"version":3,"file":"ArcballControls.cjs","sources":["../../src/controls/ArcballControls.ts"],"sourcesContent":["import {\n GridHelper,\n EllipseCurve,\n BufferGeometry,\n Line,\n LineBasicMaterial,\n Raycaster,\n Group,\n Box3,\n Sphere,\n Quaternion,\n Vector2,\n Vector3,\n Matrix4,\n MathUtils,\n Scene,\n PerspectiveCamera,\n OrthographicCamera,\n Mesh,\n Material,\n} from 'three'\nimport { EventDispatcher } from './EventDispatcher'\nimport { StandardControlsEventMap } from './StandardControlsEventMap'\n\ntype Camera = OrthographicCamera | PerspectiveCamera\ntype Operation = 'PAN' | 'ROTATE' | 'ZOOM' | 'FOV'\ntype MouseButtonType = number | 'WHEEL'\ntype ModifierKey = 'CTRL' | 'SHIFT'\ntype MouseAction = {\n operation: Operation\n mouse: MouseButtonType\n key: ModifierKey | null\n}\n\ntype Transformation = {\n camera: Matrix4 | null\n gizmos: Matrix4 | null\n}\n\n//trackball state\nconst STATE = {\n IDLE: Symbol(),\n ROTATE: Symbol(),\n PAN: Symbol(),\n SCALE: Symbol(),\n FOV: Symbol(),\n FOCUS: Symbol(),\n ZROTATE: Symbol(),\n TOUCH_MULTI: Symbol(),\n ANIMATION_FOCUS: Symbol(),\n ANIMATION_ROTATE: Symbol(),\n}\n\nconst INPUT = {\n NONE: Symbol(),\n ONE_FINGER: Symbol(),\n ONE_FINGER_SWITCHED: Symbol(),\n TWO_FINGER: Symbol(),\n MULT_FINGER: Symbol(),\n CURSOR: Symbol(),\n}\n\n//cursor center coordinates\nconst _center = {\n x: 0,\n y: 0,\n}\n\n//transformation matrices for gizmos and camera\nconst _transformation: Transformation = {\n camera: /* @__PURE__ */ new Matrix4(),\n gizmos: /* @__PURE__ */ new Matrix4(),\n}\n\n//events\nconst _changeEvent = { type: 'change' }\nconst _startEvent = { type: 'start' }\nconst _endEvent = { type: 'end' }\n\n/**\n *\n * @param {CamOrthographicCamera | PerspectiveCameraera} camera Virtual camera used in the scene\n * @param {HTMLElement=null} domElement Renderer's dom element\n * @param {Scene=null} scene The scene to be rendered\n */\nclass ArcballControls extends EventDispatcher<StandardControlsEventMap> {\n private camera: OrthographicCamera | PerspectiveCamera | null\n private domElement: HTMLElement | null | undefined\n private scene: Scene | null | undefined\n\n private mouseActions: (MouseAction & { state: Symbol })[]\n private _mouseOp: Operation | null\n\n private _v2_1: Vector2\n private _v3_1: Vector3\n private _v3_2: Vector3\n\n private _m4_1: Matrix4\n private _m4_2: Matrix4\n\n private _quat: Quaternion\n\n private _translationMatrix: Matrix4\n private _rotationMatrix: Matrix4\n private _scaleMatrix: Matrix4\n\n private _rotationAxis: Vector3\n\n private _cameraMatrixState: Matrix4\n private _cameraProjectionState: Matrix4\n\n private _fovState: number\n private _upState: Vector3\n private _zoomState: number\n private _nearPos: number\n private _farPos: number\n\n private _gizmoMatrixState: Matrix4\n\n private _up0: Vector3\n private _zoom0: number\n private _fov0: number\n private _initialNear: number\n private _nearPos0: number\n private _initialFar: number\n private _farPos0: number\n private _cameraMatrixState0: Matrix4\n private _gizmoMatrixState0: Matrix4\n\n private _button: MouseButtonType\n private _touchStart: PointerEvent[]\n private _touchCurrent: PointerEvent[]\n private _input: Symbol\n\n private _switchSensibility: number\n private _startFingerDistance: number\n private _currentFingerDistance: number\n private _startFingerRotation: number\n private _currentFingerRotation: number\n\n private _devPxRatio: number\n private _downValid: boolean\n private _nclicks: number\n private _downEvents: PointerEvent[]\n private _clickStart: number\n private _maxDownTime: number\n private _maxInterval: number\n private _posThreshold: number\n private _movementThreshold: number\n\n private _currentCursorPosition: Vector3\n private _startCursorPosition: Vector3\n\n private _grid: GridHelper | null\n private _gridPosition: Vector3\n\n private _gizmos: Group\n private _curvePts: number\n\n private _timeStart: number\n private _animationId: number\n\n public focusAnimationTime: number\n\n private _timePrev: number\n private _timeCurrent: number\n private _anglePrev: number\n private _angleCurrent: number\n private _cursorPosPrev: Vector3\n private _cursorPosCurr: Vector3\n private _wPrev: number\n private _wCurr: number\n\n public adjustNearFar: boolean\n public scaleFactor: number\n public dampingFactor: number\n public wMax: number\n public enableAnimations: boolean\n public enableGrid: boolean\n public cursorZoom: boolean\n public minFov: number\n public maxFov: number\n\n public enabled: boolean\n public enablePan: boolean\n public enableRotate: boolean\n public enableZoom: boolean\n\n public minDistance: number\n public maxDistance: number\n public minZoom: number\n public maxZoom: number\n\n readonly target: Vector3\n private _currentTarget: Vector3\n\n private _tbRadius: number\n\n private _state: Symbol\n\n constructor(\n camera: Camera | null,\n domElement: HTMLElement | null | undefined = null,\n scene: Scene | null | undefined = null,\n ) {\n super()\n this.camera = null\n this.domElement = domElement\n this.scene = scene\n\n this.mouseActions = []\n this._mouseOp = null\n\n //global vectors and matrices that are used in some operations to avoid creating new objects every time (e.g. every time cursor moves)\n this._v2_1 = new Vector2()\n this._v3_1 = new Vector3()\n this._v3_2 = new Vector3()\n\n this._m4_1 = new Matrix4()\n this._m4_2 = new Matrix4()\n\n this._quat = new Quaternion()\n\n //transformation matrices\n this._translationMatrix = new Matrix4() //matrix for translation operation\n this._rotationMatrix = new Matrix4() //matrix for rotation operation\n this._scaleMatrix = new Matrix4() //matrix for scaling operation\n\n this._rotationAxis = new Vector3() //axis for rotate operation\n\n //camera state\n this._cameraMatrixState = new Matrix4()\n this._cameraProjectionState = new Matrix4()\n\n this._fovState = 1\n this._upState = new Vector3()\n this._zoomState = 1\n this._nearPos = 0\n this._farPos = 0\n\n this._gizmoMatrixState = new Matrix4()\n\n //initial values\n this._up0 = new Vector3()\n this._zoom0 = 1\n this._fov0 = 0\n this._initialNear = 0\n this._nearPos0 = 0\n this._initialFar = 0\n this._farPos0 = 0\n this._cameraMatrixState0 = new Matrix4()\n this._gizmoMatrixState0 = new Matrix4()\n\n //pointers array\n this._button = -1\n this._touchStart = []\n this._touchCurrent = []\n this._input = INPUT.NONE\n\n //two fingers touch interaction\n this._switchSensibility = 32 //minimum movement to be performed to fire single pan start after the second finger has been released\n this._startFingerDistance = 0 //distance between two fingers\n this._currentFingerDistance = 0\n this._startFingerRotation = 0 //amount of rotation performed with two fingers\n this._currentFingerRotation = 0\n\n //double tap\n this._devPxRatio = 0\n this._downValid = true\n this._nclicks = 0\n this._downEvents = []\n this._clickStart = 0 //first click time\n this._maxDownTime = 250\n this._maxInterval = 300\n this._posThreshold = 24\n this._movementThreshold = 24\n\n //cursor positions\n this._currentCursorPosition = new Vector3()\n this._startCursorPosition = new Vector3()\n\n //grid\n this._grid = null //grid to be visualized during pan operation\n this._gridPosition = new Vector3()\n\n //gizmos\n this._gizmos = new Group()\n this._curvePts = 128\n\n //animations\n this._timeStart = -1 //initial time\n this._animationId = -1\n\n //focus animation\n this.focusAnimationTime = 500 //duration of focus animation in ms\n\n //rotate animation\n this._timePrev = 0 //time at which previous rotate operation has been detected\n this._timeCurrent = 0 //time at which current rotate operation has been detected\n this._anglePrev = 0 //angle of previous rotation\n this._angleCurrent = 0 //angle of current rotation\n this._cursorPosPrev = new Vector3() //cursor position when previous rotate operation has been detected\n this._cursorPosCurr = new Vector3() //cursor position when current rotate operation has been detected\n this._wPrev = 0 //angular velocity of the previous rotate operation\n this._wCurr = 0 //angular velocity of the current rotate operation\n\n //parameters\n this.adjustNearFar = false\n this.scaleFactor = 1.1 //zoom/distance multiplier\n this.dampingFactor = 25\n this.wMax = 20 //maximum angular velocity allowed\n this.enableAnimations = true //if animations should be performed\n this.enableGrid = false //if grid should be showed during pan operation\n this.cursorZoom = false //if wheel zoom should be cursor centered\n this.minFov = 5\n this.maxFov = 90\n\n this.enabled = true\n this.enablePan = true\n this.enableRotate = true\n this.enableZoom = true\n\n this.minDistance = 0\n this.maxDistance = Infinity\n this.minZoom = 0\n this.maxZoom = Infinity\n\n //trackball parameters\n this.target = new Vector3(0, 0, 0)\n this._currentTarget = new Vector3(0, 0, 0)\n\n this._tbRadius = 1\n\n //FSA\n this._state = STATE.IDLE\n\n this.setCamera(camera)\n\n if (this.scene) {\n this.scene.add(this._gizmos)\n }\n\n this._devPxRatio = window.devicePixelRatio\n\n this.initializeMouseActions()\n\n if (this.domElement) this.connect(this.domElement)\n\n window.addEventListener('resize', this.onWindowResize)\n }\n\n //listeners\n\n private onWindowResize = (): void => {\n const scale = (this._gizmos.scale.x + this._gizmos.scale.y + this._gizmos.scale.z) / 3\n if (this.camera) {\n const tbRadius = this.calculateTbRadius(this.camera)\n if (tbRadius !== undefined) {\n this._tbRadius = tbRadius\n }\n }\n\n const newRadius = this._tbRadius / scale\n // @ts-ignore\n const curve = new EllipseCurve(0, 0, newRadius, newRadius)\n const points = curve.getPoints(this._curvePts)\n const curveGeometry = new BufferGeometry().setFromPoints(points)\n\n for (const gizmo in this._gizmos.children) {\n const child = this._gizmos.children[gizmo] as Mesh\n child.geometry = curveGeometry\n }\n\n // @ts-ignore\n this.dispatchEvent(_changeEvent)\n }\n\n private onContextMenu = (event: MouseEvent): void => {\n if (!this.enabled) {\n return\n }\n\n for (let i = 0; i < this.mouseActions.length; i++) {\n if (this.mouseActions[i].mouse == 2) {\n //prevent only if button 2 is actually used\n event.preventDefault()\n break\n }\n }\n }\n\n private onPointerCancel = (): void => {\n this._touchStart.splice(0, this._touchStart.length)\n this._touchCurrent.splice(0, this._touchCurrent.length)\n this._input = INPUT.NONE\n }\n\n private onPointerDown = (event: PointerEvent): void => {\n if (event.button == 0 && event.isPrimary) {\n this._downValid = true\n this._downEvents.push(event)\n } else {\n this._downValid = false\n }\n\n if (event.pointerType == 'touch' && this._input != INPUT.CURSOR) {\n this._touchStart.push(event)\n this._touchCurrent.push(event)\n\n switch (this._input) {\n case INPUT.NONE:\n //singleStart\n this._input = INPUT.ONE_FINGER\n this.onSinglePanStart(event, 'ROTATE')\n\n window.addEventListener('pointermove', this.onPointerMove)\n window.addEventListener('pointerup', this.onPointerUp)\n\n break\n\n case INPUT.ONE_FINGER:\n case INPUT.ONE_FINGER_SWITCHED:\n //doubleStart\n this._input = INPUT.TWO_FINGER\n\n this.onRotateStart()\n this.onPinchStart()\n this.onDoublePanStart()\n\n break\n\n case INPUT.TWO_FINGER:\n //multipleStart\n this._input = INPUT.MULT_FINGER\n this.onTriplePanStart()\n break\n }\n } else if (event.pointerType != 'touch' && this._input == INPUT.NONE) {\n let modifier: ModifierKey | null = null\n\n if (event.ctrlKey || event.metaKey) {\n modifier = 'CTRL'\n } else if (event.shiftKey) {\n modifier = 'SHIFT'\n }\n\n this._mouseOp = this.getOpFromAction(event.button, modifier)\n if (this._mouseOp) {\n window.addEventListener('pointermove', this.onPointerMove)\n window.addEventListener('pointerup', this.onPointerUp)\n\n //singleStart\n this._input = INPUT.CURSOR\n this._button = event.button\n this.onSinglePanStart(event, this._mouseOp)\n }\n }\n }\n\n private onPointerMove = (event: PointerEvent): void => {\n if (event.pointerType == 'touch' && this._input != INPUT.CURSOR) {\n switch (this._input) {\n case INPUT.ONE_FINGER:\n //singleMove\n this.updateTouchEvent(event)\n\n this.onSinglePanMove(event, STATE.ROTATE)\n break\n\n case INPUT.ONE_FINGER_SWITCHED:\n const movement = this.calculatePointersDistance(this._touchCurrent[0], event) * this._devPxRatio\n\n if (movement >= this._switchSensibility) {\n //singleMove\n this._input = INPUT.ONE_FINGER\n this.updateTouchEvent(event)\n\n this.onSinglePanStart(event, 'ROTATE')\n break\n }\n\n break\n\n case INPUT.TWO_FINGER:\n //rotate/pan/pinchMove\n this.updateTouchEvent(event)\n\n this.onRotateMove()\n this.onPinchMove()\n this.onDoublePanMove()\n\n break\n\n case INPUT.MULT_FINGER:\n //multMove\n this.updateTouchEvent(event)\n\n this.onTriplePanMove()\n break\n }\n } else if (event.pointerType != 'touch' && this._input == INPUT.CURSOR) {\n let modifier: ModifierKey | null = null\n\n if (event.ctrlKey || event.metaKey) {\n modifier = 'CTRL'\n } else if (event.shiftKey) {\n modifier = 'SHIFT'\n }\n\n const mouseOpState = this.getOpStateFromAction(this._button, modifier)\n\n if (mouseOpState) {\n this.onSinglePanMove(event, mouseOpState)\n }\n }\n\n //checkDistance\n if (this._downValid) {\n const movement =\n this.calculatePointersDistance(this._downEvents[this._downEvents.length - 1], event) * this._devPxRatio\n if (movement > this._movementThreshold) {\n this._downValid = false\n }\n }\n }\n\n private onPointerUp = (event: PointerEvent): void => {\n if (event.pointerType == 'touch' && this._input != INPUT.CURSOR) {\n const nTouch = this._touchCurrent.length\n\n for (let i = 0; i < nTouch; i++) {\n if (this._touchCurrent[i].pointerId == event.pointerId) {\n this._touchCurrent.splice(i, 1)\n this._touchStart.splice(i, 1)\n break\n }\n }\n\n switch (this._input) {\n case INPUT.ONE_FINGER:\n case INPUT.ONE_FINGER_SWITCHED:\n //singleEnd\n window.removeEventListener('pointermove', this.onPointerMove)\n window.removeEventListener('pointerup', this.onPointerUp)\n\n this._input = INPUT.NONE\n this.onSinglePanEnd()\n\n break\n\n case INPUT.TWO_FINGER:\n //doubleEnd\n this.onDoublePanEnd()\n this.onPinchEnd()\n this.onRotateEnd()\n\n //switching to singleStart\n this._input = INPUT.ONE_FINGER_SWITCHED\n\n break\n\n case INPUT.MULT_FINGER:\n if (this._touchCurrent.length == 0) {\n window.removeEventListener('pointermove', this.onPointerMove)\n window.removeEventListener('pointerup', this.onPointerUp)\n\n //multCancel\n this._input = INPUT.NONE\n this.onTriplePanEnd()\n }\n\n break\n }\n } else if (event.pointerType != 'touch' && this._input == INPUT.CURSOR) {\n window.removeEventListener('pointermove', this.onPointerMove)\n window.removeEventListener('pointerup', this.onPointerUp)\n\n this._input = INPUT.NONE\n this.onSinglePanEnd()\n this._button = -1\n }\n\n if (event.isPrimary) {\n if (this._downValid) {\n const downTime = event.timeStamp - this._downEvents[this._downEvents.length - 1].timeStamp\n\n if (downTime <= this._maxDownTime) {\n if (this._nclicks == 0) {\n //first valid click detected\n this._nclicks = 1\n this._clickStart = performance.now()\n } else {\n const clickInterval = event.timeStamp - this._clickStart\n const movement = this.calculatePointersDistance(this._downEvents[1], this._downEvents[0]) * this._devPxRatio\n\n if (clickInterval <= this._maxInterval && movement <= this._posThreshold) {\n //second valid click detected\n //fire double tap and reset values\n this._nclicks = 0\n this._downEvents.splice(0, this._downEvents.length)\n this.onDoubleTap(event)\n } else {\n //new 'first click'\n this._nclicks = 1\n this._downEvents.shift()\n this._clickStart = performance.now()\n }\n }\n } else {\n this._downValid = false\n this._nclicks = 0\n this._downEvents.splice(0, this._downEvents.length)\n }\n } else {\n this._nclicks = 0\n this._downEvents.splice(0, this._downEvents.length)\n }\n }\n }\n\n private onWheel = (event: WheelEvent): void => {\n if (this.enabled && this.enableZoom && this.domElement) {\n let modifier: ModifierKey | null = null\n\n if (event.ctrlKey || event.metaKey) {\n modifier = 'CTRL'\n } else if (event.shiftKey) {\n modifier = 'SHIFT'\n }\n\n const mouseOp = this.getOpFromAction('WHEEL', modifier)\n\n if (mouseOp) {\n event.preventDefault()\n // @ts-ignore\n this.dispatchEvent(_startEvent)\n\n const notchDeltaY = 125 //distance of one notch of mouse wheel\n let sgn = event.deltaY / notchDeltaY\n\n let size = 1\n\n if (sgn > 0) {\n size = 1 / this.scaleFactor\n } else if (sgn < 0) {\n size = this.scaleFactor\n }\n\n switch (mouseOp) {\n case 'ZOOM':\n this.updateTbState(STATE.SCALE, true)\n\n if (sgn > 0) {\n size = 1 / Math.pow(this.scaleFactor, sgn)\n } else if (sgn < 0) {\n size = Math.pow(this.scaleFactor, -sgn)\n }\n\n if (this.cursorZoom && this.enablePan) {\n let scalePoint\n\n if (this.camera instanceof OrthographicCamera) {\n scalePoint = this.unprojectOnTbPlane(this.camera, event.clientX, event.clientY, this.domElement)\n ?.applyQuaternion(this.camera.quaternion)\n .multiplyScalar(1 / this.camera.zoom)\n .add(this._gizmos.position)\n }\n\n if (this.camera instanceof PerspectiveCamera) {\n scalePoint = this.unprojectOnTbPlane(this.camera, event.clientX, event.clientY, this.domElement)\n ?.applyQuaternion(this.camera.quaternion)\n .add(this._gizmos.position)\n }\n\n if (scalePoint !== undefined) this.applyTransformMatrix(this.applyScale(size, scalePoint))\n } else {\n this.applyTransformMatrix(this.applyScale(size, this._gizmos.position))\n }\n\n if (this._grid) {\n this.disposeGrid()\n this.drawGrid()\n }\n\n this.updateTbState(STATE.IDLE, false)\n\n // @ts-ignore\n this.dispatchEvent(_changeEvent)\n // @ts-ignore\n this.dispatchEvent(_endEvent)\n\n break\n\n case 'FOV':\n if (this.camera instanceof PerspectiveCamera) {\n this.updateTbState(STATE.FOV, true)\n\n //Vertigo effect\n\n //\t fov / 2\n //\t\t|\\\n //\t\t| \\\n //\t\t| \\\n //\tx\t|\t\\\n //\t\t| \t \\\n //\t\t| \t \\\n //\t\t| _ _ _\\\n //\t\t\ty\n\n //check for iOs shift shortcut\n if (event.deltaX != 0) {\n sgn = event.deltaX / notchDeltaY\n\n size = 1\n\n if (sgn > 0) {\n size = 1 / Math.pow(this.scaleFactor, sgn)\n } else if (sgn < 0) {\n size = Math.pow(this.scaleFactor, -sgn)\n }\n }\n\n this._v3_1.setFromMatrixPosition(this._cameraMatrixState)\n const x = this._v3_1.distanceTo(this._gizmos.position)\n let xNew = x / size //distance between camera and gizmos if scale(size, scalepoint) would be performed\n\n //check min and max distance\n xNew = MathUtils.clamp(xNew, this.minDistance, this.maxDistance)\n\n const y = x * Math.tan(MathUtils.DEG2RAD * this.camera.fov * 0.5)\n\n //calculate new fov\n let newFov = MathUtils.RAD2DEG * (Math.atan(y / xNew) * 2)\n\n //check min and max fov\n if (newFov > this.maxFov) {\n newFov = this.maxFov\n } else if (newFov < this.minFov) {\n newFov = this.minFov\n }\n\n const newDistance = y / Math.tan(MathUtils.DEG2RAD * (newFov / 2))\n size = x / newDistance\n\n this.setFov(newFov)\n this.applyTransformMatrix(this.applyScale(size, this._gizmos.position, false))\n }\n\n if (this._grid) {\n this.disposeGrid()\n this.drawGrid()\n }\n\n this.updateTbState(STATE.IDLE, false)\n\n // @ts-ignore\n this.dispatchEvent(_changeEvent)\n // @ts-ignore\n this.dispatchEvent(_endEvent)\n\n break\n }\n }\n }\n }\n\n private onSinglePanStart = (event: PointerEvent, operation: Operation): void => {\n if (this.enabled && this.domElement) {\n // @ts-ignore\n this.dispatchEvent(_startEvent)\n\n this.setCenter(event.clientX, event.clientY)\n\n switch (operation) {\n case 'PAN':\n if (!this.enablePan) return\n\n if (this._animationId != -1) {\n cancelAnimationFrame(this._animationId)\n this._animationId = -1\n this._timeStart = -1\n\n this.activateGizmos(false)\n // @ts-ignore\n this.dispatchEvent(_changeEvent)\n }\n\n if (this.camera) {\n this.updateTbState(STATE.PAN, true)\n const rayDir = this.unprojectOnTbPlane(this.camera, _center.x, _center.y, this.domElement)\n if (rayDir !== undefined) {\n this._startCursorPosition.copy(rayDir)\n }\n if (this.enableGrid) {\n this.drawGrid()\n // @ts-ignore\n this.dispatchEvent(_changeEvent)\n }\n }\n\n break\n\n case 'ROTATE':\n if (!this.enableRotate) return\n\n if (this._animationId != -1) {\n cancelAnimationFrame(this._animationId)\n this._animationId = -1\n this._timeStart = -1\n }\n\n if (this.camera) {\n this.updateTbState(STATE.ROTATE, true)\n const rayDir = this.unprojectOnTbSurface(this.camera, _center.x, _center.y, this.domElement, this._tbRadius)\n if (rayDir !== undefined) {\n this._startCursorPosition.copy(rayDir)\n }\n this.activateGizmos(true)\n if (this.enableAnimations) {\n this._timePrev = this._timeCurrent = performance.now()\n this._angleCurrent = this._anglePrev = 0\n this._cursorPosPrev.copy(this._startCursorPosition)\n this._cursorPosCurr.copy(this._cursorPosPrev)\n this._wCurr = 0\n this._wPrev = this._wCurr\n }\n }\n\n // @ts-ignore\n this.dispatchEvent(_changeEvent)\n break\n\n case 'FOV':\n if (!this.enableZoom) return\n\n if (this.camera instanceof PerspectiveCamera) {\n if (this._animationId != -1) {\n cancelAnimationFrame(this._animationId)\n this._animationId = -1\n this._timeStart = -1\n\n this.activateGizmos(false)\n // @ts-ignore\n this.dispatchEvent(_changeEvent)\n }\n\n this.updateTbState(STATE.FOV, true)\n this._startCursorPosition.setY(this.getCursorNDC(_center.x, _center.y, this.domElement).y * 0.5)\n this._currentCursorPosition.copy(this._startCursorPosition)\n }\n break\n\n case 'ZOOM':\n if (!this.enableZoom) return\n\n if (this._animationId != -1) {\n cancelAnimationFrame(this._animationId)\n this._animationId = -1\n this._timeStart = -1\n\n this.activateGizmos(false)\n // @ts-ignore\n this.dispatchEvent(_changeEvent)\n }\n\n this.updateTbState(STATE.SCALE, true)\n this._startCursorPosition.setY(this.getCursorNDC(_center.x, _center.y, this.domElement).y * 0.5)\n this._currentCursorPosition.copy(this._startCursorPosition)\n break\n }\n }\n }\n\n private onSinglePanMove = (event: PointerEvent, opState: Symbol): void => {\n if (this.enabled && this.domElement) {\n const restart = opState != this._state\n this.setCenter(event.clientX, event.clientY)\n\n switch (opState) {\n case STATE.PAN:\n if (this.enablePan && this.camera) {\n if (restart) {\n //switch to pan operation\n\n // @ts-ignore\n this.dispatchEvent(_endEvent)\n // @ts-ignore\n this.dispatchEvent(_startEvent)\n\n this.updateTbState(opState, true)\n const rayDir = this.unprojectOnTbPlane(this.camera, _center.x, _center.y, this.domElement)\n if (rayDir !== undefined) {\n this._startCursorPosition.copy(rayDir)\n }\n if (this.enableGrid) {\n this.drawGrid()\n }\n\n this.activateGizmos(false)\n } else {\n //continue with pan operation\n const rayDir = this.unprojectOnTbPlane(this.camera, _center.x, _center.y, this.domElement)\n if (rayDir !== undefined) {\n this._currentCursorPosition.copy(rayDir)\n }\n this.applyTransformMatrix(this.pan(this._startCursorPosition, this._currentCursorPosition))\n }\n }\n\n break\n\n case STATE.ROTATE:\n if (this.enableRotate && this.camera) {\n if (restart) {\n //switch to rotate operation\n\n // @ts-ignore\n this.dispatchEvent(_endEvent)\n // @ts-ignore\n this.dispatchEvent(_startEvent)\n\n this.updateTbState(opState, true)\n const rayDir = this.unprojectOnTbSurface(\n this.camera,\n _center.x,\n _center.y,\n this.domElement,\n this._tbRadius,\n )\n if (rayDir !== undefined) {\n this._startCursorPosition.copy(rayDir)\n }\n\n if (this.enableGrid) {\n this.disposeGrid()\n }\n\n this.activateGizmos(true)\n } else {\n //continue with rotate operation\n const rayDir = this.unprojectOnTbSurface(\n this.camera,\n _center.x,\n _center.y,\n this.domElement,\n this._tbRadius,\n )\n if (rayDir !== undefined) {\n this._currentCursorPosition.copy(rayDir)\n }\n\n const distance = this._startCursorPosition.distanceTo(this._currentCursorPosition)\n const angle = this._startCursorPosition.angleTo(this._currentCursorPosition)\n const amount = Math.max(distance / this._tbRadius, angle) //effective rotation angle\n\n this.applyTransformMatrix(\n this.rotate(this.calculateRotationAxis(this._startCursorPosition, this._currentCursorPosition), amount),\n )\n\n if (this.enableAnimations) {\n this._timePrev = this._timeCurrent\n this._timeCurrent = performance.now()\n this._anglePrev = this._angleCurrent\n this._angleCurrent = amount\n this._cursorPosPrev.copy(this._cursorPosCurr)\n this._cursorPosCurr.copy(this._currentCursorPosition)\n this._wPrev = this._wCurr\n this._wCurr = this.calculateAngularSpeed(\n this._anglePrev,\n this._angleCurrent,\n this._timePrev,\n this._timeCurrent,\n )\n }\n }\n }\n\n break\n\n case STATE.SCALE:\n if (this.enableZoom) {\n if (restart) {\n //switch to zoom operation\n\n // @ts-ignore\n this.dispatchEvent(_endEvent)\n // @ts-ignore\n this.dispatchEvent(_startEvent)\n\n this.updateTbState(opState, true)\n this._startCursorPosition.setY(this.getCursorNDC(_center.x, _center.y, this.domElement).y * 0.5)\n this._currentCursorPosition.copy(this._startCursorPosition)\n\n if (this.enableGrid) {\n this.disposeGrid()\n }\n\n this.activateGizmos(false)\n } else {\n //continue with zoom operation\n const screenNotches = 8 //how many wheel notches corresponds to a full screen pan\n this._currentCursorPosition.setY(this.getCursorNDC(_center.x, _center.y, this.domElement).y * 0.5)\n\n const movement = this._currentCursorPosition.y - this._startCursorPosition.y\n\n let size = 1\n\n if (movement < 0) {\n size = 1 / Math.pow(this.scaleFactor, -movement * screenNotches)\n } else if (movement > 0) {\n size = Math.pow(this.scaleFactor, movement * screenNotches)\n }\n\n this.applyTransformMatrix(this.applyScale(size, this._gizmos.position))\n }\n }\n\n break\n\n case STATE.FOV:\n if (this.enableZoom && this.camera instanceof PerspectiveCamera) {\n if (restart) {\n //switch to fov operation\n\n // @ts-ignore\n this.dispatchEvent(_endEvent)\n // @ts-ignore\n this.dispatchEvent(_startEvent)\n\n this.updateTbState(opState, true)\n this._startCursorPosition.setY(this.getCursorNDC(_center.x, _center.y, this.domElement).y * 0.5)\n this._currentCursorPosition.copy(this._startCursorPosition)\n\n if (this.enableGrid) {\n this.disposeGrid()\n }\n\n this.activateGizmos(false)\n } else {\n //continue with fov operation\n const screenNotches = 8 //how many wheel notches corresponds to a full screen pan\n this._currentCursorPosition.setY(this.getCursorNDC(_center.x, _center.y, this.domElement).y * 0.5)\n\n const movement = this._currentCursorPosition.y - this._startCursorPosition.y\n\n let size = 1\n\n if (movement < 0) {\n size = 1 / Math.pow(this.scaleFactor, -movement * screenNotches)\n } else if (movement > 0) {\n size = Math.pow(this.scaleFactor, movement * screenNotches)\n }\n\n this._v3_1.setFromMatrixPosition(this._cameraMatrixState)\n const x = this._v3_1.distanceTo(this._gizmos.position)\n let xNew = x / size //distance between camera and gizmos if scale(size, scalepoint) would be performed\n\n //check min and max distance\n xNew = MathUtils.clamp(xNew, this.minDistance, this.maxDistance)\n\n const y = x * Math.tan(MathUtils.DEG2RAD * this._fovState * 0.5)\n\n //calculate new fov\n let newFov = MathUtils.RAD2DEG * (Math.atan(y / xNew) * 2)\n\n //check min and max fov\n newFov = MathUtils.clamp(newFov, this.minFov, this.maxFov)\n\n const newDistance = y / Math.tan(MathUtils.DEG2RAD * (newFov / 2))\n size = x / newDistance\n this._v3_2.setFromMatrixPosition(this._gizmoMatrixState)\n\n this.setFov(newFov)\n this.applyTransformMatrix(this.applyScale(size, this._v3_2, false))\n\n //adjusting distance\n const direction = this._gizmos.position\n .clone()\n .sub(this.camera.position)\n .normalize()\n .multiplyScalar(newDistance / x)\n this._m4_1.makeTranslation(direction.x, direction.y, direction.z)\n }\n }\n\n break\n }\n\n // @ts-ignore\n this.dispatchEvent(_changeEvent)\n }\n }\n\n private onSinglePanEnd = (): void => {\n if (this._state == STATE.ROTATE) {\n if (!this.enableRotate) {\n return\n }\n\n if (this.enableAnimations) {\n //perform rotation animation\n const deltaTime = performance.now() - this._timeCurrent\n if (deltaTime < 120) {\n const w = Math.abs((this._wPrev + this._wCurr) / 2)\n\n const self = this\n this._animationId = window.requestAnimationFrame(function (t) {\n self.updateTbState(STATE.ANIMATION_ROTATE, true)\n const rotationAxis = self.calculateRotationAxis(self._cursorPosPrev, self._cursorPosCurr)\n\n self.onRotationAnim(t, rotationAxis, Math.min(w, self.wMax))\n })\n } else {\n //cursor has been standing still for over 120 ms since last movement\n this.updateTbState(STATE.IDLE, false)\n this.activateGizmos(false)\n // @ts-ignore\n this.dispatchEvent(_changeEvent)\n }\n } else {\n this.updateTbState(STATE.IDLE, false)\n this.activateGizmos(false)\n // @ts-ignore\n this.dispatchEvent(_changeEvent)\n }\n } else if (this._state == STATE.PAN || this._state == STATE.IDLE) {\n this.updateTbState(STATE.IDLE, false)\n\n if (this.enableGrid) {\n this.disposeGrid()\n }\n\n this.activateGizmos(false)\n // @ts-ignore\n this.dispatchEvent(_changeEvent)\n }\n\n // @ts-ignore\n this.dispatchEvent(_endEvent)\n }\n\n private onDoubleTap = (event: PointerEvent): void => {\n if (this.enabled && this.enablePan && this.scene && this.camera && this.domElement) {\n // @ts-ignore\n this.dispatchEvent(_startEvent)\n\n this.setCenter(event.clientX, event.clientY)\n const hitP = this.unprojectOnObj(this.getCursorNDC(_center.x, _center.y, this.domElement), this.camera)\n\n if (hitP && this.enableAnimations) {\n const self = this\n if (this._animationId != -1) {\n window.cancelAnimationFrame(this._animationId)\n }\n\n this._timeStart = -1\n this._animationId = window.requestAnimationFrame(function (t) {\n self.updateTbState(STATE.ANIMATION_FOCUS, true)\n self.onFocusAnim(t, hitP, self._cameraMatrixState, self._gizmoMatrixState)\n })\n } else if (hitP && !this.enableAnimations) {\n this.updateTbState(STATE.FOCUS, true)\n this.focus(hitP, this.scaleFactor)\n this.updateTbState(STATE.IDLE, false)\n // @ts-ignore\n this.dispatchEvent(_changeEvent)\n }\n }\n\n // @ts-ignore\n this.dispatchEvent(_endEvent)\n }\n\n private onDoublePanStart = (): void => {\n if (this.enabled && this.enablePan && this.camera && this.domElement) {\n // @ts-ignore\n this.dispatchEvent(_startEvent)\n\n this.updateTbState(STATE.PAN, true)\n\n this.setCenter(\n (this._touchCurrent[0].clientX + this._touchCurrent[1].clientX) / 2,\n (this._touchCurrent[0].clientY + this._touchCurrent[1].clientY) / 2,\n )\n\n const rayDir = this.unprojectOnTbPlane(this.camera, _center.x, _center.y, this.domElement, true)\n if (rayDir !== undefined) {\n this._startCursorPosition.copy(rayDir)\n }\n this._currentCursorPosition.copy(this._startCursorPosition)\n\n this.activateGizmos(false)\n }\n }\n\n private onDoublePanMove = (): void => {\n if (this.enabled && this.enablePan && this.camera && this.domElement) {\n this.setCenter(\n (this._touchCurrent[0].clientX + this._touchCurrent[1].clientX) / 2,\n (this._touchCurrent[0].clientY + this._touchCurrent[1].clientY) / 2,\n )\n\n if (this._state != STATE.PAN) {\n this.updateTbState(STATE.PAN, true)\n this._startCursorPosition.copy(this._currentCursorPosition)\n }\n\n const rayDir = this.unprojectOnTbPlane(this.camera, _center.x, _center.y, this.domElement, true)\n if (rayDir !== undefined) this._currentCursorPosition.copy(rayDir)\n this.applyTransformMatrix(this.pan(this._startCursorPosition, this._currentCursorPosition, true))\n // @ts-ignore\n this.dispatchEvent(_changeEvent)\n }\n }\n\n private onDoublePanEnd = (): void => {\n this.updateTbState(STATE.IDLE, false)\n // @ts-ignore\n this.dispatchEvent(_endEvent)\n }\n\n private onRotateStart = (): void => {\n if (this.enabled && this.enableRotate) {\n // @ts-ignore\n this.dispatchEvent(_startEvent)\n\n this.updateTbState(STATE.ZROTATE, true)\n\n //this._startFingerRotation = event.rotation;\n\n this._startFingerRotation =\n this.getAngle(this._touchCurrent[1], this._touchCurrent[0]) +\n this.getAngle(this._touchStart[1], this._touchStart[0])\n this._currentFingerRotation = this._startFingerRotation\n\n this.camera?.getWorldDirection(this._rotationAxis) //rotation axis\n\n if (!this.enablePan && !this.enableZoom) {\n this.activateGizmos(true)\n }\n }\n }\n\n private onRotateMove = (): void => {\n if (this.enabled && this.enableRotate && this.camera && this.domElement) {\n this.setCenter(\n (this._touchCurrent[0].clientX + this._touchCurrent[1].clientX) / 2,\n (this._touchCurrent[0].clientY + this._touchCurrent[1].clientY) / 2,\n )\n let rotationPoint\n\n if (this._state != STATE.ZROTATE) {\n this.updateTbState(STATE.ZROTATE, true)\n this._startFingerRotation = this._currentFingerRotation\n }\n\n //this._currentFingerRotation = event.rotation;\n this._currentFingerRotation =\n this.getAngle(this._touchCurrent[1], this._touchCurrent[0]) +\n this.getAngle(this._touchStart[1], this._touchStart[0])\n\n if (!this.enablePan) {\n rotationPoint = new Vector3().setFromMatrixPosition(this._gizmoMatrixState)\n } else if (this.camera) {\n this._v3_2.setFromMatrixPosition(this._gizmoMatrixState)\n rotationPoint = this.unprojectOnTbPlane(this.camera, _center.x, _center.y, this.domElement)\n ?.applyQuaternion(this.camera.quaternion)\n .multiplyScalar(1 / this.camera.zoom)\n .add(this._v3_2)\n }\n\n const amount = MathUtils.DEG2RAD * (this._startFingerRotation - this._currentFingerRotation)\n\n if (rotationPoint !== undefined) {\n this.applyTransformMatrix(this.zRotate(rotationPoint, amount))\n }\n // @ts-ignore\n this.dispatchEvent(_changeEvent)\n }\n }\n\n private onRotateEnd = (): void => {\n this.updateTbState(STATE.IDLE, false)\n this.activateGizmos(false)\n // @ts-ignore\n this.dispatchEvent(_endEvent)\n }\n\n private onPinchStart = (): void => {\n if (this.enabled && this.enableZoom) {\n // @ts-ignore\n this.dispatchEvent(_startEvent)\n this.updateTbState(STATE.SCALE, true)\n\n this._startFingerDistance = this.calculatePointersDistance(this._touchCurrent[0], this._touchCurrent[1])\n this._currentFingerDistance = this._startFingerDistance\n\n this.activateGizmos(false)\n }\n }\n\n private onPinchMove = (): void => {\n if (this.enabled && this.enableZoom && this.domElement) {\n this.setCenter(\n (this._touchCurrent[0].clientX + this._touchCurrent[1].clientX) / 2,\n (this._touchCurrent[0].clientY + this._touchCurrent[1].clientY) / 2,\n )\n const minDistance = 12 //minimum distance between fingers (in css pixels)\n\n if (this._state != STATE.SCALE) {\n this._startFingerDistance = this._currentFingerDistance\n this.updateTbState(STATE.SCALE, true)\n }\n\n this._currentFingerDistance = Math.max(\n this.calculatePointersDistance(this._touchCurrent[0], this._touchCurrent[1]),\n minDistance * this._devPxRatio,\n )\n const amount = this._currentFingerDistance / this._startFingerDistance\n\n let scalePoint\n\n if (!this.enablePan) {\n scalePoint = this._gizmos.position\n } else {\n if (this.camera instanceof OrthographicCamera) {\n scalePoint = this.unprojectOnTbPlane(this.camera, _center.x, _center.y, this.domElement)\n ?.applyQuaternion(this.camera.quaternion)\n .multiplyScalar(1 / this.camera.zoom)\n .add(this._gizmos.position)\n } else if (this.camera instanceof PerspectiveCamera) {\n scalePoint = this.unprojectOnTbPlane(this.camera, _center.x, _center.y, this.domElement)\n ?.applyQuaternion(this.camera.quaternion)\n .add(this._gizmos.position)\n }\n }\n\n if (scalePoint !== undefined) {\n this.applyTransformMatrix(this.applyScale(amount, scalePoint))\n }\n // @ts-ignore\n this.dispatchEvent(_changeEvent)\n }\n }\n\n private onPinchEnd = (): void => {\n this.updateTbState(STATE.IDLE, false)\n // @ts-ignore\n this.dispatchEvent(_endEvent)\n }\n\n private onTriplePanStart = (): void => {\n if (this.enabled && this.enableZoom && this.domElement) {\n // @ts-ignore\n this.dispatchEvent(_startEvent)\n\n this.updateTbState(STATE.SCALE, true)\n\n //const center = event.center;\n let clientX = 0\n let clientY = 0\n const nFingers = this._touchCurrent.length\n\n for (let i = 0; i < nFingers; i++) {\n clientX += this._touchCurrent[i].clientX\n clientY += this._touchCurrent[i].clientY\n }\n\n this.setCenter(clientX / nFingers, clientY / nFingers)\n\n this._startCursorPosition.setY(this.getCursorNDC(_center.x, _center.y, this.domElement).y * 0.5)\n this._currentCursorPosition.copy(this._startCursorPosition)\n }\n }\n\n private onTriplePanMove = (): void => {\n if (this.enabled && this.enableZoom && this.camera && this.domElement) {\n //\t fov / 2\n //\t\t|\\\n //\t\t| \\\n //\t\t| \\\n //\tx\t|\t\\\n //\t\t| \t \\\n //\t\t| \t \\\n //\t\t| _ _ _\\\n //\t\t\ty\n\n //const center = event.center;\n let clientX = 0\n let clientY = 0\n const nFingers = this._touchCurrent.length\n\n for (let i = 0; i < nFingers; i++) {\n clientX += this._touchCurrent[i].clientX\n clientY += this._touchCurrent[i].clientY\n }\n\n this.setCenter(clientX / nFingers, clientY / nFingers)\n\n const screenNotches = 8 //how many wheel notches corresponds to a full screen pan\n this._currentCursorPosition.setY(this.getCursorNDC(_center.x, _center.y, this.domElement).y * 0.5)\n\n const movement = this._currentCursorPosition.y - this._startCursorPosition.y\n\n let size = 1\n\n if (movement < 0) {\n size = 1 / Math.pow(this.scaleFactor, -movement * screenNotches)\n } else if (movement > 0) {\n size = Math.pow(this.scaleFactor, movement * screenNotches)\n }\n\n this._v3_1.setFromMatrixPosition(this._cameraMatrixState)\n const x = this._v3_1.distanceTo(this._gizmos.position)\n let xNew = x / size //distance between camera and gizmos if scale(size, scalepoint) would be performed\n\n //check min and max distance\n xNew = MathUtils.clamp(xNew, this.minDistance, this.maxDistance)\n\n const y = x * Math.tan(MathUtils.DEG2RAD * this._fovState * 0.5)\n\n //calculate new fov\n let newFov = MathUtils.RAD2DEG * (Math.atan(y / xNew) * 2)\n\n //check min and max fov\n newFov = MathUtils.clamp(newFov, this.minFov, this.maxFov)\n\n const newDistance = y / Math.tan(MathUtils.DEG2RAD * (newFov / 2))\n size = x / newDistance\n this._v3_2.setFromMatrixPosition(this._gizmoMatrixState)\n\n this.setFov(newFov)\n this.applyTransformMatrix(this.applyScale(size, this._v3_2, false))\n\n //adjusting distance\n const direction = this._gizmos.position\n .clone()\n .sub(this.camera.position)\n .normalize()\n .multiplyScalar(newDistance / x)\n this._m4_1.makeTranslation(direction.x, direction.y, direction.z)\n\n // @ts-ignore\n this.dispatchEvent(_changeEvent)\n }\n }\n\n private onTriplePanEnd = (): void => {\n this.updateTbState(STATE.IDLE, false)\n // @ts-ignore\n this.dispatchEvent(_endEvent)\n //this.dispatchEvent( _changeEvent );\n }\n\n /**\n * Set _center's x/y coordinates\n * @param {Number} clientX\n * @param {Number} clientY\n */\n private setCenter = (clientX: number, clientY: number): void => {\n _center.x = clientX\n _center.y = clientY\n }\n\n /**\n * Set default mouse actions\n */\n private initializeMouseActions = (): void => {\n this.setMouseAction('PAN', 0, 'CTRL')\n this.setMouseAction('PAN', 2)\n\n this.setMouseAction('ROTATE', 0)\n\n this.setMouseAction('ZOOM', 'WHEEL')\n this.setMouseAction('ZOOM', 1)\n\n this.setMouseAction('FOV', 'WHEEL', 'SHIFT')\n this.setMouseAction('FOV', 1, 'SHIFT')\n }\n\n /**\n * Set a new mouse action by specifying the operation to be performed and a mouse/key combination. In case of conflict, replaces the existing one\n * @param {String} operation The operation to be performed ('PAN', 'ROTATE', 'ZOOM', 'FOV)\n * @param {*} mouse A mouse button (0, 1, 2) or 'WHEEL' for wheel notches\n * @param {*} key The keyboard modifier ('CTRL', 'SHIFT') or null if key is not needed\n * @returns {Boolean} True if the mouse action has been successfully added, false otherwise\n */\n private setMouseAction = (operation: Operation, mouse: MouseButtonType, key: ModifierKey | null = null): boolean => {\n const operationInput = ['PAN', 'ROTATE', 'ZOOM', 'FOV']\n const mouseInput = [0, 1, 2, 'WHEEL']\n const keyInput = ['CTRL', 'SHIFT', null]\n let state\n\n if (!operationInput.includes(operation) || !mouseInput.includes(mouse) || !keyInput.includes(key)) {\n //invalid parameters\n return false\n }\n\n if (mouse == 'WHEEL') {\n if (operation != 'ZOOM' && operation != 'FOV') {\n //cannot associate 2D operation to 1D input\n return false\n }\n }\n\n switch (operation) {\n case 'PAN':\n state = STATE.PAN\n break\n\n case 'ROTATE':\n state = STATE.ROTATE\n break\n\n case 'ZOOM':\n state = STATE.SCALE\n break\n\n case 'FOV':\n state = STATE.FOV\n break\n }\n\n const action = {\n operation: operation,\n mouse: mouse,\n key: key,\n state: state,\n }\n\n for (let i = 0; i < this.mouseActions.length; i++) {\n if (this.mouseActions[i].mouse == action.mouse && this.mouseActions[i].key == action.key) {\n this.mouseActions.splice(i, 1, action)\n return true\n }\n }\n\n this.mouseActions.push(action)\n return true\n }\n\n /**\n * Return the operation associated to a mouse/keyboard combination\n * @param {*} mouse A mouse button (0, 1, 2) or 'WHEEL' for wheel notches\n * @param {*} key The keyboard modifier ('CTRL', 'SHIFT') or null if key is not needed\n * @returns The operation if it has been found, null otherwise\n */\n private getOpFromAction = (mouse: MouseButtonType, key: ModifierKey | null): Operation | null => {\n let action\n\n for (let i = 0; i < this.mouseActions.length; i++) {\n action = this.mouseActions[i]\n if (action.mouse == mouse && action.key == key) {\n return action.operation\n }\n }\n\n if (key) {\n for (let i = 0; i < this.mouseActions.length; i++) {\n action = this.mouseActions[i]\n if (action.mouse == mouse && action.key == null) {\n return action.operation\n }\n }\n }\n\n return null\n }\n\n /**\n * Get the operation associated to mouse and key combination and returns the corresponding FSA state\n * @param {Number} mouse M