UNPKG

awv3

Version:
691 lines (516 loc) 22.2 kB
import _taggedTemplateLiteralLoose from "@babel/runtime/helpers/taggedTemplateLiteralLoose"; import _createClass from "@babel/runtime/helpers/createClass"; var _class, _temp; var _templateObject = /*#__PURE__*/ _taggedTemplateLiteralLoose(["View undefined"], ["View undefined"]); import * as THREE from 'three'; import { errUndefined } from '../core/helpers'; import { lastCreated } from '../core/canvas'; import { exponential, elastic } from '../animation/easing'; function QuaternionFromLocation(location) { var zAxis = location.eyeVec.clone().normalize(); var yAxis = location.upDir; var xAxis = new THREE.Vector3().crossVectors(yAxis, zAxis); var matrix = new THREE.Matrix4(); matrix.makeBasis(xAxis, yAxis, zAxis); var quaternion = new THREE.Quaternion(); matrix.decompose(new THREE.Vector3(), quaternion, new THREE.Vector3()); return quaternion; } function QuaternionToAxes(quaternion) { var matrix = new THREE.Matrix4(); matrix.compose(new THREE.Vector3(), quaternion, new THREE.Vector3(1, 1, 1)); var xAxis = new THREE.Vector3(); var yAxis = new THREE.Vector3(); var zAxis = new THREE.Vector3(); matrix.extractBasis(xAxis, yAxis, zAxis); return [xAxis, yAxis, zAxis]; } function InterpolateCameraLocations(startLocation, endLocation, parameter) { //interpolate target linearly var interpTarget = startLocation.target.clone().lerp(endLocation.target, parameter); //interpolate eye-target distance exponentially var startDist = startLocation.eyeVec.length(); var endDist = endLocation.eyeVec.length(); var interpDist = startDist * Math.pow(endDist / startDist, parameter); //perform SLERP between orientations (as quaternions) var startQuaternion = QuaternionFromLocation(startLocation); var endQuaternion = QuaternionFromLocation(endLocation); var interpQuaternion = startQuaternion.clone().slerp(endQuaternion, parameter); interpQuaternion.normalize(); var interpAxes = QuaternionToAxes(interpQuaternion); var interpLocation = { target: interpTarget, eyeVec: interpAxes[2].multiplyScalar(interpDist), upDir: interpAxes[1] }; return interpLocation; } var Trackball = (_temp = _class = /*#__PURE__*/ function () { var _proto = Trackball.prototype; _proto.realCamera = function realCamera() { return this.camera || this.view.camera; }; //returns true if controls are actively changing the camera now //note: if false is returned, then view may avoid rendering new frames _createClass(Trackball, [{ key: "inMotion", get: function get() { //TODO: detect when idle return true; } }]); function Trackball(view, options) { if (view === void 0) { view = errUndefined(_templateObject); } if (options === void 0) { options = {}; } Object.defineProperty(this, "_quatInverse", { configurable: true, enumerable: true, writable: true, value: new THREE.Quaternion() }); this.view = view; this.canvas = view.canvas; this.dom = view.dom; this.camera = options.camera; var camera = this.realCamera(); //=================== //====== API ======== //=================== //if false, then all user actions do nothing this.enabled = true; //TODO: // this.screen = { left: 0, top: 0, width: 0, height: 0 } //speed of camera response to user input this.rotateSpeed = 5.0; this.zoomSpeed = 2.0; this.panSpeed = 0.8; //allows disabling some parts of user controls this.noRotate = false; this.noZoom = false; this.noPan = false; //false = changes are animated; true = changes are applied immediately //note: this flag applies only to user controls smoothing, API-caused changes are always animated this.staticMoving = true; //TODO: what is this exactly? this.dynamicDampingFactor = 0.2; //distance from camera to target is limited into [min, max] this.minDistance = 0; this.maxDistance = Infinity; //=================== //==== internals ==== //=================== this.target = new THREE.Vector3(); this._lastPosition = new THREE.Vector3(); this._state = Trackball.STATE.NONE; this._prevState = Trackball.STATE.NONE; this._eye = new THREE.Vector3(); this.changeThreshold = 0.000001; //mouse cursor position in "circle" coordinates, current and previous: //1. for ROTATE and TOUCH_ROTATE states: this._movePrev = new THREE.Vector2(); this._moveCurr = new THREE.Vector2(); //2. for ZOOM state: this._zoomStart = new THREE.Vector2(); this._zoomEnd = new THREE.Vector2(); //3. for PAN state: this._panStart = new THREE.Vector2(); this._panEnd = new THREE.Vector2(); //distance between two fingers on touch screen, current and previous: //4. for TOUCH_ZOOM_PAN state: this._touchZoomDistanceStart = 0; this._touchZoomDistanceEnd = 0; //rotation axis of the last rotation (in world coordinates, in ROTATE state): this._axis = new THREE.Vector3(); this._lastAxis = new THREE.Vector3(); //angle step of the last rotation (in radians, in ROTATE state): this._angle = undefined; this._lastAngle = 0; //TODO: the remaining stuff is most likely some unimportant temporaries... this._vector = new THREE.Vector2(); this._quaternion = new THREE.Quaternion(); this._eyeDirection = new THREE.Vector3(); this._objectUpDirection = new THREE.Vector3(); this._objectSidewaysDirection = new THREE.Vector3(); this._moveDirection = new THREE.Vector3(); this._mouseChange = new THREE.Vector2(); this._objectUp = new THREE.Vector3(); this._pan = new THREE.Vector3(); //duration for any automated camera animation this.duration = 500; //animation for automated camera movement in progress this.auto = undefined; //ensure that initial camera location is valid var location = this.getCurrentLocation(); var err = this._isLocationValid(location); if (err) { console.log("Camera error: " + err); this.fixLocation(location); this._setLocation(location); } //initial location of camera is saved, use this.reset to get back to it this.initialLocation = location; /*this._changeEvent = { type: 'change' } this._startEvent = { type: 'start' } this._endEvent = { type: 'end' }*/ } _proto._getMouseOnScreen = function _getMouseOnScreen(pageX, pageY) { this._vector.set((pageX - this.view.left) / this.view.width, (pageY - this.view.top) / this.view.height); return this._vector; }; _proto._getMouseOnCircle = function _getMouseOnCircle(pageX, pageY) { this._vector.set((pageX - this.view.width * 0.5 - this.view.left) / (this.view.width * 0.5), (this.view.height + 2 * (this.view.top - pageY)) / this.view.width // screen.width intentional ); return this._vector; }; _proto._rotateCamera = function _rotateCamera() { var camera = this.realCamera(); this._moveDirection.set(this._moveCurr.x - this._movePrev.x, this._moveCurr.y - this._movePrev.y, 0); this._angle = this._moveDirection.length(); if (this._angle) { this._eye.copy(camera.position).sub(this.target); this._eyeDirection.copy(this._eye).normalize(); this._objectUpDirection.copy(camera.up).normalize(); this._objectSidewaysDirection.crossVectors(this._objectUpDirection, this._eyeDirection).normalize(); this._objectUpDirection.setLength(this._moveCurr.y - this._movePrev.y); this._objectSidewaysDirection.setLength(this._moveCurr.x - this._movePrev.x); this._moveDirection.copy(this._objectUpDirection.add(this._objectSidewaysDirection)); this._axis.crossVectors(this._moveDirection, this._eye).normalize(); this._angle *= this.rotateSpeed; this._quaternion.setFromAxisAngle(this._axis, this._angle); this._eye.applyQuaternion(this._quaternion); camera.up.applyQuaternion(this._quaternion); this._lastAxis.copy(this._axis); this._lastAngle = this._angle; } else if (!this.staticMoving && this._lastAngle) { this._lastAngle *= Math.sqrt(1.0 - this.dynamicDampingFactor); this._eye.copy(camera.position).sub(this.target); this._quaternion.setFromAxisAngle(this._lastAxis, this._lastAngle); this._eye.applyQuaternion(this._quaternion); camera.up.applyQuaternion(this._quaternion); } this._movePrev.copy(this._moveCurr); }; _proto._zoomCamera = function _zoomCamera() { var factor; if (this._state === Trackball.STATE.TOUCH_ZOOM_PAN) { factor = this._touchZoomDistanceStart / this._touchZoomDistanceEnd; this._touchZoomDistanceStart = this._touchZoomDistanceEnd; this._eye.multiplyScalar(factor); } else { factor = 1.0 + (this._zoomEnd.y - this._zoomStart.y) * this.zoomSpeed; if (factor !== 1.0 && factor > 0.0) { this._eye.multiplyScalar(factor); } if (this.staticMoving) { this._zoomStart.copy(this._zoomEnd); } else { this._zoomStart.y += (this._zoomEnd.y - this._zoomStart.y) * this.dynamicDampingFactor; } } }; _proto._panCamera = function _panCamera() { var camera = this.realCamera(); this._mouseChange.copy(this._panEnd).sub(this._panStart); if (this._mouseChange.lengthSq()) { this._mouseChange.multiplyScalar(this._eye.length() * this.panSpeed); this._pan.copy(this._eye).cross(camera.up).setLength(this._mouseChange.x); this._pan.add(this._objectUp.copy(camera.up).setLength(this._mouseChange.y)); camera.position.add(this._pan); this.target.add(this._pan); if (this.staticMoving) { this._panStart.copy(this._panEnd); } else { this._panStart.add(this._mouseChange.subVectors(this._panEnd, this._panStart).multiplyScalar(this.dynamicDampingFactor)); } } }; _proto._checkDistances = function _checkDistances() { var camera = this.realCamera(); if (!this.noZoom || !this.noPan) { if (this._eye.lengthSq() > this.maxDistance * this.maxDistance) { camera.position.addVectors(this.target, this._eye.setLength(this.maxDistance)); this._zoomStart.copy(this._zoomEnd); } if (this._eye.lengthSq() < this.minDistance * this.minDistance) { camera.position.addVectors(this.target, this._eye.setLength(this.minDistance)); this._zoomStart.copy(this._zoomEnd); } } }; _proto.update = function update() { var camera = this.realCamera(); this._eye.subVectors(camera.position, this.target); if (this.auto) this._updateAutomation();else { if (!this.noRotate) this._rotateCamera(); if (!this.noZoom) this._zoomCamera(); if (!this.noPan) this._panCamera(); } camera.position.addVectors(this.target, this._eye); this._checkDistances(); camera.lookAt(this.target); if (this._lastPosition.distanceToSquared(camera.position) > this.changeThreshold) { //this.dispatchEvent(this._changeEvent) this._lastPosition.copy(camera.position); } }; //fixes geometric issues in the given location (in case if it invalid) _proto.fixLocation = function fixLocation(location) { var eyeDist = location.eyeVec.length(); var eyeDir = location.eyeVec.clone().normalize(); if (eyeDist < 1e-6 || eyeDist > 1e+6) location.eyeVec.copy(eyeDir); location.upDir.sub(location.upDir.clone().projectOnVector(eyeDir)); var upLen = location.upDir.length(); if (upLen < 1e-6) { var _arr = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; for (var _i = 0; _i < _arr.length; _i++) { var coords = _arr[_i]; var vec = new THREE.Vector3().fromArray(coords); var projVec = vec.clone().sub(vec.clone().projectOnVector(eyeDir)); var projLen = projVec.length(); if (upLen < projLen) { upLen = projLen; location.upDir.copy(projVec); } } } location.upDir.normalize(); }; _proto._isLocationValid = function _isLocationValid(location) { if (!location) return "location is undefined"; if (!(location.target instanceof THREE.Vector3)) return "target has bad type: " + location.target; if (!(location.eyeVec instanceof THREE.Vector3)) return "eye vector has bad type: " + location.eyeVec; if (!(location.upDir instanceof THREE.Vector3)) return "up direction has bad type: " + location.upDir; var eyeDist = location.eyeVec.length(); if (isNaN(eyeDist) || eyeDist < 1e-6 || eyeDist > 1e+6) return "target distance is wrong: " + eyeDist; var upLen = location.upDir.length(); if (isNaN(upLen) || Math.abs(upLen - 1.0) > 1e-6) return "up direction has wrong length: " + upLen; var cosAngle = location.upDir.dot(location.eyeVec.clone().normalize()); if (Math.abs(cosAngle) > 1e-6) return "angle between eye and up vectors is not right: " + Math.acos(cosAngle); return undefined; }; //returns current location (position, zoom, orientation) of the camera _proto.getCurrentLocation = function getCurrentLocation() { var camera = this.realCamera(); return { //focus point; camera rotates around it target: this.target.clone(), //vector from focus point to camera position (inverted view direction) eyeVec: camera.position.clone().sub(this.target), //unit direction orthogonal to eye vector, points "up" in the camera view upDir: camera.up.clone() }; }; //changes camera location to given values (internal usage only) _proto._setLocation = function _setLocation(location) { var camera = this.realCamera(); this.target.copy(location.target); camera.position.copy(location.target.clone().add(location.eyeVec)); camera.up.copy(location.upDir); this._eye.subVectors(camera.position, this.target); camera.lookAt(this.target); }; //start animation to prescribed camera location _proto.rotateToLocation = function rotateToLocation(location) { var err = this._isLocationValid(location); if (err) { console.log("Trackball error: " + err); return this; } this._startAutomation(location); return this; }; //resets camera location to given (or initial) values //stops any movement in progress _proto.reset = function reset(location) { if (location === void 0) { location = this.initialLocation; } var err = this._isLocationValid(location); if (err) { console.log("Trackball error: " + err); return this; } var camera = this.realCamera(); this._state = Trackball.STATE.NONE; this._prevState = Trackball.STATE.NONE; this._setLocation(location); //this.dispatchEvent(this._changeEvent) this._lastPosition.copy(camera.position); this.auto = undefined; return this; }; _proto._startAutomation = function _startAutomation(endLocation) { this.auto = { start: this.getCurrentLocation(), end: endLocation, time: this.canvas.renderer.time, duration: this.duration }; }; _proto._updateAutomation = function _updateAutomation(overrideParameter) { if (overrideParameter === void 0) { overrideParameter = undefined; } if (!this.auto) return this; var parameter = (this.canvas.renderer.time - this.auto.time) / this.auto.duration; if (overrideParameter !== undefined) parameter = overrideParameter; parameter = THREE.Math.clamp(parameter, 0, 1); var interpLocation = InterpolateCameraLocations(this.auto.start, this.auto.end, parameter); this._setLocation(interpLocation); if (parameter === 1.0) this.auto = undefined; }; //==================================== // Orbit compatibility layer // you'd better NOT use it! // it is a complete mess =( //==================================== //start animated rotation to given spherical angles _proto.rotate = function rotate(theta, phi) { var location = this.getCurrentLocation(); var radius = location.eyeVec.length(); var eye = new THREE.Vector3(); eye.x = radius * Math.sin(phi) * Math.sin(theta); eye.y = radius * Math.cos(phi); eye.z = radius * Math.sin(phi) * Math.cos(theta); var up = new THREE.Vector3(); up.x = -Math.cos(phi) * Math.sin(theta); up.y = Math.sin(phi); up.z = -Math.cos(phi) * Math.cos(theta); eye.applyQuaternion(this._quatInverse); up.applyQuaternion(this._quatInverse); return this.rotateToLocation({ target: this.target, eyeVec: eye, upDir: up }); }; _proto.rotatePhi = function rotatePhi() { return this; }; _proto.rotateTheta = function rotateTheta() { return this; }; // (end of Orbit compatibility layer) //==================================== _proto.zoom = function zoom() { return this; }; _proto.focus = function focus() { return this; }; _proto.pan = function pan() { return this; }; _proto.fov = function fov() { return this; }; _proto.wait = function wait() { return this; }; _proto.waitActive = function waitActive() { return this; }; //complete automated animation _proto.now = function now() { this._updateAutomation(1.0); return this; }; //stop automated animation _proto.stop = function stop() { this.auto = undefined; return this; }; //store current camera location as default one (substitutes initial location) _proto.store = function store() { this.initialLocation = this.getCurrentLocation(); return this; }; //restore back to default location (with animation) _proto.back = function back() { this.rotateToLocation(this.initialLocation); return this; }; //======================================= // event handlers (do not call directly) //======================================= _proto.onMouseDown = function onMouseDown(event) { if (this.enabled === false) return; if (this._state === Trackball.STATE.NONE) { this._state = event.button; } if (this._state === Trackball.STATE.ROTATE && !this.noRotate) { this._moveCurr.copy(this._getMouseOnCircle(event.pageX, event.pageY)); this._movePrev.copy(this._moveCurr); } else if (this._state === Trackball.STATE.ZOOM && !this.noZoom) { this._zoomStart.copy(this._getMouseOnScreen(event.pageX, event.pageY)); this._zoomEnd.copy(this._zoomStart); } else if (this._state === Trackball.STATE.PAN && !this.noPan) { this._panStart.copy(this._getMouseOnScreen(event.pageX, event.pageY)); this._panEnd.copy(this._panStart); } }; _proto.onMouseMove = function onMouseMove(event) { if (this.enabled === false) return; if (this._state === Trackball.STATE.ROTATE && !this.noRotate) { this._movePrev.copy(this._moveCurr); this._moveCurr.copy(this._getMouseOnCircle(event.pageX, event.pageY)); } else if (this._state === Trackball.STATE.ZOOM && !this.noZoom) { this._zoomEnd.copy(this._getMouseOnScreen(event.pageX, event.pageY)); } else if (this._state === Trackball.STATE.PAN && !this.noPan) { this._panEnd.copy(this._getMouseOnScreen(event.pageX, event.pageY)); } }; _proto.onMouseUp = function onMouseUp(event) { if (this.enabled === false) return; this._state = Trackball.STATE.NONE; }; _proto.onMouseWheel = function onMouseWheel(event) { if (this.enabled === false) return; this._zoomStart.y += event.delta * 0.01; }; _proto.onTouchStart = function onTouchStart(event) { if (this.enabled === false) return; switch (event.touches.length) { case 1: this._state = Trackball.STATE.TOUCH_ROTATE; this._moveCurr.copy(this._getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)); this._movePrev.copy(this._moveCurr); break; default: // 2 or more this._state = Trackball.STATE.TOUCH_ZOOM_PAN; var dx = event.touches[0].pageX - event.touches[1].pageX; var dy = event.touches[0].pageY - event.touches[1].pageY; this._touchZoomDistanceEnd = this._touchZoomDistanceStart = Math.sqrt(dx * dx + dy * dy); var x = (event.touches[0].pageX + event.touches[1].pageX) / 2; var y = (event.touches[0].pageY + event.touches[1].pageY) / 2; this._panStart.copy(this._getMouseOnScreen(x, y)); this._panEnd.copy(this._panStart); break; } }; _proto.onTouchMove = function onTouchMove(event) { if (this.enabled === false) return; switch (event.touches.length) { case 1: this._movePrev.copy(this._moveCurr); this._moveCurr.copy(this._getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)); break; default: // 2 or more var dx = event.touches[0].pageX - event.touches[1].pageX; var dy = event.touches[0].pageY - event.touches[1].pageY; this._touchZoomDistanceEnd = Math.sqrt(dx * dx + dy * dy); var x = (event.touches[0].pageX + event.touches[1].pageX) / 2; var y = (event.touches[0].pageY + event.touches[1].pageY) / 2; this._panEnd.copy(this._getMouseOnScreen(x, y)); break; } }; _proto.onTouchEnd = function onTouchEnd(event) { if (this.enabled === false) return; switch (event.touches.length) { case 0: this._state = Trackball.STATE.NONE; break; case 1: this._state = Trackball.STATE.TOUCH_ROTATE; this._moveCurr.copy(this._getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)); this._movePrev.copy(this._moveCurr); break; } }; _createClass(Trackball, [{ key: "up", set: function set(value) { var quat = new THREE.Quaternion().setFromUnitVectors(value, new THREE.Vector3(0, 1, 0)); this._quatInverse = quat.clone().inverse(); } }]); return Trackball; }(), Object.defineProperty(_class, "STATE", { configurable: true, enumerable: true, writable: true, value: { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 //returns the camera being managed by these controls } }), _temp); export { Trackball as default };