2558 lines
112 KiB
JavaScript
2558 lines
112 KiB
JavaScript
/*!
|
|
* 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 };
|