Spaces:
Running
Running
| /*! | |
| * camera-controls | |
| * https://github.com/yomotsu/camera-controls | |
| * (c) 2017 @yomotsu | |
| * Released under the MIT License. | |
| */ | |
| // see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons#value | |
| const MOUSE_BUTTON = { | |
| LEFT: 1, | |
| RIGHT: 2, | |
| MIDDLE: 4, | |
| }; | |
| const ACTION = Object.freeze({ | |
| NONE: 0b0, | |
| ROTATE: 0b1, | |
| TRUCK: 0b10, | |
| SCREEN_PAN: 0b100, | |
| OFFSET: 0b1000, | |
| DOLLY: 0b10000, | |
| ZOOM: 0b100000, | |
| TOUCH_ROTATE: 0b1000000, | |
| TOUCH_TRUCK: 0b10000000, | |
| TOUCH_SCREEN_PAN: 0b100000000, | |
| TOUCH_OFFSET: 0b1000000000, | |
| TOUCH_DOLLY: 0b10000000000, | |
| TOUCH_ZOOM: 0b100000000000, | |
| TOUCH_DOLLY_TRUCK: 0b1000000000000, | |
| TOUCH_DOLLY_SCREEN_PAN: 0b10000000000000, | |
| TOUCH_DOLLY_OFFSET: 0b100000000000000, | |
| TOUCH_DOLLY_ROTATE: 0b1000000000000000, | |
| TOUCH_ZOOM_TRUCK: 0b10000000000000000, | |
| TOUCH_ZOOM_OFFSET: 0b100000000000000000, | |
| TOUCH_ZOOM_SCREEN_PAN: 0b1000000000000000000, | |
| TOUCH_ZOOM_ROTATE: 0b10000000000000000000, | |
| }); | |
| const DOLLY_DIRECTION = { | |
| NONE: 0, | |
| IN: 1, | |
| OUT: -1, | |
| }; | |
| function isPerspectiveCamera(camera) { | |
| return camera.isPerspectiveCamera; | |
| } | |
| function isOrthographicCamera(camera) { | |
| return camera.isOrthographicCamera; | |
| } | |
| const PI_2 = Math.PI * 2; | |
| const PI_HALF = Math.PI / 2; | |
| const EPSILON = 1e-5; | |
| const DEG2RAD = Math.PI / 180; | |
| function clamp(value, min, max) { | |
| return Math.max(min, Math.min(max, value)); | |
| } | |
| function approxZero(number, error = EPSILON) { | |
| return Math.abs(number) < error; | |
| } | |
| function approxEquals(a, b, error = EPSILON) { | |
| return approxZero(a - b, error); | |
| } | |
| function roundToStep(value, step) { | |
| return Math.round(value / step) * step; | |
| } | |
| function infinityToMaxNumber(value) { | |
| if (isFinite(value)) | |
| return value; | |
| if (value < 0) | |
| return -Number.MAX_VALUE; | |
| return Number.MAX_VALUE; | |
| } | |
| function maxNumberToInfinity(value) { | |
| if (Math.abs(value) < Number.MAX_VALUE) | |
| return value; | |
| return value * Infinity; | |
| } | |
| // https://docs.unity3d.com/ScriptReference/Mathf.SmoothDamp.html | |
| // https://github.com/Unity-Technologies/UnityCsReference/blob/a2bdfe9b3c4cd4476f44bf52f848063bfaf7b6b9/Runtime/Export/Math/Mathf.cs#L308 | |
| function smoothDamp(current, target, currentVelocityRef, smoothTime, maxSpeed = Infinity, deltaTime) { | |
| // Based on Game Programming Gems 4 Chapter 1.10 | |
| smoothTime = Math.max(0.0001, smoothTime); | |
| const omega = 2 / smoothTime; | |
| const x = omega * deltaTime; | |
| const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x); | |
| let change = current - target; | |
| const originalTo = target; | |
| // Clamp maximum speed | |
| const maxChange = maxSpeed * smoothTime; | |
| change = clamp(change, -maxChange, maxChange); | |
| target = current - change; | |
| const temp = (currentVelocityRef.value + omega * change) * deltaTime; | |
| currentVelocityRef.value = (currentVelocityRef.value - omega * temp) * exp; | |
| let output = target + (change + temp) * exp; | |
| // Prevent overshooting | |
| if (originalTo - current > 0.0 === output > originalTo) { | |
| output = originalTo; | |
| currentVelocityRef.value = (output - originalTo) / deltaTime; | |
| } | |
| return output; | |
| } | |
| // https://docs.unity3d.com/ScriptReference/Vector3.SmoothDamp.html | |
| // https://github.com/Unity-Technologies/UnityCsReference/blob/a2bdfe9b3c4cd4476f44bf52f848063bfaf7b6b9/Runtime/Export/Math/Vector3.cs#L97 | |
| function smoothDampVec3(current, target, currentVelocityRef, smoothTime, maxSpeed = Infinity, deltaTime, out) { | |
| // Based on Game Programming Gems 4 Chapter 1.10 | |
| smoothTime = Math.max(0.0001, smoothTime); | |
| const omega = 2 / smoothTime; | |
| const x = omega * deltaTime; | |
| const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x); | |
| let targetX = target.x; | |
| let targetY = target.y; | |
| let targetZ = target.z; | |
| let changeX = current.x - targetX; | |
| let changeY = current.y - targetY; | |
| let changeZ = current.z - targetZ; | |
| const originalToX = targetX; | |
| const originalToY = targetY; | |
| const originalToZ = targetZ; | |
| // Clamp maximum speed | |
| const maxChange = maxSpeed * smoothTime; | |
| const maxChangeSq = maxChange * maxChange; | |
| const magnitudeSq = changeX * changeX + changeY * changeY + changeZ * changeZ; | |
| if (magnitudeSq > maxChangeSq) { | |
| const magnitude = Math.sqrt(magnitudeSq); | |
| changeX = changeX / magnitude * maxChange; | |
| changeY = changeY / magnitude * maxChange; | |
| changeZ = changeZ / magnitude * maxChange; | |
| } | |
| targetX = current.x - changeX; | |
| targetY = current.y - changeY; | |
| targetZ = current.z - changeZ; | |
| const tempX = (currentVelocityRef.x + omega * changeX) * deltaTime; | |
| const tempY = (currentVelocityRef.y + omega * changeY) * deltaTime; | |
| const tempZ = (currentVelocityRef.z + omega * changeZ) * deltaTime; | |
| currentVelocityRef.x = (currentVelocityRef.x - omega * tempX) * exp; | |
| currentVelocityRef.y = (currentVelocityRef.y - omega * tempY) * exp; | |
| currentVelocityRef.z = (currentVelocityRef.z - omega * tempZ) * exp; | |
| out.x = targetX + (changeX + tempX) * exp; | |
| out.y = targetY + (changeY + tempY) * exp; | |
| out.z = targetZ + (changeZ + tempZ) * exp; | |
| // Prevent overshooting | |
| const origMinusCurrentX = originalToX - current.x; | |
| const origMinusCurrentY = originalToY - current.y; | |
| const origMinusCurrentZ = originalToZ - current.z; | |
| const outMinusOrigX = out.x - originalToX; | |
| const outMinusOrigY = out.y - originalToY; | |
| const outMinusOrigZ = out.z - originalToZ; | |
| if (origMinusCurrentX * outMinusOrigX + origMinusCurrentY * outMinusOrigY + origMinusCurrentZ * outMinusOrigZ > 0) { | |
| out.x = originalToX; | |
| out.y = originalToY; | |
| out.z = originalToZ; | |
| currentVelocityRef.x = (out.x - originalToX) / deltaTime; | |
| currentVelocityRef.y = (out.y - originalToY) / deltaTime; | |
| currentVelocityRef.z = (out.z - originalToZ) / deltaTime; | |
| } | |
| return out; | |
| } | |
| function extractClientCoordFromEvent(pointers, out) { | |
| out.set(0, 0); | |
| pointers.forEach((pointer) => { | |
| out.x += pointer.clientX; | |
| out.y += pointer.clientY; | |
| }); | |
| out.x /= pointers.length; | |
| out.y /= pointers.length; | |
| } | |
| function notSupportedInOrthographicCamera(camera, message) { | |
| if (isOrthographicCamera(camera)) { | |
| console.warn(`${message} is not supported in OrthographicCamera`); | |
| return true; | |
| } | |
| return false; | |
| } | |
| class EventDispatcher { | |
| constructor() { | |
| this._listeners = {}; | |
| } | |
| /** | |
| * Adds the specified event listener. | |
| * @param type event name | |
| * @param listener handler function | |
| * @category Methods | |
| */ | |
| addEventListener(type, listener) { | |
| const listeners = this._listeners; | |
| if (listeners[type] === undefined) | |
| listeners[type] = []; | |
| if (listeners[type].indexOf(listener) === -1) | |
| listeners[type].push(listener); | |
| } | |
| /** | |
| * Presence of the specified event listener. | |
| * @param type event name | |
| * @param listener handler function | |
| * @category Methods | |
| */ | |
| hasEventListener(type, listener) { | |
| const listeners = this._listeners; | |
| return listeners[type] !== undefined && listeners[type].indexOf(listener) !== -1; | |
| } | |
| /** | |
| * Removes the specified event listener | |
| * @param type event name | |
| * @param listener handler function | |
| * @category Methods | |
| */ | |
| removeEventListener(type, listener) { | |
| const listeners = this._listeners; | |
| const listenerArray = listeners[type]; | |
| if (listenerArray !== undefined) { | |
| const index = listenerArray.indexOf(listener); | |
| if (index !== -1) | |
| listenerArray.splice(index, 1); | |
| } | |
| } | |
| /** | |
| * Removes all event listeners | |
| * @param type event name | |
| * @category Methods | |
| */ | |
| removeAllEventListeners(type) { | |
| if (!type) { | |
| this._listeners = {}; | |
| return; | |
| } | |
| if (Array.isArray(this._listeners[type])) | |
| this._listeners[type].length = 0; | |
| } | |
| /** | |
| * Fire an event type. | |
| * @param event DispatcherEvent | |
| * @category Methods | |
| */ | |
| dispatchEvent(event) { | |
| const listeners = this._listeners; | |
| const listenerArray = listeners[event.type]; | |
| if (listenerArray !== undefined) { | |
| event.target = this; | |
| const array = listenerArray.slice(0); | |
| for (let i = 0, l = array.length; i < l; i++) { | |
| array[i].call(this, event); | |
| } | |
| } | |
| } | |
| } | |
| var _a; | |
| const VERSION = '2.10.1'; // will be replaced with `version` in package.json during the build process. | |
| const TOUCH_DOLLY_FACTOR = 1 / 8; | |
| const isMac = /Mac/.test((_a = globalThis === null || globalThis === void 0 ? void 0 : globalThis.navigator) === null || _a === void 0 ? void 0 : _a.platform); | |
| let THREE; | |
| let _ORIGIN; | |
| let _AXIS_Y; | |
| let _AXIS_Z; | |
| let _v2; | |
| let _v3A; | |
| let _v3B; | |
| let _v3C; | |
| let _cameraDirection; | |
| let _xColumn; | |
| let _yColumn; | |
| let _zColumn; | |
| let _deltaTarget; | |
| let _deltaOffset; | |
| let _sphericalA; | |
| let _sphericalB; | |
| let _box3A; | |
| let _box3B; | |
| let _sphere; | |
| let _quaternionA; | |
| let _quaternionB; | |
| let _rotationMatrix; | |
| let _raycaster; | |
| class CameraControls extends EventDispatcher { | |
| /** | |
| * Injects THREE as the dependency. You can then proceed to use CameraControls. | |
| * | |
| * e.g | |
| * ```javascript | |
| * CameraControls.install( { THREE: THREE } ); | |
| * ``` | |
| * | |
| * Note: If you do not wish to use enter three.js to reduce file size(tree-shaking for example), make a subset to install. | |
| * | |
| * ```js | |
| * import { | |
| * Vector2, | |
| * Vector3, | |
| * Vector4, | |
| * Quaternion, | |
| * Matrix4, | |
| * Spherical, | |
| * Box3, | |
| * Sphere, | |
| * Raycaster, | |
| * MathUtils, | |
| * } from 'three'; | |
| * | |
| * const subsetOfTHREE = { | |
| * Vector2 : Vector2, | |
| * Vector3 : Vector3, | |
| * Vector4 : Vector4, | |
| * Quaternion: Quaternion, | |
| * Matrix4 : Matrix4, | |
| * Spherical : Spherical, | |
| * Box3 : Box3, | |
| * Sphere : Sphere, | |
| * Raycaster : Raycaster, | |
| * }; | |
| * CameraControls.install( { THREE: subsetOfTHREE } ); | |
| * ``` | |
| * @category Statics | |
| */ | |
| static install(libs) { | |
| THREE = libs.THREE; | |
| _ORIGIN = Object.freeze(new THREE.Vector3(0, 0, 0)); | |
| _AXIS_Y = Object.freeze(new THREE.Vector3(0, 1, 0)); | |
| _AXIS_Z = Object.freeze(new THREE.Vector3(0, 0, 1)); | |
| _v2 = new THREE.Vector2(); | |
| _v3A = new THREE.Vector3(); | |
| _v3B = new THREE.Vector3(); | |
| _v3C = new THREE.Vector3(); | |
| _cameraDirection = new THREE.Vector3(); | |
| _xColumn = new THREE.Vector3(); | |
| _yColumn = new THREE.Vector3(); | |
| _zColumn = new THREE.Vector3(); | |
| _deltaTarget = new THREE.Vector3(); | |
| _deltaOffset = new THREE.Vector3(); | |
| _sphericalA = new THREE.Spherical(); | |
| _sphericalB = new THREE.Spherical(); | |
| _box3A = new THREE.Box3(); | |
| _box3B = new THREE.Box3(); | |
| _sphere = new THREE.Sphere(); | |
| _quaternionA = new THREE.Quaternion(); | |
| _quaternionB = new THREE.Quaternion(); | |
| _rotationMatrix = new THREE.Matrix4(); | |
| _raycaster = new THREE.Raycaster(); | |
| } | |
| /** | |
| * list all ACTIONs | |
| * @category Statics | |
| */ | |
| static get ACTION() { | |
| return ACTION; | |
| } | |
| /** | |
| * @deprecated Use `cameraControls.mouseButtons.left = CameraControls.ACTION.SCREEN_PAN` instead. | |
| */ | |
| set verticalDragToForward(_) { | |
| console.warn('camera-controls: `verticalDragToForward` was removed. Use `mouseButtons.left = CameraControls.ACTION.SCREEN_PAN` instead.'); | |
| } | |
| /** | |
| * Creates a `CameraControls` instance. | |
| * | |
| * Note: | |
| * You **must install** three.js before using camera-controls. see [#install](#install) | |
| * Not doing so will lead to runtime errors (`undefined` references to THREE). | |
| * | |
| * e.g. | |
| * ``` | |
| * CameraControls.install( { THREE } ); | |
| * const cameraControls = new CameraControls( camera, domElement ); | |
| * ``` | |
| * | |
| * @param camera A `THREE.PerspectiveCamera` or `THREE.OrthographicCamera` to be controlled. | |
| * @param domElement A `HTMLElement` for the draggable area, usually `renderer.domElement`. | |
| * @category Constructor | |
| */ | |
| constructor(camera, domElement) { | |
| super(); | |
| /** | |
| * Minimum vertical angle in radians. | |
| * The angle has to be between `0` and `.maxPolarAngle` inclusive. | |
| * The default value is `0`. | |
| * | |
| * e.g. | |
| * ``` | |
| * cameraControls.maxPolarAngle = 0; | |
| * ``` | |
| * @category Properties | |
| */ | |
| this.minPolarAngle = 0; // radians | |
| /** | |
| * Maximum vertical angle in radians. | |
| * The angle has to be between `.maxPolarAngle` and `Math.PI` inclusive. | |
| * The default value is `Math.PI`. | |
| * | |
| * e.g. | |
| * ``` | |
| * cameraControls.maxPolarAngle = Math.PI; | |
| * ``` | |
| * @category Properties | |
| */ | |
| this.maxPolarAngle = Math.PI; // radians | |
| /** | |
| * Minimum horizontal angle in radians. | |
| * The angle has to be less than `.maxAzimuthAngle`. | |
| * The default value is `- Infinity`. | |
| * | |
| * e.g. | |
| * ``` | |
| * cameraControls.minAzimuthAngle = - Infinity; | |
| * ``` | |
| * @category Properties | |
| */ | |
| this.minAzimuthAngle = -Infinity; // radians | |
| /** | |
| * Maximum horizontal angle in radians. | |
| * The angle has to be greater than `.minAzimuthAngle`. | |
| * The default value is `Infinity`. | |
| * | |
| * e.g. | |
| * ``` | |
| * cameraControls.maxAzimuthAngle = Infinity; | |
| * ``` | |
| * @category Properties | |
| */ | |
| this.maxAzimuthAngle = Infinity; // radians | |
| // How far you can dolly in and out ( PerspectiveCamera only ) | |
| /** | |
| * Minimum distance for dolly. The value must be higher than `0`. Default is `Number.EPSILON`. | |
| * PerspectiveCamera only. | |
| * @category Properties | |
| */ | |
| this.minDistance = Number.EPSILON; | |
| /** | |
| * Maximum distance for dolly. The value must be higher than `minDistance`. Default is `Infinity`. | |
| * PerspectiveCamera only. | |
| * @category Properties | |
| */ | |
| this.maxDistance = Infinity; | |
| /** | |
| * `true` to enable Infinity Dolly for wheel and pinch. Use this with `minDistance` and `maxDistance` | |
| * If the Dolly distance is less (or over) than the `minDistance` (or `maxDistance`), `infinityDolly` will keep the distance and pushes the target position instead. | |
| * @category Properties | |
| */ | |
| this.infinityDolly = false; | |
| /** | |
| * Minimum camera zoom. | |
| * @category Properties | |
| */ | |
| this.minZoom = 0.01; | |
| /** | |
| * Maximum camera zoom. | |
| * @category Properties | |
| */ | |
| this.maxZoom = Infinity; | |
| /** | |
| * Approximate time in seconds to reach the target. A smaller value will reach the target faster. | |
| * @category Properties | |
| */ | |
| this.smoothTime = 0.25; | |
| /** | |
| * the smoothTime while dragging | |
| * @category Properties | |
| */ | |
| this.draggingSmoothTime = 0.125; | |
| /** | |
| * Max transition speed in unit-per-seconds | |
| * @category Properties | |
| */ | |
| this.maxSpeed = Infinity; | |
| /** | |
| * Speed of azimuth (horizontal) rotation. | |
| * @category Properties | |
| */ | |
| this.azimuthRotateSpeed = 1.0; | |
| /** | |
| * Speed of polar (vertical) rotation. | |
| * @category Properties | |
| */ | |
| this.polarRotateSpeed = 1.0; | |
| /** | |
| * Speed of mouse-wheel dollying. | |
| * @category Properties | |
| */ | |
| this.dollySpeed = 1.0; | |
| /** | |
| * `true` to invert direction when dollying or zooming via drag | |
| * @category Properties | |
| */ | |
| this.dollyDragInverted = false; | |
| /** | |
| * Speed of drag for truck and pedestal. | |
| * @category Properties | |
| */ | |
| this.truckSpeed = 2.0; | |
| /** | |
| * `true` to enable Dolly-in to the mouse cursor coords. | |
| * @category Properties | |
| */ | |
| this.dollyToCursor = false; | |
| /** | |
| * @category Properties | |
| */ | |
| this.dragToOffset = false; | |
| /** | |
| * Friction ratio of the boundary. | |
| * @category Properties | |
| */ | |
| this.boundaryFriction = 0.0; | |
| /** | |
| * Controls how soon the `rest` event fires as the camera slows. | |
| * @category Properties | |
| */ | |
| this.restThreshold = 0.01; | |
| /** | |
| * An array of Meshes to collide with camera. | |
| * Be aware colliderMeshes may decrease performance. The collision test uses 4 raycasters from the camera since the near plane has 4 corners. | |
| * @category Properties | |
| */ | |
| this.colliderMeshes = []; | |
| /** | |
| * Force cancel user dragging. | |
| * @category Methods | |
| */ | |
| // cancel will be overwritten in the constructor. | |
| this.cancel = () => { }; | |
| this._enabled = true; | |
| this._state = ACTION.NONE; | |
| this._viewport = null; | |
| this._changedDolly = 0; | |
| this._changedZoom = 0; | |
| this._hasRested = true; | |
| this._boundaryEnclosesCamera = false; | |
| this._needsUpdate = true; | |
| this._updatedLastTime = false; | |
| this._elementRect = new DOMRect(); | |
| this._isDragging = false; | |
| this._dragNeedsUpdate = true; | |
| this._activePointers = []; | |
| this._lockedPointer = null; | |
| this._interactiveArea = new DOMRect(0, 0, 1, 1); | |
| // Use draggingSmoothTime over smoothTime while true. | |
| // set automatically true on user-dragging start. | |
| // set automatically false on programmable methods call. | |
| this._isUserControllingRotate = false; | |
| this._isUserControllingDolly = false; | |
| this._isUserControllingTruck = false; | |
| this._isUserControllingOffset = false; | |
| this._isUserControllingZoom = false; | |
| this._lastDollyDirection = DOLLY_DIRECTION.NONE; | |
| // velocities for smoothDamp | |
| this._thetaVelocity = { value: 0 }; | |
| this._phiVelocity = { value: 0 }; | |
| this._radiusVelocity = { value: 0 }; | |
| this._targetVelocity = new THREE.Vector3(); | |
| this._focalOffsetVelocity = new THREE.Vector3(); | |
| this._zoomVelocity = { value: 0 }; | |
| this._truckInternal = (deltaX, deltaY, dragToOffset, screenSpacePanning) => { | |
| let truckX; | |
| let pedestalY; | |
| if (isPerspectiveCamera(this._camera)) { | |
| const offset = _v3A.copy(this._camera.position).sub(this._target); | |
| // half of the fov is center to top of screen | |
| const fov = this._camera.getEffectiveFOV() * DEG2RAD; | |
| const targetDistance = offset.length() * Math.tan(fov * 0.5); | |
| truckX = (this.truckSpeed * deltaX * targetDistance / this._elementRect.height); | |
| pedestalY = (this.truckSpeed * deltaY * targetDistance / this._elementRect.height); | |
| } | |
| else if (isOrthographicCamera(this._camera)) { | |
| const camera = this._camera; | |
| truckX = this.truckSpeed * deltaX * (camera.right - camera.left) / camera.zoom / this._elementRect.width; | |
| pedestalY = this.truckSpeed * deltaY * (camera.top - camera.bottom) / camera.zoom / this._elementRect.height; | |
| } | |
| else { | |
| return; | |
| } | |
| if (screenSpacePanning) { | |
| dragToOffset ? | |
| this.setFocalOffset(this._focalOffsetEnd.x + truckX, this._focalOffsetEnd.y, this._focalOffsetEnd.z, true) : | |
| this.truck(truckX, 0, true); | |
| this.forward(-pedestalY, true); | |
| } | |
| else { | |
| dragToOffset ? | |
| this.setFocalOffset(this._focalOffsetEnd.x + truckX, this._focalOffsetEnd.y + pedestalY, this._focalOffsetEnd.z, true) : | |
| this.truck(truckX, pedestalY, true); | |
| } | |
| }; | |
| this._rotateInternal = (deltaX, deltaY) => { | |
| const theta = PI_2 * this.azimuthRotateSpeed * deltaX / this._elementRect.height; // divide by *height* to refer the resolution | |
| const phi = PI_2 * this.polarRotateSpeed * deltaY / this._elementRect.height; | |
| this.rotate(theta, phi, true); | |
| }; | |
| this._dollyInternal = (delta, x, y) => { | |
| const dollyScale = Math.pow(0.95, -delta * this.dollySpeed); | |
| const lastDistance = this._sphericalEnd.radius; | |
| const distance = this._sphericalEnd.radius * dollyScale; | |
| const clampedDistance = clamp(distance, this.minDistance, this.maxDistance); | |
| const overflowedDistance = clampedDistance - distance; | |
| if (this.infinityDolly && this.dollyToCursor) { | |
| this._dollyToNoClamp(distance, true); | |
| } | |
| else if (this.infinityDolly && !this.dollyToCursor) { | |
| this.dollyInFixed(overflowedDistance, true); | |
| this._dollyToNoClamp(clampedDistance, true); | |
| } | |
| else { | |
| this._dollyToNoClamp(clampedDistance, true); | |
| } | |
| if (this.dollyToCursor) { | |
| this._changedDolly += (this.infinityDolly ? distance : clampedDistance) - lastDistance; | |
| this._dollyControlCoord.set(x, y); | |
| } | |
| this._lastDollyDirection = Math.sign(-delta); | |
| }; | |
| this._zoomInternal = (delta, x, y) => { | |
| const zoomScale = Math.pow(0.95, delta * this.dollySpeed); | |
| const lastZoom = this._zoom; | |
| const zoom = this._zoom * zoomScale; | |
| // for both PerspectiveCamera and OrthographicCamera | |
| this.zoomTo(zoom, true); | |
| if (this.dollyToCursor) { | |
| this._changedZoom += zoom - lastZoom; | |
| this._dollyControlCoord.set(x, y); | |
| } | |
| }; | |
| // Check if the user has installed THREE | |
| if (typeof THREE === 'undefined') { | |
| console.error('camera-controls: `THREE` is undefined. You must first run `CameraControls.install( { THREE: THREE } )`. Check the docs for further information.'); | |
| } | |
| this._camera = camera; | |
| this._yAxisUpSpace = new THREE.Quaternion().setFromUnitVectors(this._camera.up, _AXIS_Y); | |
| this._yAxisUpSpaceInverse = this._yAxisUpSpace.clone().invert(); | |
| this._state = ACTION.NONE; | |
| // the location | |
| this._target = new THREE.Vector3(); | |
| this._targetEnd = this._target.clone(); | |
| this._focalOffset = new THREE.Vector3(); | |
| this._focalOffsetEnd = this._focalOffset.clone(); | |
| // rotation | |
| this._spherical = new THREE.Spherical().setFromVector3(_v3A.copy(this._camera.position).applyQuaternion(this._yAxisUpSpace)); | |
| this._sphericalEnd = this._spherical.clone(); | |
| this._lastDistance = this._spherical.radius; | |
| this._zoom = this._camera.zoom; | |
| this._zoomEnd = this._zoom; | |
| this._lastZoom = this._zoom; | |
| // collisionTest uses nearPlane.s | |
| this._nearPlaneCorners = [ | |
| new THREE.Vector3(), | |
| new THREE.Vector3(), | |
| new THREE.Vector3(), | |
| new THREE.Vector3(), | |
| ]; | |
| this._updateNearPlaneCorners(); | |
| // Target cannot move outside of this box | |
| this._boundary = new THREE.Box3(new THREE.Vector3(-Infinity, -Infinity, -Infinity), new THREE.Vector3(Infinity, Infinity, Infinity)); | |
| // reset | |
| this._cameraUp0 = this._camera.up.clone(); | |
| this._target0 = this._target.clone(); | |
| this._position0 = this._camera.position.clone(); | |
| this._zoom0 = this._zoom; | |
| this._focalOffset0 = this._focalOffset.clone(); | |
| this._dollyControlCoord = new THREE.Vector2(); | |
| // configs | |
| this.mouseButtons = { | |
| left: ACTION.ROTATE, | |
| middle: ACTION.DOLLY, | |
| right: ACTION.TRUCK, | |
| wheel: isPerspectiveCamera(this._camera) ? ACTION.DOLLY : | |
| isOrthographicCamera(this._camera) ? ACTION.ZOOM : | |
| ACTION.NONE, | |
| }; | |
| this.touches = { | |
| one: ACTION.TOUCH_ROTATE, | |
| two: isPerspectiveCamera(this._camera) ? ACTION.TOUCH_DOLLY_TRUCK : | |
| isOrthographicCamera(this._camera) ? ACTION.TOUCH_ZOOM_TRUCK : | |
| ACTION.NONE, | |
| three: ACTION.TOUCH_TRUCK, | |
| }; | |
| const dragStartPosition = new THREE.Vector2(); | |
| const lastDragPosition = new THREE.Vector2(); | |
| const dollyStart = new THREE.Vector2(); | |
| const onPointerDown = (event) => { | |
| if (!this._enabled || !this._domElement) | |
| return; | |
| if (this._interactiveArea.left !== 0 || | |
| this._interactiveArea.top !== 0 || | |
| this._interactiveArea.width !== 1 || | |
| this._interactiveArea.height !== 1) { | |
| const elRect = this._domElement.getBoundingClientRect(); | |
| const left = event.clientX / elRect.width; | |
| const top = event.clientY / elRect.height; | |
| // check if the interactiveArea contains the drag start position. | |
| if (left < this._interactiveArea.left || | |
| left > this._interactiveArea.right || | |
| top < this._interactiveArea.top || | |
| top > this._interactiveArea.bottom) | |
| return; | |
| } | |
| // Don't call `event.preventDefault()` on the pointerdown event | |
| // to keep receiving pointermove evens outside dragging iframe | |
| // https://taye.me/blog/tips/2015/11/16/mouse-drag-outside-iframe/ | |
| const mouseButton = event.pointerType !== 'mouse' ? null : | |
| (event.buttons & MOUSE_BUTTON.LEFT) === MOUSE_BUTTON.LEFT ? MOUSE_BUTTON.LEFT : | |
| (event.buttons & MOUSE_BUTTON.MIDDLE) === MOUSE_BUTTON.MIDDLE ? MOUSE_BUTTON.MIDDLE : | |
| (event.buttons & MOUSE_BUTTON.RIGHT) === MOUSE_BUTTON.RIGHT ? MOUSE_BUTTON.RIGHT : | |
| null; | |
| if (mouseButton !== null) { | |
| const zombiePointer = this._findPointerByMouseButton(mouseButton); | |
| zombiePointer && this._disposePointer(zombiePointer); | |
| } | |
| if ((event.buttons & MOUSE_BUTTON.LEFT) === MOUSE_BUTTON.LEFT && this._lockedPointer) | |
| return; | |
| const pointer = { | |
| pointerId: event.pointerId, | |
| clientX: event.clientX, | |
| clientY: event.clientY, | |
| deltaX: 0, | |
| deltaY: 0, | |
| mouseButton, | |
| }; | |
| this._activePointers.push(pointer); | |
| // eslint-disable-next-line no-undef | |
| this._domElement.ownerDocument.removeEventListener('pointermove', onPointerMove, { passive: false }); | |
| this._domElement.ownerDocument.removeEventListener('pointerup', onPointerUp); | |
| this._domElement.ownerDocument.addEventListener('pointermove', onPointerMove, { passive: false }); | |
| this._domElement.ownerDocument.addEventListener('pointerup', onPointerUp); | |
| this._isDragging = true; | |
| startDragging(event); | |
| }; | |
| const onPointerMove = (event) => { | |
| if (event.cancelable) | |
| event.preventDefault(); | |
| const pointerId = event.pointerId; | |
| const pointer = this._lockedPointer || this._findPointerById(pointerId); | |
| if (!pointer) | |
| return; | |
| pointer.clientX = event.clientX; | |
| pointer.clientY = event.clientY; | |
| pointer.deltaX = event.movementX; | |
| pointer.deltaY = event.movementY; | |
| this._state = 0; | |
| if (event.pointerType === 'touch') { | |
| switch (this._activePointers.length) { | |
| case 1: | |
| this._state = this.touches.one; | |
| break; | |
| case 2: | |
| this._state = this.touches.two; | |
| break; | |
| case 3: | |
| this._state = this.touches.three; | |
| break; | |
| } | |
| } | |
| else { | |
| if ((!this._isDragging && this._lockedPointer) || | |
| this._isDragging && (event.buttons & MOUSE_BUTTON.LEFT) === MOUSE_BUTTON.LEFT) { | |
| this._state = this._state | this.mouseButtons.left; | |
| } | |
| if (this._isDragging && (event.buttons & MOUSE_BUTTON.MIDDLE) === MOUSE_BUTTON.MIDDLE) { | |
| this._state = this._state | this.mouseButtons.middle; | |
| } | |
| if (this._isDragging && (event.buttons & MOUSE_BUTTON.RIGHT) === MOUSE_BUTTON.RIGHT) { | |
| this._state = this._state | this.mouseButtons.right; | |
| } | |
| } | |
| dragging(); | |
| }; | |
| const onPointerUp = (event) => { | |
| const pointer = this._findPointerById(event.pointerId); | |
| if (pointer && pointer === this._lockedPointer) | |
| return; | |
| pointer && this._disposePointer(pointer); | |
| if (event.pointerType === 'touch') { | |
| switch (this._activePointers.length) { | |
| case 0: | |
| this._state = ACTION.NONE; | |
| break; | |
| case 1: | |
| this._state = this.touches.one; | |
| break; | |
| case 2: | |
| this._state = this.touches.two; | |
| break; | |
| case 3: | |
| this._state = this.touches.three; | |
| break; | |
| } | |
| } | |
| else { | |
| this._state = ACTION.NONE; | |
| } | |
| endDragging(); | |
| }; | |
| let lastScrollTimeStamp = -1; | |
| const onMouseWheel = (event) => { | |
| if (!this._domElement) | |
| return; | |
| if (!this._enabled || this.mouseButtons.wheel === ACTION.NONE) | |
| return; | |
| if (this._interactiveArea.left !== 0 || | |
| this._interactiveArea.top !== 0 || | |
| this._interactiveArea.width !== 1 || | |
| this._interactiveArea.height !== 1) { | |
| const elRect = this._domElement.getBoundingClientRect(); | |
| const left = event.clientX / elRect.width; | |
| const top = event.clientY / elRect.height; | |
| // check if the interactiveArea contains the drag start position. | |
| if (left < this._interactiveArea.left || | |
| left > this._interactiveArea.right || | |
| top < this._interactiveArea.top || | |
| top > this._interactiveArea.bottom) | |
| return; | |
| } | |
| event.preventDefault(); | |
| if (this.dollyToCursor || | |
| this.mouseButtons.wheel === ACTION.ROTATE || | |
| this.mouseButtons.wheel === ACTION.TRUCK) { | |
| const now = performance.now(); | |
| // only need to fire this at scroll start. | |
| if (lastScrollTimeStamp - now < 1000) | |
| this._getClientRect(this._elementRect); | |
| lastScrollTimeStamp = now; | |
| } | |
| // Ref: https://github.com/cedricpinson/osgjs/blob/00e5a7e9d9206c06fdde0436e1d62ab7cb5ce853/sources/osgViewer/input/source/InputSourceMouse.js#L89-L103 | |
| const deltaYFactor = isMac ? -1 : -3; | |
| // Checks event.ctrlKey to detect multi-touch gestures on a trackpad. | |
| const delta = (event.deltaMode === 1 || event.ctrlKey) ? event.deltaY / deltaYFactor : event.deltaY / (deltaYFactor * 10); | |
| const x = this.dollyToCursor ? (event.clientX - this._elementRect.x) / this._elementRect.width * 2 - 1 : 0; | |
| const y = this.dollyToCursor ? (event.clientY - this._elementRect.y) / this._elementRect.height * -2 + 1 : 0; | |
| switch (this.mouseButtons.wheel) { | |
| case ACTION.ROTATE: { | |
| this._rotateInternal(event.deltaX, event.deltaY); | |
| this._isUserControllingRotate = true; | |
| break; | |
| } | |
| case ACTION.TRUCK: { | |
| this._truckInternal(event.deltaX, event.deltaY, false, false); | |
| this._isUserControllingTruck = true; | |
| break; | |
| } | |
| case ACTION.SCREEN_PAN: { | |
| this._truckInternal(event.deltaX, event.deltaY, false, true); | |
| this._isUserControllingTruck = true; | |
| break; | |
| } | |
| case ACTION.OFFSET: { | |
| this._truckInternal(event.deltaX, event.deltaY, true, false); | |
| this._isUserControllingOffset = true; | |
| break; | |
| } | |
| case ACTION.DOLLY: { | |
| this._dollyInternal(-delta, x, y); | |
| this._isUserControllingDolly = true; | |
| break; | |
| } | |
| case ACTION.ZOOM: { | |
| this._zoomInternal(-delta, x, y); | |
| this._isUserControllingZoom = true; | |
| break; | |
| } | |
| } | |
| this.dispatchEvent({ type: 'control' }); | |
| }; | |
| const onContextMenu = (event) => { | |
| if (!this._domElement || !this._enabled) | |
| return; | |
| // contextmenu event is fired right after pointerdown | |
| // remove attached handlers and active pointer, if interrupted by contextmenu. | |
| if (this.mouseButtons.right === CameraControls.ACTION.NONE) { | |
| const pointerId = event instanceof PointerEvent ? event.pointerId : 0; | |
| const pointer = this._findPointerById(pointerId); | |
| pointer && this._disposePointer(pointer); | |
| // eslint-disable-next-line no-undef | |
| this._domElement.ownerDocument.removeEventListener('pointermove', onPointerMove, { passive: false }); | |
| this._domElement.ownerDocument.removeEventListener('pointerup', onPointerUp); | |
| return; | |
| } | |
| event.preventDefault(); | |
| }; | |
| const startDragging = (event) => { | |
| if (!this._enabled) | |
| return; | |
| extractClientCoordFromEvent(this._activePointers, _v2); | |
| this._getClientRect(this._elementRect); | |
| dragStartPosition.copy(_v2); | |
| lastDragPosition.copy(_v2); | |
| const isMultiTouch = this._activePointers.length >= 2; | |
| if (isMultiTouch) { | |
| // 2 finger pinch | |
| const dx = _v2.x - this._activePointers[1].clientX; | |
| const dy = _v2.y - this._activePointers[1].clientY; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| dollyStart.set(0, distance); | |
| // center coords of 2 finger truck | |
| const x = (this._activePointers[0].clientX + this._activePointers[1].clientX) * 0.5; | |
| const y = (this._activePointers[0].clientY + this._activePointers[1].clientY) * 0.5; | |
| lastDragPosition.set(x, y); | |
| } | |
| this._state = 0; | |
| if (!event) { | |
| if (this._lockedPointer) | |
| this._state = this._state | this.mouseButtons.left; | |
| } | |
| else if ('pointerType' in event && event.pointerType === 'touch') { | |
| switch (this._activePointers.length) { | |
| case 1: | |
| this._state = this.touches.one; | |
| break; | |
| case 2: | |
| this._state = this.touches.two; | |
| break; | |
| case 3: | |
| this._state = this.touches.three; | |
| break; | |
| } | |
| } | |
| else { | |
| if (!this._lockedPointer && (event.buttons & MOUSE_BUTTON.LEFT) === MOUSE_BUTTON.LEFT) { | |
| this._state = this._state | this.mouseButtons.left; | |
| } | |
| if ((event.buttons & MOUSE_BUTTON.MIDDLE) === MOUSE_BUTTON.MIDDLE) { | |
| this._state = this._state | this.mouseButtons.middle; | |
| } | |
| if ((event.buttons & MOUSE_BUTTON.RIGHT) === MOUSE_BUTTON.RIGHT) { | |
| this._state = this._state | this.mouseButtons.right; | |
| } | |
| } | |
| // stop current movement on drag start | |
| // - rotate | |
| if ((this._state & ACTION.ROTATE) === ACTION.ROTATE || | |
| (this._state & ACTION.TOUCH_ROTATE) === ACTION.TOUCH_ROTATE || | |
| (this._state & ACTION.TOUCH_DOLLY_ROTATE) === ACTION.TOUCH_DOLLY_ROTATE || | |
| (this._state & ACTION.TOUCH_ZOOM_ROTATE) === ACTION.TOUCH_ZOOM_ROTATE) { | |
| this._sphericalEnd.theta = this._spherical.theta; | |
| this._sphericalEnd.phi = this._spherical.phi; | |
| this._thetaVelocity.value = 0; | |
| this._phiVelocity.value = 0; | |
| } | |
| // - truck and screen-pan | |
| if ((this._state & ACTION.TRUCK) === ACTION.TRUCK || | |
| (this._state & ACTION.SCREEN_PAN) === ACTION.SCREEN_PAN || | |
| (this._state & ACTION.TOUCH_TRUCK) === ACTION.TOUCH_TRUCK || | |
| (this._state & ACTION.TOUCH_SCREEN_PAN) === ACTION.TOUCH_SCREEN_PAN || | |
| (this._state & ACTION.TOUCH_DOLLY_TRUCK) === ACTION.TOUCH_DOLLY_TRUCK || | |
| (this._state & ACTION.TOUCH_DOLLY_SCREEN_PAN) === ACTION.TOUCH_DOLLY_SCREEN_PAN || | |
| (this._state & ACTION.TOUCH_ZOOM_TRUCK) === ACTION.TOUCH_ZOOM_TRUCK || | |
| (this._state & ACTION.TOUCH_ZOOM_SCREEN_PAN) === ACTION.TOUCH_DOLLY_SCREEN_PAN) { | |
| this._targetEnd.copy(this._target); | |
| this._targetVelocity.set(0, 0, 0); | |
| } | |
| // - dolly | |
| if ((this._state & ACTION.DOLLY) === ACTION.DOLLY || | |
| (this._state & ACTION.TOUCH_DOLLY) === ACTION.TOUCH_DOLLY || | |
| (this._state & ACTION.TOUCH_DOLLY_TRUCK) === ACTION.TOUCH_DOLLY_TRUCK || | |
| (this._state & ACTION.TOUCH_DOLLY_SCREEN_PAN) === ACTION.TOUCH_DOLLY_SCREEN_PAN || | |
| (this._state & ACTION.TOUCH_DOLLY_OFFSET) === ACTION.TOUCH_DOLLY_OFFSET || | |
| (this._state & ACTION.TOUCH_DOLLY_ROTATE) === ACTION.TOUCH_DOLLY_ROTATE) { | |
| this._sphericalEnd.radius = this._spherical.radius; | |
| this._radiusVelocity.value = 0; | |
| } | |
| // - zoom | |
| if ((this._state & ACTION.ZOOM) === ACTION.ZOOM || | |
| (this._state & ACTION.TOUCH_ZOOM) === ACTION.TOUCH_ZOOM || | |
| (this._state & ACTION.TOUCH_ZOOM_TRUCK) === ACTION.TOUCH_ZOOM_TRUCK || | |
| (this._state & ACTION.TOUCH_ZOOM_SCREEN_PAN) === ACTION.TOUCH_ZOOM_SCREEN_PAN || | |
| (this._state & ACTION.TOUCH_ZOOM_OFFSET) === ACTION.TOUCH_ZOOM_OFFSET || | |
| (this._state & ACTION.TOUCH_ZOOM_ROTATE) === ACTION.TOUCH_ZOOM_ROTATE) { | |
| this._zoomEnd = this._zoom; | |
| this._zoomVelocity.value = 0; | |
| } | |
| // - offset | |
| if ((this._state & ACTION.OFFSET) === ACTION.OFFSET || | |
| (this._state & ACTION.TOUCH_OFFSET) === ACTION.TOUCH_OFFSET || | |
| (this._state & ACTION.TOUCH_DOLLY_OFFSET) === ACTION.TOUCH_DOLLY_OFFSET || | |
| (this._state & ACTION.TOUCH_ZOOM_OFFSET) === ACTION.TOUCH_ZOOM_OFFSET) { | |
| this._focalOffsetEnd.copy(this._focalOffset); | |
| this._focalOffsetVelocity.set(0, 0, 0); | |
| } | |
| this.dispatchEvent({ type: 'controlstart' }); | |
| }; | |
| const dragging = () => { | |
| if (!this._enabled || !this._dragNeedsUpdate) | |
| return; | |
| this._dragNeedsUpdate = false; | |
| extractClientCoordFromEvent(this._activePointers, _v2); | |
| // When pointer lock is enabled clientX, clientY, screenX, and screenY remain 0. | |
| // If pointer lock is enabled, use the Delta directory, and assume active-pointer is not multiple. | |
| const isPointerLockActive = this._domElement && this._domElement.ownerDocument.pointerLockElement === this._domElement; | |
| const lockedPointer = isPointerLockActive ? this._lockedPointer || this._activePointers[0] : null; | |
| const deltaX = lockedPointer ? -lockedPointer.deltaX : lastDragPosition.x - _v2.x; | |
| const deltaY = lockedPointer ? -lockedPointer.deltaY : lastDragPosition.y - _v2.y; | |
| lastDragPosition.copy(_v2); | |
| // rotate | |
| if ((this._state & ACTION.ROTATE) === ACTION.ROTATE || | |
| (this._state & ACTION.TOUCH_ROTATE) === ACTION.TOUCH_ROTATE || | |
| (this._state & ACTION.TOUCH_DOLLY_ROTATE) === ACTION.TOUCH_DOLLY_ROTATE || | |
| (this._state & ACTION.TOUCH_ZOOM_ROTATE) === ACTION.TOUCH_ZOOM_ROTATE) { | |
| this._rotateInternal(deltaX, deltaY); | |
| this._isUserControllingRotate = true; | |
| } | |
| // mouse dolly or zoom | |
| if ((this._state & ACTION.DOLLY) === ACTION.DOLLY || | |
| (this._state & ACTION.ZOOM) === ACTION.ZOOM) { | |
| const dollyX = this.dollyToCursor ? (dragStartPosition.x - this._elementRect.x) / this._elementRect.width * 2 - 1 : 0; | |
| const dollyY = this.dollyToCursor ? (dragStartPosition.y - this._elementRect.y) / this._elementRect.height * -2 + 1 : 0; | |
| const dollyDirection = this.dollyDragInverted ? -1 : 1; | |
| if ((this._state & ACTION.DOLLY) === ACTION.DOLLY) { | |
| this._dollyInternal(dollyDirection * deltaY * TOUCH_DOLLY_FACTOR, dollyX, dollyY); | |
| this._isUserControllingDolly = true; | |
| } | |
| else { | |
| this._zoomInternal(dollyDirection * deltaY * TOUCH_DOLLY_FACTOR, dollyX, dollyY); | |
| this._isUserControllingZoom = true; | |
| } | |
| } | |
| // touch dolly or zoom | |
| if ((this._state & ACTION.TOUCH_DOLLY) === ACTION.TOUCH_DOLLY || | |
| (this._state & ACTION.TOUCH_ZOOM) === ACTION.TOUCH_ZOOM || | |
| (this._state & ACTION.TOUCH_DOLLY_TRUCK) === ACTION.TOUCH_DOLLY_TRUCK || | |
| (this._state & ACTION.TOUCH_ZOOM_TRUCK) === ACTION.TOUCH_ZOOM_TRUCK || | |
| (this._state & ACTION.TOUCH_DOLLY_SCREEN_PAN) === ACTION.TOUCH_DOLLY_SCREEN_PAN || | |
| (this._state & ACTION.TOUCH_ZOOM_SCREEN_PAN) === ACTION.TOUCH_ZOOM_SCREEN_PAN || | |
| (this._state & ACTION.TOUCH_DOLLY_OFFSET) === ACTION.TOUCH_DOLLY_OFFSET || | |
| (this._state & ACTION.TOUCH_ZOOM_OFFSET) === ACTION.TOUCH_ZOOM_OFFSET || | |
| (this._state & ACTION.TOUCH_DOLLY_ROTATE) === ACTION.TOUCH_DOLLY_ROTATE || | |
| (this._state & ACTION.TOUCH_ZOOM_ROTATE) === ACTION.TOUCH_ZOOM_ROTATE) { | |
| const dx = _v2.x - this._activePointers[1].clientX; | |
| const dy = _v2.y - this._activePointers[1].clientY; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| const dollyDelta = dollyStart.y - distance; | |
| dollyStart.set(0, distance); | |
| const dollyX = this.dollyToCursor ? (lastDragPosition.x - this._elementRect.x) / this._elementRect.width * 2 - 1 : 0; | |
| const dollyY = this.dollyToCursor ? (lastDragPosition.y - this._elementRect.y) / this._elementRect.height * -2 + 1 : 0; | |
| if ((this._state & ACTION.TOUCH_DOLLY) === ACTION.TOUCH_DOLLY || | |
| (this._state & ACTION.TOUCH_DOLLY_ROTATE) === ACTION.TOUCH_DOLLY_ROTATE || | |
| (this._state & ACTION.TOUCH_DOLLY_TRUCK) === ACTION.TOUCH_DOLLY_TRUCK || | |
| (this._state & ACTION.TOUCH_DOLLY_SCREEN_PAN) === ACTION.TOUCH_DOLLY_SCREEN_PAN || | |
| (this._state & ACTION.TOUCH_DOLLY_OFFSET) === ACTION.TOUCH_DOLLY_OFFSET) { | |
| this._dollyInternal(dollyDelta * TOUCH_DOLLY_FACTOR, dollyX, dollyY); | |
| this._isUserControllingDolly = true; | |
| } | |
| else { | |
| this._zoomInternal(dollyDelta * TOUCH_DOLLY_FACTOR, dollyX, dollyY); | |
| this._isUserControllingZoom = true; | |
| } | |
| } | |
| // truck | |
| if ((this._state & ACTION.TRUCK) === ACTION.TRUCK || | |
| (this._state & ACTION.TOUCH_TRUCK) === ACTION.TOUCH_TRUCK || | |
| (this._state & ACTION.TOUCH_DOLLY_TRUCK) === ACTION.TOUCH_DOLLY_TRUCK || | |
| (this._state & ACTION.TOUCH_ZOOM_TRUCK) === ACTION.TOUCH_ZOOM_TRUCK) { | |
| this._truckInternal(deltaX, deltaY, false, false); | |
| this._isUserControllingTruck = true; | |
| } | |
| // screen-pan | |
| if ((this._state & ACTION.SCREEN_PAN) === ACTION.SCREEN_PAN || | |
| (this._state & ACTION.TOUCH_SCREEN_PAN) === ACTION.TOUCH_SCREEN_PAN || | |
| (this._state & ACTION.TOUCH_DOLLY_SCREEN_PAN) === ACTION.TOUCH_DOLLY_SCREEN_PAN || | |
| (this._state & ACTION.TOUCH_ZOOM_SCREEN_PAN) === ACTION.TOUCH_ZOOM_SCREEN_PAN) { | |
| this._truckInternal(deltaX, deltaY, false, true); | |
| this._isUserControllingTruck = true; | |
| } | |
| // offset | |
| if ((this._state & ACTION.OFFSET) === ACTION.OFFSET || | |
| (this._state & ACTION.TOUCH_OFFSET) === ACTION.TOUCH_OFFSET || | |
| (this._state & ACTION.TOUCH_DOLLY_OFFSET) === ACTION.TOUCH_DOLLY_OFFSET || | |
| (this._state & ACTION.TOUCH_ZOOM_OFFSET) === ACTION.TOUCH_ZOOM_OFFSET) { | |
| this._truckInternal(deltaX, deltaY, true, false); | |
| this._isUserControllingOffset = true; | |
| } | |
| this.dispatchEvent({ type: 'control' }); | |
| }; | |
| const endDragging = () => { | |
| extractClientCoordFromEvent(this._activePointers, _v2); | |
| lastDragPosition.copy(_v2); | |
| this._dragNeedsUpdate = false; | |
| if (this._activePointers.length === 0 || | |
| (this._activePointers.length === 1 && this._activePointers[0] === this._lockedPointer)) { | |
| this._isDragging = false; | |
| } | |
| if (this._activePointers.length === 0 && this._domElement) { | |
| // eslint-disable-next-line no-undef | |
| this._domElement.ownerDocument.removeEventListener('pointermove', onPointerMove, { passive: false }); | |
| this._domElement.ownerDocument.removeEventListener('pointerup', onPointerUp); | |
| this.dispatchEvent({ type: 'controlend' }); | |
| } | |
| }; | |
| this.lockPointer = () => { | |
| if (!this._enabled || !this._domElement) | |
| return; | |
| this.cancel(); | |
| // Element.requestPointerLock is allowed to happen without any pointer active - create a faux one for compatibility with controls | |
| this._lockedPointer = { | |
| pointerId: -1, | |
| clientX: 0, | |
| clientY: 0, | |
| deltaX: 0, | |
| deltaY: 0, | |
| mouseButton: null, | |
| }; | |
| this._activePointers.push(this._lockedPointer); | |
| // eslint-disable-next-line no-undef | |
| this._domElement.ownerDocument.removeEventListener('pointermove', onPointerMove, { passive: false }); | |
| this._domElement.ownerDocument.removeEventListener('pointerup', onPointerUp); | |
| this._domElement.requestPointerLock(); | |
| this._domElement.ownerDocument.addEventListener('pointerlockchange', onPointerLockChange); | |
| this._domElement.ownerDocument.addEventListener('pointerlockerror', onPointerLockError); | |
| this._domElement.ownerDocument.addEventListener('pointermove', onPointerMove, { passive: false }); | |
| this._domElement.ownerDocument.addEventListener('pointerup', onPointerUp); | |
| startDragging(); | |
| }; | |
| this.unlockPointer = () => { | |
| var _a, _b, _c; | |
| if (this._lockedPointer !== null) { | |
| this._disposePointer(this._lockedPointer); | |
| this._lockedPointer = null; | |
| } | |
| (_a = this._domElement) === null || _a === void 0 ? void 0 : _a.ownerDocument.exitPointerLock(); | |
| (_b = this._domElement) === null || _b === void 0 ? void 0 : _b.ownerDocument.removeEventListener('pointerlockchange', onPointerLockChange); | |
| (_c = this._domElement) === null || _c === void 0 ? void 0 : _c.ownerDocument.removeEventListener('pointerlockerror', onPointerLockError); | |
| this.cancel(); | |
| }; | |
| const onPointerLockChange = () => { | |
| const isPointerLockActive = this._domElement && this._domElement.ownerDocument.pointerLockElement === this._domElement; | |
| if (!isPointerLockActive) | |
| this.unlockPointer(); | |
| }; | |
| const onPointerLockError = () => { | |
| this.unlockPointer(); | |
| }; | |
| this._addAllEventListeners = (domElement) => { | |
| this._domElement = domElement; | |
| this._domElement.style.touchAction = 'none'; | |
| this._domElement.style.userSelect = 'none'; | |
| this._domElement.style.webkitUserSelect = 'none'; | |
| this._domElement.addEventListener('pointerdown', onPointerDown); | |
| this._domElement.addEventListener('pointercancel', onPointerUp); | |
| this._domElement.addEventListener('wheel', onMouseWheel, { passive: false }); | |
| this._domElement.addEventListener('contextmenu', onContextMenu); | |
| }; | |
| this._removeAllEventListeners = () => { | |
| if (!this._domElement) | |
| return; | |
| this._domElement.style.touchAction = ''; | |
| this._domElement.style.userSelect = ''; | |
| this._domElement.style.webkitUserSelect = ''; | |
| this._domElement.removeEventListener('pointerdown', onPointerDown); | |
| this._domElement.removeEventListener('pointercancel', onPointerUp); | |
| // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener#matching_event_listeners_for_removal | |
| // > it's probably wise to use the same values used for the call to `addEventListener()` when calling `removeEventListener()` | |
| // see https://github.com/microsoft/TypeScript/issues/32912#issuecomment-522142969 | |
| // eslint-disable-next-line no-undef | |
| this._domElement.removeEventListener('wheel', onMouseWheel, { passive: false }); | |
| this._domElement.removeEventListener('contextmenu', onContextMenu); | |
| // eslint-disable-next-line no-undef | |
| this._domElement.ownerDocument.removeEventListener('pointermove', onPointerMove, { passive: false }); | |
| this._domElement.ownerDocument.removeEventListener('pointerup', onPointerUp); | |
| this._domElement.ownerDocument.removeEventListener('pointerlockchange', onPointerLockChange); | |
| this._domElement.ownerDocument.removeEventListener('pointerlockerror', onPointerLockError); | |
| }; | |
| this.cancel = () => { | |
| if (this._state === ACTION.NONE) | |
| return; | |
| this._state = ACTION.NONE; | |
| this._activePointers.length = 0; | |
| endDragging(); | |
| }; | |
| if (domElement) | |
| this.connect(domElement); | |
| this.update(0); | |
| } | |
| /** | |
| * The camera to be controlled | |
| * @category Properties | |
| */ | |
| get camera() { | |
| return this._camera; | |
| } | |
| set camera(camera) { | |
| this._camera = camera; | |
| this.updateCameraUp(); | |
| this._camera.updateProjectionMatrix(); | |
| this._updateNearPlaneCorners(); | |
| this._needsUpdate = true; | |
| } | |
| /** | |
| * Whether or not the controls are enabled. | |
| * `false` to disable user dragging/touch-move, but all methods works. | |
| * @category Properties | |
| */ | |
| get enabled() { | |
| return this._enabled; | |
| } | |
| set enabled(enabled) { | |
| this._enabled = enabled; | |
| if (!this._domElement) | |
| return; | |
| if (enabled) { | |
| this._domElement.style.touchAction = 'none'; | |
| this._domElement.style.userSelect = 'none'; | |
| this._domElement.style.webkitUserSelect = 'none'; | |
| } | |
| else { | |
| this.cancel(); | |
| this._domElement.style.touchAction = ''; | |
| this._domElement.style.userSelect = ''; | |
| this._domElement.style.webkitUserSelect = ''; | |
| } | |
| } | |
| /** | |
| * Returns `true` if the controls are active updating. | |
| * readonly value. | |
| * @category Properties | |
| */ | |
| get active() { | |
| return !this._hasRested; | |
| } | |
| /** | |
| * Getter for the current `ACTION`. | |
| * readonly value. | |
| * @category Properties | |
| */ | |
| get currentAction() { | |
| return this._state; | |
| } | |
| /** | |
| * get/set Current distance. | |
| * @category Properties | |
| */ | |
| get distance() { | |
| return this._spherical.radius; | |
| } | |
| set distance(distance) { | |
| if (this._spherical.radius === distance && | |
| this._sphericalEnd.radius === distance) | |
| return; | |
| this._spherical.radius = distance; | |
| this._sphericalEnd.radius = distance; | |
| this._needsUpdate = true; | |
| } | |
| // horizontal angle | |
| /** | |
| * get/set the azimuth angle (horizontal) in radians. | |
| * Every 360 degrees turn is added to `.azimuthAngle` value, which is accumulative. | |
| * @category Properties | |
| */ | |
| get azimuthAngle() { | |
| return this._spherical.theta; | |
| } | |
| set azimuthAngle(azimuthAngle) { | |
| if (this._spherical.theta === azimuthAngle && | |
| this._sphericalEnd.theta === azimuthAngle) | |
| return; | |
| this._spherical.theta = azimuthAngle; | |
| this._sphericalEnd.theta = azimuthAngle; | |
| this._needsUpdate = true; | |
| } | |
| // vertical angle | |
| /** | |
| * get/set the polar angle (vertical) in radians. | |
| * @category Properties | |
| */ | |
| get polarAngle() { | |
| return this._spherical.phi; | |
| } | |
| set polarAngle(polarAngle) { | |
| if (this._spherical.phi === polarAngle && | |
| this._sphericalEnd.phi === polarAngle) | |
| return; | |
| this._spherical.phi = polarAngle; | |
| this._sphericalEnd.phi = polarAngle; | |
| this._needsUpdate = true; | |
| } | |
| /** | |
| * Whether camera position should be enclosed in the boundary or not. | |
| * @category Properties | |
| */ | |
| get boundaryEnclosesCamera() { | |
| return this._boundaryEnclosesCamera; | |
| } | |
| set boundaryEnclosesCamera(boundaryEnclosesCamera) { | |
| this._boundaryEnclosesCamera = boundaryEnclosesCamera; | |
| this._needsUpdate = true; | |
| } | |
| /** | |
| * Set drag-start, touches and wheel enable area in the domElement. | |
| * each values are between `0` and `1` inclusive, where `0` is left/top and `1` is right/bottom of the screen. | |
| * e.g. `{ x: 0, y: 0, width: 1, height: 1 }` for entire area. | |
| * @category Properties | |
| */ | |
| set interactiveArea(interactiveArea) { | |
| this._interactiveArea.width = clamp(interactiveArea.width, 0, 1); | |
| this._interactiveArea.height = clamp(interactiveArea.height, 0, 1); | |
| this._interactiveArea.x = clamp(interactiveArea.x, 0, 1 - this._interactiveArea.width); | |
| this._interactiveArea.y = clamp(interactiveArea.y, 0, 1 - this._interactiveArea.height); | |
| } | |
| /** | |
| * Adds the specified event listener. | |
| * Applicable event types (which is `K`) are: | |
| * | Event name | Timing | | |
| * | ------------------- | ------ | | |
| * | `'controlstart'` | When the user starts to control the camera via mouse / touches. ¹ | | |
| * | `'control'` | When the user controls the camera (dragging). | | |
| * | `'controlend'` | When the user ends to control the camera. ¹ | | |
| * | `'transitionstart'` | When any kind of transition starts, either user control or using a method with `enableTransition = true` | | |
| * | `'update'` | When the camera position is updated. | | |
| * | `'wake'` | When the camera starts moving. | | |
| * | `'rest'` | When the camera movement is below `.restThreshold` ². | | |
| * | `'sleep'` | When the camera end moving. | | |
| * | |
| * 1. `mouseButtons.wheel` (Mouse wheel control) does not emit `'controlstart'` and `'controlend'`. `mouseButtons.wheel` uses scroll-event internally, and scroll-event happens intermittently. That means "start" and "end" cannot be detected. | |
| * 2. Due to damping, `sleep` will usually fire a few seconds after the camera _appears_ to have stopped moving. If you want to do something (e.g. enable UI, perform another transition) at the point when the camera has stopped, you probably want the `rest` event. This can be fine tuned using the `.restThreshold` parameter. See the [Rest and Sleep Example](https://yomotsu.github.io/camera-controls/examples/rest-and-sleep.html). | |
| * | |
| * e.g. | |
| * ``` | |
| * cameraControl.addEventListener( 'controlstart', myCallbackFunction ); | |
| * ``` | |
| * @param type event name | |
| * @param listener handler function | |
| * @category Methods | |
| */ | |
| addEventListener(type, listener) { | |
| super.addEventListener(type, listener); | |
| } | |
| /** | |
| * Removes the specified event listener | |
| * e.g. | |
| * ``` | |
| * cameraControl.addEventListener( 'controlstart', myCallbackFunction ); | |
| * ``` | |
| * @param type event name | |
| * @param listener handler function | |
| * @category Methods | |
| */ | |
| removeEventListener(type, listener) { | |
| super.removeEventListener(type, listener); | |
| } | |
| /** | |
| * Rotate azimuthal angle(horizontal) and polar angle(vertical). | |
| * Every value is added to the current value. | |
| * @param azimuthAngle Azimuth rotate angle. In radian. | |
| * @param polarAngle Polar rotate angle. In radian. | |
| * @param enableTransition Whether to move smoothly or immediately | |
| * @category Methods | |
| */ | |
| rotate(azimuthAngle, polarAngle, enableTransition = false) { | |
| return this.rotateTo(this._sphericalEnd.theta + azimuthAngle, this._sphericalEnd.phi + polarAngle, enableTransition); | |
| } | |
| /** | |
| * Rotate azimuthal angle(horizontal) to the given angle and keep the same polar angle(vertical) target. | |
| * | |
| * e.g. | |
| * ``` | |
| * cameraControls.rotateAzimuthTo( 30 * THREE.MathUtils.DEG2RAD, true ); | |
| * ``` | |
| * @param azimuthAngle Azimuth rotate angle. In radian. | |
| * @param enableTransition Whether to move smoothly or immediately | |
| * @category Methods | |
| */ | |
| rotateAzimuthTo(azimuthAngle, enableTransition = false) { | |
| return this.rotateTo(azimuthAngle, this._sphericalEnd.phi, enableTransition); | |
| } | |
| /** | |
| * Rotate polar angle(vertical) to the given angle and keep the same azimuthal angle(horizontal) target. | |
| * | |
| * e.g. | |
| * ``` | |
| * cameraControls.rotatePolarTo( 30 * THREE.MathUtils.DEG2RAD, true ); | |
| * ``` | |
| * @param polarAngle Polar rotate angle. In radian. | |
| * @param enableTransition Whether to move smoothly or immediately | |
| * @category Methods | |
| */ | |
| rotatePolarTo(polarAngle, enableTransition = false) { | |
| return this.rotateTo(this._sphericalEnd.theta, polarAngle, enableTransition); | |
| } | |
| /** | |
| * Rotate azimuthal angle(horizontal) and polar angle(vertical) to the given angle. | |
| * Camera view will rotate over the orbit pivot absolutely: | |
| * | |
| * azimuthAngle | |
| * ``` | |
| * 0º | |
| * \ | |
| * 90º -----+----- -90º | |
| * \ | |
| * 180º | |
| * ``` | |
| * | direction | angle | | |
| * | --------- | ---------------------- | | |
| * | front | 0º | | |
| * | left | 90º (`Math.PI / 2`) | | |
| * | right | -90º (`- Math.PI / 2`) | | |
| * | back | 180º (`Math.PI`) | | |
| * | |
| * polarAngle | |
| * ``` | |
| * 180º | |
| * | | |
| * 90º | |
| * | | |
| * 0º | |
| * ``` | |
| * | direction | angle | | |
| * | -------------------- | ---------------------- | | |
| * | top/sky | 180º (`Math.PI`) | | |
| * | horizontal from view | 90º (`Math.PI / 2`) | | |
| * | bottom/floor | 0º | | |
| * | |
| * @param azimuthAngle Azimuth rotate angle to. In radian. | |
| * @param polarAngle Polar rotate angle to. In radian. | |
| * @param enableTransition Whether to move smoothly or immediately | |
| * @category Methods | |
| */ | |
| rotateTo(azimuthAngle, polarAngle, enableTransition = false) { | |
| this._isUserControllingRotate = false; | |
| const theta = clamp(azimuthAngle, this.minAzimuthAngle, this.maxAzimuthAngle); | |
| const phi = clamp(polarAngle, this.minPolarAngle, this.maxPolarAngle); | |
| this._sphericalEnd.theta = theta; | |
| this._sphericalEnd.phi = phi; | |
| this._sphericalEnd.makeSafe(); | |
| this._needsUpdate = true; | |
| if (!enableTransition) { | |
| this._spherical.theta = this._sphericalEnd.theta; | |
| this._spherical.phi = this._sphericalEnd.phi; | |
| } | |
| const resolveImmediately = !enableTransition || | |
| approxEquals(this._spherical.theta, this._sphericalEnd.theta, this.restThreshold) && | |
| approxEquals(this._spherical.phi, this._sphericalEnd.phi, this.restThreshold); | |
| return this._createOnRestPromise(resolveImmediately); | |
| } | |
| /** | |
| * Dolly in/out camera position. | |
| * @param distance Distance of dollyIn. Negative number for dollyOut. | |
| * @param enableTransition Whether to move smoothly or immediately. | |
| * @category Methods | |
| */ | |
| dolly(distance, enableTransition = false) { | |
| return this.dollyTo(this._sphericalEnd.radius - distance, enableTransition); | |
| } | |
| /** | |
| * Dolly in/out camera position to given distance. | |
| * @param distance Distance of dolly. | |
| * @param enableTransition Whether to move smoothly or immediately. | |
| * @category Methods | |
| */ | |
| dollyTo(distance, enableTransition = false) { | |
| this._isUserControllingDolly = false; | |
| this._lastDollyDirection = DOLLY_DIRECTION.NONE; | |
| this._changedDolly = 0; | |
| return this._dollyToNoClamp(clamp(distance, this.minDistance, this.maxDistance), enableTransition); | |
| } | |
| _dollyToNoClamp(distance, enableTransition = false) { | |
| const lastRadius = this._sphericalEnd.radius; | |
| const hasCollider = this.colliderMeshes.length >= 1; | |
| if (hasCollider) { | |
| const maxDistanceByCollisionTest = this._collisionTest(); | |
| const isCollided = approxEquals(maxDistanceByCollisionTest, this._spherical.radius); | |
| const isDollyIn = lastRadius > distance; | |
| if (!isDollyIn && isCollided) | |
| return Promise.resolve(); | |
| this._sphericalEnd.radius = Math.min(distance, maxDistanceByCollisionTest); | |
| } | |
| else { | |
| this._sphericalEnd.radius = distance; | |
| } | |
| this._needsUpdate = true; | |
| if (!enableTransition) { | |
| this._spherical.radius = this._sphericalEnd.radius; | |
| } | |
| const resolveImmediately = !enableTransition || approxEquals(this._spherical.radius, this._sphericalEnd.radius, this.restThreshold); | |
| return this._createOnRestPromise(resolveImmediately); | |
| } | |
| /** | |
| * Dolly in, but does not change the distance between the target and the camera, and moves the target position instead. | |
| * Specify a negative value for dolly out. | |
| * @param distance Distance of dolly. | |
| * @param enableTransition Whether to move smoothly or immediately. | |
| * @category Methods | |
| */ | |
| dollyInFixed(distance, enableTransition = false) { | |
| this._targetEnd.add(this._getCameraDirection(_cameraDirection).multiplyScalar(distance)); | |
| if (!enableTransition) { | |
| this._target.copy(this._targetEnd); | |
| } | |
| const resolveImmediately = !enableTransition || | |
| approxEquals(this._target.x, this._targetEnd.x, this.restThreshold) && | |
| approxEquals(this._target.y, this._targetEnd.y, this.restThreshold) && | |
| approxEquals(this._target.z, this._targetEnd.z, this.restThreshold); | |
| return this._createOnRestPromise(resolveImmediately); | |
| } | |
| /** | |
| * Zoom in/out camera. The value is added to camera zoom. | |
| * Limits set with `.minZoom` and `.maxZoom` | |
| * @param zoomStep zoom scale | |
| * @param enableTransition Whether to move smoothly or immediately | |
| * @category Methods | |
| */ | |
| zoom(zoomStep, enableTransition = false) { | |
| return this.zoomTo(this._zoomEnd + zoomStep, enableTransition); | |
| } | |
| /** | |
| * Zoom in/out camera to given scale. The value overwrites camera zoom. | |
| * Limits set with .minZoom and .maxZoom | |
| * @param zoom | |
| * @param enableTransition | |
| * @category Methods | |
| */ | |
| zoomTo(zoom, enableTransition = false) { | |
| this._isUserControllingZoom = false; | |
| this._zoomEnd = clamp(zoom, this.minZoom, this.maxZoom); | |
| this._needsUpdate = true; | |
| if (!enableTransition) { | |
| this._zoom = this._zoomEnd; | |
| } | |
| const resolveImmediately = !enableTransition || approxEquals(this._zoom, this._zoomEnd, this.restThreshold); | |
| this._changedZoom = 0; | |
| return this._createOnRestPromise(resolveImmediately); | |
| } | |
| /** | |
| * @deprecated `pan()` has been renamed to `truck()` | |
| * @category Methods | |
| */ | |
| pan(x, y, enableTransition = false) { | |
| console.warn('`pan` has been renamed to `truck`'); | |
| return this.truck(x, y, enableTransition); | |
| } | |
| /** | |
| * Truck and pedestal camera using current azimuthal angle | |
| * @param x Horizontal translate amount | |
| * @param y Vertical translate amount | |
| * @param enableTransition Whether to move smoothly or immediately | |
| * @category Methods | |
| */ | |
| truck(x, y, enableTransition = false) { | |
| this._camera.updateMatrix(); | |
| _xColumn.setFromMatrixColumn(this._camera.matrix, 0); | |
| _yColumn.setFromMatrixColumn(this._camera.matrix, 1); | |
| _xColumn.multiplyScalar(x); | |
| _yColumn.multiplyScalar(-y); | |
| const offset = _v3A.copy(_xColumn).add(_yColumn); | |
| const to = _v3B.copy(this._targetEnd).add(offset); | |
| return this.moveTo(to.x, to.y, to.z, enableTransition); | |
| } | |
| /** | |
| * Move forward / backward. | |
| * @param distance Amount to move forward / backward. Negative value to move backward | |
| * @param enableTransition Whether to move smoothly or immediately | |
| * @category Methods | |
| */ | |
| forward(distance, enableTransition = false) { | |
| _v3A.setFromMatrixColumn(this._camera.matrix, 0); | |
| _v3A.crossVectors(this._camera.up, _v3A); | |
| _v3A.multiplyScalar(distance); | |
| const to = _v3B.copy(this._targetEnd).add(_v3A); | |
| return this.moveTo(to.x, to.y, to.z, enableTransition); | |
| } | |
| /** | |
| * Move up / down. | |
| * @param height Amount to move up / down. Negative value to move down | |
| * @param enableTransition Whether to move smoothly or immediately | |
| * @category Methods | |
| */ | |
| elevate(height, enableTransition = false) { | |
| _v3A.copy(this._camera.up).multiplyScalar(height); | |
| return this.moveTo(this._targetEnd.x + _v3A.x, this._targetEnd.y + _v3A.y, this._targetEnd.z + _v3A.z, enableTransition); | |
| } | |
| /** | |
| * Move target position to given point. | |
| * @param x x coord to move center position | |
| * @param y y coord to move center position | |
| * @param z z coord to move center position | |
| * @param enableTransition Whether to move smoothly or immediately | |
| * @category Methods | |
| */ | |
| moveTo(x, y, z, enableTransition = false) { | |
| this._isUserControllingTruck = false; | |
| const offset = _v3A.set(x, y, z).sub(this._targetEnd); | |
| this._encloseToBoundary(this._targetEnd, offset, this.boundaryFriction); | |
| this._needsUpdate = true; | |
| if (!enableTransition) { | |
| this._target.copy(this._targetEnd); | |
| } | |
| const resolveImmediately = !enableTransition || | |
| approxEquals(this._target.x, this._targetEnd.x, this.restThreshold) && | |
| approxEquals(this._target.y, this._targetEnd.y, this.restThreshold) && | |
| approxEquals(this._target.z, this._targetEnd.z, this.restThreshold); | |
| return this._createOnRestPromise(resolveImmediately); | |
| } | |
| /** | |
| * Look in the given point direction. | |
| * @param x point x. | |
| * @param y point y. | |
| * @param z point z. | |
| * @param enableTransition Whether to move smoothly or immediately. | |
| * @returns Transition end promise | |
| * @category Methods | |
| */ | |
| lookInDirectionOf(x, y, z, enableTransition = false) { | |
| const point = _v3A.set(x, y, z); | |
| const direction = point.sub(this._targetEnd).normalize(); | |
| const position = direction.multiplyScalar(-this._sphericalEnd.radius).add(this._targetEnd); | |
| return this.setPosition(position.x, position.y, position.z, enableTransition); | |
| } | |
| /** | |
| * Fit the viewport to the box or the bounding box of the object, using the nearest axis. paddings are in unit. | |
| * set `cover: true` to fill enter screen. | |
| * e.g. | |
| * ``` | |
| * cameraControls.fitToBox( myMesh ); | |
| * ``` | |
| * @param box3OrObject Axis aligned bounding box to fit the view. | |
| * @param enableTransition Whether to move smoothly or immediately. | |
| * @param options | `<object>` { cover: boolean, paddingTop: number, paddingLeft: number, paddingBottom: number, paddingRight: number } | |
| * @returns Transition end promise | |
| * @category Methods | |
| */ | |
| fitToBox(box3OrObject, enableTransition, { cover = false, paddingLeft = 0, paddingRight = 0, paddingBottom = 0, paddingTop = 0 } = {}) { | |
| const promises = []; | |
| const aabb = box3OrObject.isBox3 | |
| ? _box3A.copy(box3OrObject) | |
| : _box3A.setFromObject(box3OrObject); | |
| if (aabb.isEmpty()) { | |
| console.warn('camera-controls: fitTo() cannot be used with an empty box. Aborting'); | |
| Promise.resolve(); | |
| } | |
| // round to closest axis ( forward | backward | right | left | top | bottom ) | |
| const theta = roundToStep(this._sphericalEnd.theta, PI_HALF); | |
| const phi = roundToStep(this._sphericalEnd.phi, PI_HALF); | |
| promises.push(this.rotateTo(theta, phi, enableTransition)); | |
| const normal = _v3A.setFromSpherical(this._sphericalEnd).normalize(); | |
| const rotation = _quaternionA.setFromUnitVectors(normal, _AXIS_Z); | |
| const viewFromPolar = approxEquals(Math.abs(normal.y), 1); | |
| if (viewFromPolar) { | |
| rotation.multiply(_quaternionB.setFromAxisAngle(_AXIS_Y, theta)); | |
| } | |
| rotation.multiply(this._yAxisUpSpaceInverse); | |
| // make oriented bounding box | |
| const bb = _box3B.makeEmpty(); | |
| // left bottom back corner | |
| _v3B.copy(aabb.min).applyQuaternion(rotation); | |
| bb.expandByPoint(_v3B); | |
| // right bottom back corner | |
| _v3B.copy(aabb.min).setX(aabb.max.x).applyQuaternion(rotation); | |
| bb.expandByPoint(_v3B); | |
| // left top back corner | |
| _v3B.copy(aabb.min).setY(aabb.max.y).applyQuaternion(rotation); | |
| bb.expandByPoint(_v3B); | |
| // right top back corner | |
| _v3B.copy(aabb.max).setZ(aabb.min.z).applyQuaternion(rotation); | |
| bb.expandByPoint(_v3B); | |
| // left bottom front corner | |
| _v3B.copy(aabb.min).setZ(aabb.max.z).applyQuaternion(rotation); | |
| bb.expandByPoint(_v3B); | |
| // right bottom front corner | |
| _v3B.copy(aabb.max).setY(aabb.min.y).applyQuaternion(rotation); | |
| bb.expandByPoint(_v3B); | |
| // left top front corner | |
| _v3B.copy(aabb.max).setX(aabb.min.x).applyQuaternion(rotation); | |
| bb.expandByPoint(_v3B); | |
| // right top front corner | |
| _v3B.copy(aabb.max).applyQuaternion(rotation); | |
| bb.expandByPoint(_v3B); | |
| // add padding | |
| bb.min.x -= paddingLeft; | |
| bb.min.y -= paddingBottom; | |
| bb.max.x += paddingRight; | |
| bb.max.y += paddingTop; | |
| rotation.setFromUnitVectors(_AXIS_Z, normal); | |
| if (viewFromPolar) { | |
| rotation.premultiply(_quaternionB.invert()); | |
| } | |
| rotation.premultiply(this._yAxisUpSpace); | |
| const bbSize = bb.getSize(_v3A); | |
| const center = bb.getCenter(_v3B).applyQuaternion(rotation); | |
| if (isPerspectiveCamera(this._camera)) { | |
| const distance = this.getDistanceToFitBox(bbSize.x, bbSize.y, bbSize.z, cover); | |
| promises.push(this.moveTo(center.x, center.y, center.z, enableTransition)); | |
| promises.push(this.dollyTo(distance, enableTransition)); | |
| promises.push(this.setFocalOffset(0, 0, 0, enableTransition)); | |
| } | |
| else if (isOrthographicCamera(this._camera)) { | |
| const camera = this._camera; | |
| const width = camera.right - camera.left; | |
| const height = camera.top - camera.bottom; | |
| const zoom = cover ? Math.max(width / bbSize.x, height / bbSize.y) : Math.min(width / bbSize.x, height / bbSize.y); | |
| promises.push(this.moveTo(center.x, center.y, center.z, enableTransition)); | |
| promises.push(this.zoomTo(zoom, enableTransition)); | |
| promises.push(this.setFocalOffset(0, 0, 0, enableTransition)); | |
| } | |
| return Promise.all(promises); | |
| } | |
| /** | |
| * Fit the viewport to the sphere or the bounding sphere of the object. | |
| * @param sphereOrMesh | |
| * @param enableTransition | |
| * @category Methods | |
| */ | |
| fitToSphere(sphereOrMesh, enableTransition) { | |
| const promises = []; | |
| const isObject3D = 'isObject3D' in sphereOrMesh; | |
| const boundingSphere = isObject3D ? | |
| CameraControls.createBoundingSphere(sphereOrMesh, _sphere) : | |
| _sphere.copy(sphereOrMesh); | |
| promises.push(this.moveTo(boundingSphere.center.x, boundingSphere.center.y, boundingSphere.center.z, enableTransition)); | |
| if (isPerspectiveCamera(this._camera)) { | |
| const distanceToFit = this.getDistanceToFitSphere(boundingSphere.radius); | |
| promises.push(this.dollyTo(distanceToFit, enableTransition)); | |
| } | |
| else if (isOrthographicCamera(this._camera)) { | |
| const width = this._camera.right - this._camera.left; | |
| const height = this._camera.top - this._camera.bottom; | |
| const diameter = 2 * boundingSphere.radius; | |
| const zoom = Math.min(width / diameter, height / diameter); | |
| promises.push(this.zoomTo(zoom, enableTransition)); | |
| } | |
| promises.push(this.setFocalOffset(0, 0, 0, enableTransition)); | |
| return Promise.all(promises); | |
| } | |
| /** | |
| * Look at the `target` from the `position`. | |
| * @param positionX | |
| * @param positionY | |
| * @param positionZ | |
| * @param targetX | |
| * @param targetY | |
| * @param targetZ | |
| * @param enableTransition | |
| * @category Methods | |
| */ | |
| setLookAt(positionX, positionY, positionZ, targetX, targetY, targetZ, enableTransition = false) { | |
| this._isUserControllingRotate = false; | |
| this._isUserControllingDolly = false; | |
| this._isUserControllingTruck = false; | |
| this._lastDollyDirection = DOLLY_DIRECTION.NONE; | |
| this._changedDolly = 0; | |
| const target = _v3B.set(targetX, targetY, targetZ); | |
| const position = _v3A.set(positionX, positionY, positionZ); | |
| this._targetEnd.copy(target); | |
| this._sphericalEnd.setFromVector3(position.sub(target).applyQuaternion(this._yAxisUpSpace)); | |
| this.normalizeRotations(); | |
| this._needsUpdate = true; | |
| if (!enableTransition) { | |
| this._target.copy(this._targetEnd); | |
| this._spherical.copy(this._sphericalEnd); | |
| } | |
| const resolveImmediately = !enableTransition || | |
| approxEquals(this._target.x, this._targetEnd.x, this.restThreshold) && | |
| approxEquals(this._target.y, this._targetEnd.y, this.restThreshold) && | |
| approxEquals(this._target.z, this._targetEnd.z, this.restThreshold) && | |
| approxEquals(this._spherical.theta, this._sphericalEnd.theta, this.restThreshold) && | |
| approxEquals(this._spherical.phi, this._sphericalEnd.phi, this.restThreshold) && | |
| approxEquals(this._spherical.radius, this._sphericalEnd.radius, this.restThreshold); | |
| return this._createOnRestPromise(resolveImmediately); | |
| } | |
| /** | |
| * Similar to setLookAt, but it interpolates between two states. | |
| * @param positionAX | |
| * @param positionAY | |
| * @param positionAZ | |
| * @param targetAX | |
| * @param targetAY | |
| * @param targetAZ | |
| * @param positionBX | |
| * @param positionBY | |
| * @param positionBZ | |
| * @param targetBX | |
| * @param targetBY | |
| * @param targetBZ | |
| * @param t | |
| * @param enableTransition | |
| * @category Methods | |
| */ | |
| lerpLookAt(positionAX, positionAY, positionAZ, targetAX, targetAY, targetAZ, positionBX, positionBY, positionBZ, targetBX, targetBY, targetBZ, t, enableTransition = false) { | |
| this._isUserControllingRotate = false; | |
| this._isUserControllingDolly = false; | |
| this._isUserControllingTruck = false; | |
| this._lastDollyDirection = DOLLY_DIRECTION.NONE; | |
| this._changedDolly = 0; | |
| const targetA = _v3A.set(targetAX, targetAY, targetAZ); | |
| const positionA = _v3B.set(positionAX, positionAY, positionAZ); | |
| _sphericalA.setFromVector3(positionA.sub(targetA).applyQuaternion(this._yAxisUpSpace)); | |
| const targetB = _v3C.set(targetBX, targetBY, targetBZ); | |
| const positionB = _v3B.set(positionBX, positionBY, positionBZ); | |
| _sphericalB.setFromVector3(positionB.sub(targetB).applyQuaternion(this._yAxisUpSpace)); | |
| this._targetEnd.copy(targetA.lerp(targetB, t)); // tricky | |
| const deltaTheta = _sphericalB.theta - _sphericalA.theta; | |
| const deltaPhi = _sphericalB.phi - _sphericalA.phi; | |
| const deltaRadius = _sphericalB.radius - _sphericalA.radius; | |
| this._sphericalEnd.set(_sphericalA.radius + deltaRadius * t, _sphericalA.phi + deltaPhi * t, _sphericalA.theta + deltaTheta * t); | |
| this.normalizeRotations(); | |
| this._needsUpdate = true; | |
| if (!enableTransition) { | |
| this._target.copy(this._targetEnd); | |
| this._spherical.copy(this._sphericalEnd); | |
| } | |
| const resolveImmediately = !enableTransition || | |
| approxEquals(this._target.x, this._targetEnd.x, this.restThreshold) && | |
| approxEquals(this._target.y, this._targetEnd.y, this.restThreshold) && | |
| approxEquals(this._target.z, this._targetEnd.z, this.restThreshold) && | |
| approxEquals(this._spherical.theta, this._sphericalEnd.theta, this.restThreshold) && | |
| approxEquals(this._spherical.phi, this._sphericalEnd.phi, this.restThreshold) && | |
| approxEquals(this._spherical.radius, this._sphericalEnd.radius, this.restThreshold); | |
| return this._createOnRestPromise(resolveImmediately); | |
| } | |
| /** | |
| * Set angle and distance by given position. | |
| * An alias of `setLookAt()`, without target change. Thus keep gazing at the current target | |
| * @param positionX | |
| * @param positionY | |
| * @param positionZ | |
| * @param enableTransition | |
| * @category Methods | |
| */ | |
| setPosition(positionX, positionY, positionZ, enableTransition = false) { | |
| return this.setLookAt(positionX, positionY, positionZ, this._targetEnd.x, this._targetEnd.y, this._targetEnd.z, enableTransition); | |
| } | |
| /** | |
| * Set the target position where gaze at. | |
| * An alias of `setLookAt()`, without position change. Thus keep the same position. | |
| * @param targetX | |
| * @param targetY | |
| * @param targetZ | |
| * @param enableTransition | |
| * @category Methods | |
| */ | |
| setTarget(targetX, targetY, targetZ, enableTransition = false) { | |
| const pos = this.getPosition(_v3A); | |
| const promise = this.setLookAt(pos.x, pos.y, pos.z, targetX, targetY, targetZ, enableTransition); | |
| // see https://github.com/yomotsu/camera-controls/issues/335 | |
| this._sphericalEnd.phi = clamp(this._sphericalEnd.phi, this.minPolarAngle, this.maxPolarAngle); | |
| return promise; | |
| } | |
| /** | |
| * Set focal offset using the screen parallel coordinates. z doesn't affect in Orthographic as with Dolly. | |
| * @param x | |
| * @param y | |
| * @param z | |
| * @param enableTransition | |
| * @category Methods | |
| */ | |
| setFocalOffset(x, y, z, enableTransition = false) { | |
| this._isUserControllingOffset = false; | |
| this._focalOffsetEnd.set(x, y, z); | |
| this._needsUpdate = true; | |
| if (!enableTransition) | |
| this._focalOffset.copy(this._focalOffsetEnd); | |
| const resolveImmediately = !enableTransition || | |
| approxEquals(this._focalOffset.x, this._focalOffsetEnd.x, this.restThreshold) && | |
| approxEquals(this._focalOffset.y, this._focalOffsetEnd.y, this.restThreshold) && | |
| approxEquals(this._focalOffset.z, this._focalOffsetEnd.z, this.restThreshold); | |
| return this._createOnRestPromise(resolveImmediately); | |
| } | |
| /** | |
| * Set orbit point without moving the camera. | |
| * SHOULD NOT RUN DURING ANIMATIONS. `setOrbitPoint()` will immediately fix the positions. | |
| * @param targetX | |
| * @param targetY | |
| * @param targetZ | |
| * @category Methods | |
| */ | |
| setOrbitPoint(targetX, targetY, targetZ) { | |
| this._camera.updateMatrixWorld(); | |
| _xColumn.setFromMatrixColumn(this._camera.matrixWorldInverse, 0); | |
| _yColumn.setFromMatrixColumn(this._camera.matrixWorldInverse, 1); | |
| _zColumn.setFromMatrixColumn(this._camera.matrixWorldInverse, 2); | |
| const position = _v3A.set(targetX, targetY, targetZ); | |
| const distance = position.distanceTo(this._camera.position); | |
| const cameraToPoint = position.sub(this._camera.position); | |
| _xColumn.multiplyScalar(cameraToPoint.x); | |
| _yColumn.multiplyScalar(cameraToPoint.y); | |
| _zColumn.multiplyScalar(cameraToPoint.z); | |
| _v3A.copy(_xColumn).add(_yColumn).add(_zColumn); | |
| _v3A.z = _v3A.z + distance; | |
| this.dollyTo(distance, false); | |
| this.setFocalOffset(-_v3A.x, _v3A.y, -_v3A.z, false); | |
| this.moveTo(targetX, targetY, targetZ, false); | |
| } | |
| /** | |
| * Set the boundary box that encloses the target of the camera. box3 is in THREE.Box3 | |
| * @param box3 | |
| * @category Methods | |
| */ | |
| setBoundary(box3) { | |
| if (!box3) { | |
| this._boundary.min.set(-Infinity, -Infinity, -Infinity); | |
| this._boundary.max.set(Infinity, Infinity, Infinity); | |
| this._needsUpdate = true; | |
| return; | |
| } | |
| this._boundary.copy(box3); | |
| this._boundary.clampPoint(this._targetEnd, this._targetEnd); | |
| this._needsUpdate = true; | |
| } | |
| /** | |
| * Set (or unset) the current viewport. | |
| * Set this when you want to use renderer viewport and .dollyToCursor feature at the same time. | |
| * @param viewportOrX | |
| * @param y | |
| * @param width | |
| * @param height | |
| * @category Methods | |
| */ | |
| setViewport(viewportOrX, y, width, height) { | |
| if (viewportOrX === null) { // null | |
| this._viewport = null; | |
| return; | |
| } | |
| this._viewport = this._viewport || new THREE.Vector4(); | |
| if (typeof viewportOrX === 'number') { // number | |
| this._viewport.set(viewportOrX, y, width, height); | |
| } | |
| else { // Vector4 | |
| this._viewport.copy(viewportOrX); | |
| } | |
| } | |
| /** | |
| * Calculate the distance to fit the box. | |
| * @param width box width | |
| * @param height box height | |
| * @param depth box depth | |
| * @returns distance | |
| * @category Methods | |
| */ | |
| getDistanceToFitBox(width, height, depth, cover = false) { | |
| if (notSupportedInOrthographicCamera(this._camera, 'getDistanceToFitBox')) | |
| return this._spherical.radius; | |
| const boundingRectAspect = width / height; | |
| const fov = this._camera.getEffectiveFOV() * DEG2RAD; | |
| const aspect = this._camera.aspect; | |
| const heightToFit = (cover ? boundingRectAspect > aspect : boundingRectAspect < aspect) ? height : width / aspect; | |
| return heightToFit * 0.5 / Math.tan(fov * 0.5) + depth * 0.5; | |
| } | |
| /** | |
| * Calculate the distance to fit the sphere. | |
| * @param radius sphere radius | |
| * @returns distance | |
| * @category Methods | |
| */ | |
| getDistanceToFitSphere(radius) { | |
| if (notSupportedInOrthographicCamera(this._camera, 'getDistanceToFitSphere')) | |
| return this._spherical.radius; | |
| // https://stackoverflow.com/a/44849975 | |
| const vFOV = this._camera.getEffectiveFOV() * DEG2RAD; | |
| const hFOV = Math.atan(Math.tan(vFOV * 0.5) * this._camera.aspect) * 2; | |
| const fov = 1 < this._camera.aspect ? vFOV : hFOV; | |
| return radius / (Math.sin(fov * 0.5)); | |
| } | |
| /** | |
| * Returns the orbit center position, where the camera looking at. | |
| * @param out The receiving Vector3 instance to copy the result | |
| * @param receiveEndValue Whether receive the transition end coords or current. default is `true` | |
| * @category Methods | |
| */ | |
| getTarget(out, receiveEndValue = true) { | |
| const _out = !!out && out.isVector3 ? out : new THREE.Vector3(); | |
| return _out.copy(receiveEndValue ? this._targetEnd : this._target); | |
| } | |
| /** | |
| * Returns the camera position. | |
| * @param out The receiving Vector3 instance to copy the result | |
| * @param receiveEndValue Whether receive the transition end coords or current. default is `true` | |
| * @category Methods | |
| */ | |
| getPosition(out, receiveEndValue = true) { | |
| const _out = !!out && out.isVector3 ? out : new THREE.Vector3(); | |
| return _out.setFromSpherical(receiveEndValue ? this._sphericalEnd : this._spherical).applyQuaternion(this._yAxisUpSpaceInverse).add(receiveEndValue ? this._targetEnd : this._target); | |
| } | |
| /** | |
| * Returns the spherical coordinates of the orbit. | |
| * @param out The receiving Spherical instance to copy the result | |
| * @param receiveEndValue Whether receive the transition end coords or current. default is `true` | |
| * @category Methods | |
| */ | |
| getSpherical(out, receiveEndValue = true) { | |
| const _out = out || new THREE.Spherical(); | |
| return _out.copy(receiveEndValue ? this._sphericalEnd : this._spherical); | |
| } | |
| /** | |
| * Returns the focal offset, which is how much the camera appears to be translated in screen parallel coordinates. | |
| * @param out The receiving Vector3 instance to copy the result | |
| * @param receiveEndValue Whether receive the transition end coords or current. default is `true` | |
| * @category Methods | |
| */ | |
| getFocalOffset(out, receiveEndValue = true) { | |
| const _out = !!out && out.isVector3 ? out : new THREE.Vector3(); | |
| return _out.copy(receiveEndValue ? this._focalOffsetEnd : this._focalOffset); | |
| } | |
| /** | |
| * Normalize camera azimuth angle rotation between 0 and 360 degrees. | |
| * @category Methods | |
| */ | |
| normalizeRotations() { | |
| this._sphericalEnd.theta = this._sphericalEnd.theta % PI_2; | |
| if (this._sphericalEnd.theta < 0) | |
| this._sphericalEnd.theta += PI_2; | |
| this._spherical.theta += PI_2 * Math.round((this._sphericalEnd.theta - this._spherical.theta) / PI_2); | |
| } | |
| /** | |
| * stop all transitions. | |
| */ | |
| stop() { | |
| this._focalOffset.copy(this._focalOffsetEnd); | |
| this._target.copy(this._targetEnd); | |
| this._spherical.copy(this._sphericalEnd); | |
| this._zoom = this._zoomEnd; | |
| } | |
| /** | |
| * Reset all rotation and position to defaults. | |
| * @param enableTransition | |
| * @category Methods | |
| */ | |
| reset(enableTransition = false) { | |
| if (!approxEquals(this._camera.up.x, this._cameraUp0.x) || | |
| !approxEquals(this._camera.up.y, this._cameraUp0.y) || | |
| !approxEquals(this._camera.up.z, this._cameraUp0.z)) { | |
| this._camera.up.copy(this._cameraUp0); | |
| const position = this.getPosition(_v3A); | |
| this.updateCameraUp(); | |
| this.setPosition(position.x, position.y, position.z); | |
| } | |
| const promises = [ | |
| this.setLookAt(this._position0.x, this._position0.y, this._position0.z, this._target0.x, this._target0.y, this._target0.z, enableTransition), | |
| this.setFocalOffset(this._focalOffset0.x, this._focalOffset0.y, this._focalOffset0.z, enableTransition), | |
| this.zoomTo(this._zoom0, enableTransition), | |
| ]; | |
| return Promise.all(promises); | |
| } | |
| /** | |
| * Set current camera position as the default position. | |
| * @category Methods | |
| */ | |
| saveState() { | |
| this._cameraUp0.copy(this._camera.up); | |
| this.getTarget(this._target0); | |
| this.getPosition(this._position0); | |
| this._zoom0 = this._zoom; | |
| this._focalOffset0.copy(this._focalOffset); | |
| } | |
| /** | |
| * Sync camera-up direction. | |
| * When camera-up vector is changed, `.updateCameraUp()` must be called. | |
| * @category Methods | |
| */ | |
| updateCameraUp() { | |
| this._yAxisUpSpace.setFromUnitVectors(this._camera.up, _AXIS_Y); | |
| this._yAxisUpSpaceInverse.copy(this._yAxisUpSpace).invert(); | |
| } | |
| /** | |
| * Apply current camera-up direction to the camera. | |
| * The orbit system will be re-initialized with the current position. | |
| * @category Methods | |
| */ | |
| applyCameraUp() { | |
| const cameraDirection = _v3A.subVectors(this._target, this._camera.position).normalize(); | |
| // So first find the vector off to the side, orthogonal to both this.object.up and | |
| // the "view" vector. | |
| const side = _v3B.crossVectors(cameraDirection, this._camera.up); | |
| // Then find the vector orthogonal to both this "side" vector and the "view" vector. | |
| // This vector will be the new "up" vector. | |
| this._camera.up.crossVectors(side, cameraDirection).normalize(); | |
| this._camera.updateMatrixWorld(); | |
| const position = this.getPosition(_v3A); | |
| this.updateCameraUp(); | |
| this.setPosition(position.x, position.y, position.z); | |
| } | |
| /** | |
| * Update camera position and directions. | |
| * This should be called in your tick loop every time, and returns true if re-rendering is needed. | |
| * @param delta | |
| * @returns updated | |
| * @category Methods | |
| */ | |
| update(delta) { | |
| const deltaTheta = this._sphericalEnd.theta - this._spherical.theta; | |
| const deltaPhi = this._sphericalEnd.phi - this._spherical.phi; | |
| const deltaRadius = this._sphericalEnd.radius - this._spherical.radius; | |
| const deltaTarget = _deltaTarget.subVectors(this._targetEnd, this._target); | |
| const deltaOffset = _deltaOffset.subVectors(this._focalOffsetEnd, this._focalOffset); | |
| const deltaZoom = this._zoomEnd - this._zoom; | |
| // update theta | |
| if (approxZero(deltaTheta)) { | |
| this._thetaVelocity.value = 0; | |
| this._spherical.theta = this._sphericalEnd.theta; | |
| } | |
| else { | |
| const smoothTime = this._isUserControllingRotate ? this.draggingSmoothTime : this.smoothTime; | |
| this._spherical.theta = smoothDamp(this._spherical.theta, this._sphericalEnd.theta, this._thetaVelocity, smoothTime, Infinity, delta); | |
| this._needsUpdate = true; | |
| } | |
| // update phi | |
| if (approxZero(deltaPhi)) { | |
| this._phiVelocity.value = 0; | |
| this._spherical.phi = this._sphericalEnd.phi; | |
| } | |
| else { | |
| const smoothTime = this._isUserControllingRotate ? this.draggingSmoothTime : this.smoothTime; | |
| this._spherical.phi = smoothDamp(this._spherical.phi, this._sphericalEnd.phi, this._phiVelocity, smoothTime, Infinity, delta); | |
| this._needsUpdate = true; | |
| } | |
| // update distance | |
| if (approxZero(deltaRadius)) { | |
| this._radiusVelocity.value = 0; | |
| this._spherical.radius = this._sphericalEnd.radius; | |
| } | |
| else { | |
| const smoothTime = this._isUserControllingDolly ? this.draggingSmoothTime : this.smoothTime; | |
| this._spherical.radius = smoothDamp(this._spherical.radius, this._sphericalEnd.radius, this._radiusVelocity, smoothTime, this.maxSpeed, delta); | |
| this._needsUpdate = true; | |
| } | |
| // update target position | |
| if (approxZero(deltaTarget.x) && approxZero(deltaTarget.y) && approxZero(deltaTarget.z)) { | |
| this._targetVelocity.set(0, 0, 0); | |
| this._target.copy(this._targetEnd); | |
| } | |
| else { | |
| const smoothTime = this._isUserControllingTruck ? this.draggingSmoothTime : this.smoothTime; | |
| smoothDampVec3(this._target, this._targetEnd, this._targetVelocity, smoothTime, this.maxSpeed, delta, this._target); | |
| this._needsUpdate = true; | |
| } | |
| // update focalOffset | |
| if (approxZero(deltaOffset.x) && approxZero(deltaOffset.y) && approxZero(deltaOffset.z)) { | |
| this._focalOffsetVelocity.set(0, 0, 0); | |
| this._focalOffset.copy(this._focalOffsetEnd); | |
| } | |
| else { | |
| const smoothTime = this._isUserControllingOffset ? this.draggingSmoothTime : this.smoothTime; | |
| smoothDampVec3(this._focalOffset, this._focalOffsetEnd, this._focalOffsetVelocity, smoothTime, this.maxSpeed, delta, this._focalOffset); | |
| this._needsUpdate = true; | |
| } | |
| // update zoom | |
| if (approxZero(deltaZoom)) { | |
| this._zoomVelocity.value = 0; | |
| this._zoom = this._zoomEnd; | |
| } | |
| else { | |
| const smoothTime = this._isUserControllingZoom ? this.draggingSmoothTime : this.smoothTime; | |
| this._zoom = smoothDamp(this._zoom, this._zoomEnd, this._zoomVelocity, smoothTime, Infinity, delta); | |
| } | |
| if (this.dollyToCursor) { | |
| if (isPerspectiveCamera(this._camera) && this._changedDolly !== 0) { | |
| const dollyControlAmount = this._spherical.radius - this._lastDistance; | |
| const camera = this._camera; | |
| const cameraDirection = this._getCameraDirection(_cameraDirection); | |
| const planeX = _v3A.copy(cameraDirection).cross(camera.up).normalize(); | |
| if (planeX.lengthSq() === 0) | |
| planeX.x = 1.0; | |
| const planeY = _v3B.crossVectors(planeX, cameraDirection); | |
| const worldToScreen = this._sphericalEnd.radius * Math.tan(camera.getEffectiveFOV() * DEG2RAD * 0.5); | |
| const prevRadius = this._sphericalEnd.radius - dollyControlAmount; | |
| const lerpRatio = (prevRadius - this._sphericalEnd.radius) / this._sphericalEnd.radius; | |
| const cursor = _v3C.copy(this._targetEnd) | |
| .add(planeX.multiplyScalar(this._dollyControlCoord.x * worldToScreen * camera.aspect)) | |
| .add(planeY.multiplyScalar(this._dollyControlCoord.y * worldToScreen)); | |
| const newTargetEnd = _v3A.copy(this._targetEnd).lerp(cursor, lerpRatio); | |
| const isMin = this._lastDollyDirection === DOLLY_DIRECTION.IN && this._spherical.radius <= this.minDistance; | |
| const isMax = this._lastDollyDirection === DOLLY_DIRECTION.OUT && this.maxDistance <= this._spherical.radius; | |
| if (this.infinityDolly && (isMin || isMax)) { | |
| this._sphericalEnd.radius -= dollyControlAmount; | |
| this._spherical.radius -= dollyControlAmount; | |
| const dollyAmount = _v3B.copy(cameraDirection).multiplyScalar(-dollyControlAmount); | |
| newTargetEnd.add(dollyAmount); | |
| } | |
| // target position may be moved beyond boundary. | |
| this._boundary.clampPoint(newTargetEnd, newTargetEnd); | |
| const targetEndDiff = _v3B.subVectors(newTargetEnd, this._targetEnd); | |
| this._targetEnd.copy(newTargetEnd); | |
| this._target.add(targetEndDiff); | |
| this._changedDolly -= dollyControlAmount; | |
| if (approxZero(this._changedDolly)) | |
| this._changedDolly = 0; | |
| } | |
| else if (isOrthographicCamera(this._camera) && this._changedZoom !== 0) { | |
| const dollyControlAmount = this._zoom - this._lastZoom; | |
| const camera = this._camera; | |
| const worldCursorPosition = _v3A.set(this._dollyControlCoord.x, this._dollyControlCoord.y, (camera.near + camera.far) / (camera.near - camera.far)).unproject(camera); | |
| const quaternion = _v3B.set(0, 0, -1).applyQuaternion(camera.quaternion); | |
| const cursor = _v3C.copy(worldCursorPosition).add(quaternion.multiplyScalar(-worldCursorPosition.dot(camera.up))); | |
| const prevZoom = this._zoom - dollyControlAmount; | |
| const lerpRatio = -(prevZoom - this._zoom) / this._zoom; | |
| // find the "distance" (aka plane constant in three.js) of Plane | |
| // from a given position (this._targetEnd) and normal vector (cameraDirection) | |
| // https://www.maplesoft.com/support/help/maple/view.aspx?path=MathApps%2FEquationOfAPlaneNormal#bkmrk0 | |
| const cameraDirection = this._getCameraDirection(_cameraDirection); | |
| const prevPlaneConstant = this._targetEnd.dot(cameraDirection); | |
| const newTargetEnd = _v3A.copy(this._targetEnd).lerp(cursor, lerpRatio); | |
| const newPlaneConstant = newTargetEnd.dot(cameraDirection); | |
| // Pull back the camera depth that has moved, to be the camera stationary as zoom | |
| const pullBack = cameraDirection.multiplyScalar(newPlaneConstant - prevPlaneConstant); | |
| newTargetEnd.sub(pullBack); | |
| // target position may be moved beyond boundary. | |
| this._boundary.clampPoint(newTargetEnd, newTargetEnd); | |
| const targetEndDiff = _v3B.subVectors(newTargetEnd, this._targetEnd); | |
| this._targetEnd.copy(newTargetEnd); | |
| this._target.add(targetEndDiff); | |
| // this._target.copy( this._targetEnd ); | |
| this._changedZoom -= dollyControlAmount; | |
| if (approxZero(this._changedZoom)) | |
| this._changedZoom = 0; | |
| } | |
| } | |
| if (this._camera.zoom !== this._zoom) { | |
| this._camera.zoom = this._zoom; | |
| this._camera.updateProjectionMatrix(); | |
| this._updateNearPlaneCorners(); | |
| this._needsUpdate = true; | |
| } | |
| this._dragNeedsUpdate = true; | |
| // collision detection | |
| const maxDistance = this._collisionTest(); | |
| this._spherical.radius = Math.min(this._spherical.radius, maxDistance); | |
| // decompose spherical to the camera position | |
| this._spherical.makeSafe(); | |
| this._camera.position.setFromSpherical(this._spherical).applyQuaternion(this._yAxisUpSpaceInverse).add(this._target); | |
| this._camera.lookAt(this._target); | |
| // set offset after the orbit movement | |
| const affectOffset = !approxZero(this._focalOffset.x) || | |
| !approxZero(this._focalOffset.y) || | |
| !approxZero(this._focalOffset.z); | |
| if (affectOffset) { | |
| _xColumn.setFromMatrixColumn(this._camera.matrix, 0); | |
| _yColumn.setFromMatrixColumn(this._camera.matrix, 1); | |
| _zColumn.setFromMatrixColumn(this._camera.matrix, 2); | |
| _xColumn.multiplyScalar(this._focalOffset.x); | |
| _yColumn.multiplyScalar(-this._focalOffset.y); | |
| _zColumn.multiplyScalar(this._focalOffset.z); // notice: z-offset will not affect in Orthographic. | |
| _v3A.copy(_xColumn).add(_yColumn).add(_zColumn); | |
| this._camera.position.add(_v3A); | |
| this._camera.updateMatrixWorld(); | |
| } | |
| if (this._boundaryEnclosesCamera) { | |
| this._encloseToBoundary(this._camera.position.copy(this._target), _v3A.setFromSpherical(this._spherical).applyQuaternion(this._yAxisUpSpaceInverse), 1.0); | |
| } | |
| const updated = this._needsUpdate; | |
| if (updated && !this._updatedLastTime) { | |
| this._hasRested = false; | |
| this.dispatchEvent({ type: 'wake' }); | |
| this.dispatchEvent({ type: 'update' }); | |
| } | |
| else if (updated) { | |
| this.dispatchEvent({ type: 'update' }); | |
| if (approxZero(deltaTheta, this.restThreshold) && | |
| approxZero(deltaPhi, this.restThreshold) && | |
| approxZero(deltaRadius, this.restThreshold) && | |
| approxZero(deltaTarget.x, this.restThreshold) && | |
| approxZero(deltaTarget.y, this.restThreshold) && | |
| approxZero(deltaTarget.z, this.restThreshold) && | |
| approxZero(deltaOffset.x, this.restThreshold) && | |
| approxZero(deltaOffset.y, this.restThreshold) && | |
| approxZero(deltaOffset.z, this.restThreshold) && | |
| approxZero(deltaZoom, this.restThreshold) && | |
| !this._hasRested) { | |
| this._hasRested = true; | |
| this.dispatchEvent({ type: 'rest' }); | |
| } | |
| } | |
| else if (!updated && this._updatedLastTime) { | |
| this.dispatchEvent({ type: 'sleep' }); | |
| } | |
| this._lastDistance = this._spherical.radius; | |
| this._lastZoom = this._zoom; | |
| this._updatedLastTime = updated; | |
| this._needsUpdate = false; | |
| return updated; | |
| } | |
| /** | |
| * Get all state in JSON string | |
| * @category Methods | |
| */ | |
| toJSON() { | |
| return JSON.stringify({ | |
| enabled: this._enabled, | |
| minDistance: this.minDistance, | |
| maxDistance: infinityToMaxNumber(this.maxDistance), | |
| minZoom: this.minZoom, | |
| maxZoom: infinityToMaxNumber(this.maxZoom), | |
| minPolarAngle: this.minPolarAngle, | |
| maxPolarAngle: infinityToMaxNumber(this.maxPolarAngle), | |
| minAzimuthAngle: infinityToMaxNumber(this.minAzimuthAngle), | |
| maxAzimuthAngle: infinityToMaxNumber(this.maxAzimuthAngle), | |
| smoothTime: this.smoothTime, | |
| draggingSmoothTime: this.draggingSmoothTime, | |
| dollySpeed: this.dollySpeed, | |
| truckSpeed: this.truckSpeed, | |
| dollyToCursor: this.dollyToCursor, | |
| target: this._targetEnd.toArray(), | |
| position: _v3A.setFromSpherical(this._sphericalEnd).add(this._targetEnd).toArray(), | |
| zoom: this._zoomEnd, | |
| focalOffset: this._focalOffsetEnd.toArray(), | |
| target0: this._target0.toArray(), | |
| position0: this._position0.toArray(), | |
| zoom0: this._zoom0, | |
| focalOffset0: this._focalOffset0.toArray(), | |
| }); | |
| } | |
| /** | |
| * Reproduce the control state with JSON. enableTransition is where anim or not in a boolean. | |
| * @param json | |
| * @param enableTransition | |
| * @category Methods | |
| */ | |
| fromJSON(json, enableTransition = false) { | |
| const obj = JSON.parse(json); | |
| this.enabled = obj.enabled; | |
| this.minDistance = obj.minDistance; | |
| this.maxDistance = maxNumberToInfinity(obj.maxDistance); | |
| this.minZoom = obj.minZoom; | |
| this.maxZoom = maxNumberToInfinity(obj.maxZoom); | |
| this.minPolarAngle = obj.minPolarAngle; | |
| this.maxPolarAngle = maxNumberToInfinity(obj.maxPolarAngle); | |
| this.minAzimuthAngle = maxNumberToInfinity(obj.minAzimuthAngle); | |
| this.maxAzimuthAngle = maxNumberToInfinity(obj.maxAzimuthAngle); | |
| this.smoothTime = obj.smoothTime; | |
| this.draggingSmoothTime = obj.draggingSmoothTime; | |
| this.dollySpeed = obj.dollySpeed; | |
| this.truckSpeed = obj.truckSpeed; | |
| this.dollyToCursor = obj.dollyToCursor; | |
| this._target0.fromArray(obj.target0); | |
| this._position0.fromArray(obj.position0); | |
| this._zoom0 = obj.zoom0; | |
| this._focalOffset0.fromArray(obj.focalOffset0); | |
| this.moveTo(obj.target[0], obj.target[1], obj.target[2], enableTransition); | |
| _sphericalA.setFromVector3(_v3A.fromArray(obj.position).sub(this._targetEnd).applyQuaternion(this._yAxisUpSpace)); | |
| this.rotateTo(_sphericalA.theta, _sphericalA.phi, enableTransition); | |
| this.dollyTo(_sphericalA.radius, enableTransition); | |
| this.zoomTo(obj.zoom, enableTransition); | |
| this.setFocalOffset(obj.focalOffset[0], obj.focalOffset[1], obj.focalOffset[2], enableTransition); | |
| this._needsUpdate = true; | |
| } | |
| /** | |
| * Attach all internal event handlers to enable drag control. | |
| * @category Methods | |
| */ | |
| connect(domElement) { | |
| if (this._domElement) { | |
| console.warn('camera-controls is already connected.'); | |
| return; | |
| } | |
| domElement.setAttribute('data-camera-controls-version', VERSION); | |
| this._addAllEventListeners(domElement); | |
| this._getClientRect(this._elementRect); | |
| } | |
| /** | |
| * Detach all internal event handlers to disable drag control. | |
| */ | |
| disconnect() { | |
| this.cancel(); | |
| this._removeAllEventListeners(); | |
| if (this._domElement) { | |
| this._domElement.removeAttribute('data-camera-controls-version'); | |
| this._domElement = undefined; | |
| } | |
| } | |
| /** | |
| * Dispose the cameraControls instance itself, remove all eventListeners. | |
| * @category Methods | |
| */ | |
| dispose() { | |
| // remove all user event listeners | |
| this.removeAllEventListeners(); | |
| // remove all internal event listeners | |
| this.disconnect(); | |
| } | |
| // it's okay to expose public though | |
| _getTargetDirection(out) { | |
| // divide by distance to normalize, lighter than `Vector3.prototype.normalize()` | |
| return out.setFromSpherical(this._spherical).divideScalar(this._spherical.radius).applyQuaternion(this._yAxisUpSpaceInverse); | |
| } | |
| // it's okay to expose public though | |
| _getCameraDirection(out) { | |
| return this._getTargetDirection(out).negate(); | |
| } | |
| _findPointerById(pointerId) { | |
| return this._activePointers.find((activePointer) => activePointer.pointerId === pointerId); | |
| } | |
| _findPointerByMouseButton(mouseButton) { | |
| return this._activePointers.find((activePointer) => activePointer.mouseButton === mouseButton); | |
| } | |
| _disposePointer(pointer) { | |
| this._activePointers.splice(this._activePointers.indexOf(pointer), 1); | |
| } | |
| _encloseToBoundary(position, offset, friction) { | |
| const offsetLength2 = offset.lengthSq(); | |
| if (offsetLength2 === 0.0) { // sanity check | |
| return position; | |
| } | |
| // See: https://twitter.com/FMS_Cat/status/1106508958640988161 | |
| const newTarget = _v3B.copy(offset).add(position); // target | |
| const clampedTarget = this._boundary.clampPoint(newTarget, _v3C); // clamped target | |
| const deltaClampedTarget = clampedTarget.sub(newTarget); // newTarget -> clampedTarget | |
| const deltaClampedTargetLength2 = deltaClampedTarget.lengthSq(); // squared length of deltaClampedTarget | |
| if (deltaClampedTargetLength2 === 0.0) { // when the position doesn't have to be clamped | |
| return position.add(offset); | |
| } | |
| else if (deltaClampedTargetLength2 === offsetLength2) { // when the position is completely stuck | |
| return position; | |
| } | |
| else if (friction === 0.0) { | |
| return position.add(offset).add(deltaClampedTarget); | |
| } | |
| else { | |
| const offsetFactor = 1.0 + friction * deltaClampedTargetLength2 / offset.dot(deltaClampedTarget); | |
| return position | |
| .add(_v3B.copy(offset).multiplyScalar(offsetFactor)) | |
| .add(deltaClampedTarget.multiplyScalar(1.0 - friction)); | |
| } | |
| } | |
| _updateNearPlaneCorners() { | |
| if (isPerspectiveCamera(this._camera)) { | |
| const camera = this._camera; | |
| const near = camera.near; | |
| const fov = camera.getEffectiveFOV() * DEG2RAD; | |
| const heightHalf = Math.tan(fov * 0.5) * near; // near plain half height | |
| const widthHalf = heightHalf * camera.aspect; // near plain half width | |
| this._nearPlaneCorners[0].set(-widthHalf, -heightHalf, 0); | |
| this._nearPlaneCorners[1].set(widthHalf, -heightHalf, 0); | |
| this._nearPlaneCorners[2].set(widthHalf, heightHalf, 0); | |
| this._nearPlaneCorners[3].set(-widthHalf, heightHalf, 0); | |
| } | |
| else if (isOrthographicCamera(this._camera)) { | |
| const camera = this._camera; | |
| const zoomInv = 1 / camera.zoom; | |
| const left = camera.left * zoomInv; | |
| const right = camera.right * zoomInv; | |
| const top = camera.top * zoomInv; | |
| const bottom = camera.bottom * zoomInv; | |
| this._nearPlaneCorners[0].set(left, top, 0); | |
| this._nearPlaneCorners[1].set(right, top, 0); | |
| this._nearPlaneCorners[2].set(right, bottom, 0); | |
| this._nearPlaneCorners[3].set(left, bottom, 0); | |
| } | |
| } | |
| // lateUpdate | |
| _collisionTest() { | |
| let distance = Infinity; | |
| const hasCollider = this.colliderMeshes.length >= 1; | |
| if (!hasCollider) | |
| return distance; | |
| if (notSupportedInOrthographicCamera(this._camera, '_collisionTest')) | |
| return distance; | |
| const rayDirection = this._getTargetDirection(_cameraDirection); | |
| _rotationMatrix.lookAt(_ORIGIN, rayDirection, this._camera.up); | |
| for (let i = 0; i < 4; i++) { | |
| const nearPlaneCorner = _v3B.copy(this._nearPlaneCorners[i]); | |
| nearPlaneCorner.applyMatrix4(_rotationMatrix); | |
| const origin = _v3C.addVectors(this._target, nearPlaneCorner); | |
| _raycaster.set(origin, rayDirection); | |
| _raycaster.far = this._spherical.radius + 1; | |
| const intersects = _raycaster.intersectObjects(this.colliderMeshes); | |
| if (intersects.length !== 0 && intersects[0].distance < distance) { | |
| distance = intersects[0].distance; | |
| } | |
| } | |
| return distance; | |
| } | |
| /** | |
| * Get its client rect and package into given `DOMRect` . | |
| */ | |
| _getClientRect(target) { | |
| if (!this._domElement) | |
| return; | |
| const rect = this._domElement.getBoundingClientRect(); | |
| target.x = rect.left; | |
| target.y = rect.top; | |
| if (this._viewport) { | |
| target.x += this._viewport.x; | |
| target.y += rect.height - this._viewport.w - this._viewport.y; | |
| target.width = this._viewport.z; | |
| target.height = this._viewport.w; | |
| } | |
| else { | |
| target.width = rect.width; | |
| target.height = rect.height; | |
| } | |
| return target; | |
| } | |
| _createOnRestPromise(resolveImmediately) { | |
| if (resolveImmediately) | |
| return Promise.resolve(); | |
| this._hasRested = false; | |
| this.dispatchEvent({ type: 'transitionstart' }); | |
| return new Promise((resolve) => { | |
| const onResolve = () => { | |
| this.removeEventListener('rest', onResolve); | |
| resolve(); | |
| }; | |
| this.addEventListener('rest', onResolve); | |
| }); | |
| } | |
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |
| _addAllEventListeners(_domElement) { } | |
| _removeAllEventListeners() { } | |
| /** | |
| * backward compatible | |
| * @deprecated use smoothTime (in seconds) instead | |
| * @category Properties | |
| */ | |
| get dampingFactor() { | |
| console.warn('.dampingFactor has been deprecated. use smoothTime (in seconds) instead.'); | |
| return 0; | |
| } | |
| /** | |
| * backward compatible | |
| * @deprecated use smoothTime (in seconds) instead | |
| * @category Properties | |
| */ | |
| set dampingFactor(_) { | |
| console.warn('.dampingFactor has been deprecated. use smoothTime (in seconds) instead.'); | |
| } | |
| /** | |
| * backward compatible | |
| * @deprecated use draggingSmoothTime (in seconds) instead | |
| * @category Properties | |
| */ | |
| get draggingDampingFactor() { | |
| console.warn('.draggingDampingFactor has been deprecated. use draggingSmoothTime (in seconds) instead.'); | |
| return 0; | |
| } | |
| /** | |
| * backward compatible | |
| * @deprecated use draggingSmoothTime (in seconds) instead | |
| * @category Properties | |
| */ | |
| set draggingDampingFactor(_) { | |
| console.warn('.draggingDampingFactor has been deprecated. use draggingSmoothTime (in seconds) instead.'); | |
| } | |
| static createBoundingSphere(object3d, out = new THREE.Sphere()) { | |
| const boundingSphere = out; | |
| const center = boundingSphere.center; | |
| _box3A.makeEmpty(); | |
| // find the center | |
| object3d.traverseVisible((object) => { | |
| if (!object.isMesh) | |
| return; | |
| _box3A.expandByObject(object); | |
| }); | |
| _box3A.getCenter(center); | |
| // find the radius | |
| let maxRadiusSq = 0; | |
| object3d.traverseVisible((object) => { | |
| if (!object.isMesh) | |
| return; | |
| const mesh = object; | |
| if (!mesh.geometry) | |
| return; | |
| const geometry = mesh.geometry.clone(); | |
| geometry.applyMatrix4(mesh.matrixWorld); | |
| const bufferGeometry = geometry; | |
| const position = bufferGeometry.attributes.position; | |
| for (let i = 0, l = position.count; i < l; i++) { | |
| _v3A.fromBufferAttribute(position, i); | |
| maxRadiusSq = Math.max(maxRadiusSq, center.distanceToSquared(_v3A)); | |
| } | |
| }); | |
| boundingSphere.radius = Math.sqrt(maxRadiusSq); | |
| return boundingSphere; | |
| } | |
| } | |
| export { EventDispatcher, CameraControls as default }; | |