UNPKG

itowns

Version:

A JS/WebGL framework for 3D geospatial data visualization

1,143 lines (952 loc) 46.6 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _typeof = require("@babel/runtime/helpers/typeof"); Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = exports.PLANAR_CONTROL_EVENT = exports.STATE = exports.keys = void 0; var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); var _assertThisInitialized2 = _interopRequireDefault(require("@babel/runtime/helpers/assertThisInitialized")); var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits")); var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn")); var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf")); var THREE = _interopRequireWildcard(require("three")); var _MainLoop = require("../Core/MainLoop"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function () { var Super = (0, _getPrototypeOf2["default"])(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = (0, _getPrototypeOf2["default"])(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return (0, _possibleConstructorReturn2["default"])(this, result); }; } function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } // event keycode var keys = { CTRL: 17, SPACE: 32, T: 84, Y: 89 }; exports.keys = keys; var mouseButtons = { LEFTCLICK: THREE.MOUSE.LEFT, MIDDLECLICK: THREE.MOUSE.MIDDLE, RIGHTCLICK: THREE.MOUSE.RIGHT }; var currentPressedButton; // starting camera position and orientation target var startPosition = new THREE.Vector3(); var startQuaternion = new THREE.Quaternion(); // camera initial zoom value if orthographic var cameraInitialZoom = 0; // point under the cursor var pointUnderCursor = new THREE.Vector3(); // control state var STATE = { NONE: -1, DRAG: 0, PAN: 1, ROTATE: 2, TRAVEL: 3, ORTHO_ZOOM: 4 }; // cursor shape linked to control state exports.STATE = STATE; var cursor = { "default": 'auto', drag: 'move', pan: 'cell', travel: 'wait', rotate: 'move', ortho_zoom: 'wait' }; var vectorZero = new THREE.Vector3(); // mouse movement var mousePosition = new THREE.Vector2(); var lastMousePosition = new THREE.Vector2(); var deltaMousePosition = new THREE.Vector2(0, 0); // drag movement var dragStart = new THREE.Vector3(); var dragEnd = new THREE.Vector3(); var dragDelta = new THREE.Vector3(); // camera focus point : ground point at screen center var centerPoint = new THREE.Vector3(0, 0, 0); // camera rotation var phi = 0.0; // displacement and rotation vectors var vect = new THREE.Vector3(); var quat = new THREE.Quaternion(); var vect2 = new THREE.Vector2(); // animated travel var travelEndPos = new THREE.Vector3(); var travelStartPos = new THREE.Vector3(); var travelStartRot = new THREE.Quaternion(); var travelEndRot = new THREE.Quaternion(); var travelAlpha = 0; var travelDuration = 0; var travelUseRotation = false; var travelUseSmooth = false; // zoom changes (for orthographic camera) var startZoom = 0; var endZoom = 0; // ray caster for drag movement var rayCaster = new THREE.Raycaster(); var plane = new THREE.Plane(new THREE.Vector3(0, 0, -1)); // default parameters : var defaultOptions = { enableRotation: true, rotateSpeed: 2.0, minPanSpeed: 0.05, maxPanSpeed: 15, zoomTravelTime: 0.2, // must be a number zoomFactor: 2, maxResolution: 1 / Infinity, minResolution: Infinity, maxAltitude: 50000000, groundLevel: 200, autoTravelTimeMin: 1.5, autoTravelTimeMax: 4, autoTravelTimeDist: 50000, smartTravelHeightMin: 75, smartTravelHeightMax: 500, instantTravel: false, minZenithAngle: 0, maxZenithAngle: 82.5, focusOnMouseOver: true, focusOnMouseClick: true, handleCollision: true, minDistanceCollision: 30, enableSmartTravel: true, enablePan: true }; var PLANAR_CONTROL_EVENT = { MOVED: 'moved' }; /** * Planar controls is a camera controller adapted for a planar view, with animated movements. * Usage is as follow : * <ul> * <li><b>Left mouse button:</b> drag the camera (translation on the (xy) world plane).</li> * <li><b>Right mouse button:</b> pan the camera (translation on the vertical (z) axis of the world plane).</li> * <li><b>CTRL + Left mouse button:</b> rotate the camera around the focus point.</li> * <li><b>Wheel scrolling:</b> zoom toward the cursor position.</li> * <li><b>Wheel clicking:</b> smart zoom toward the cursor position (animated).</li> * <li><b>Y key:</b> go to the starting view (animated).</li> * <li><b>T key:</b> go to the top view (animated).</li> * </ul> * * @class PlanarControls * @param {PlanarView} view the view where the controls will be used * @param {object} options * @param {boolean} [options.enableRotation=true] Enable the rotation with the `CTRL + Left mouse button` * and in animations, like the smart zoom. * @param {boolean} [options.enableSmartTravel=true] Enable smart travel with the `wheel-click / space-bar`. * @param {boolean} [options.enablePan=true] Enable pan movements with the `right-click`. * @param {number} [options.rotateSpeed=2.0] Rotate speed. * @param {number} [options.maxPanSpeed=15] Pan speed when close to maxAltitude. * @param {number} [options.minPanSpeed=0.05] Pan speed when close to the ground. * @param {number} [options.zoomTravelTime=0.2] Animation time when zooming. * @param {number} [options.zoomFactor=2] The factor the scale is multiplied by when zooming * in and divided by when zooming out. This factor can't be null. * @param {number} [options.maxResolution=0] The smallest size in meters a pixel at the center of the * view can represent. * @param {number} [options.minResolution=Infinity] The biggest size in meters a pixel at the center of the * view can represent. * @param {number} [options.maxAltitude=12000] Maximum altitude reachable when panning or zooming out. * @param {number} [options.groundLevel=200] Minimum altitude reachable when panning. * @param {number} [options.autoTravelTimeMin=1.5] Minimum duration for animated travels with the `auto` * parameter. * @param {number} [options.autoTravelTimeMax=4] Maximum duration for animated travels with the `auto` * parameter. * @param {number} [options.autoTravelTimeDist=20000] Maximum travel distance for animated travel with the * `auto` parameter. * @param {number} [options.smartTravelHeightMin=75] Minimum height above ground reachable after a smart * travel. * @param {number} [options.smartTravelHeightMax=500] Maximum height above ground reachable after a smart * travel. * @param {boolean} [options.instantTravel=false] If set to true, animated travels will have no duration. * @param {number} [options.minZenithAngle=0] The minimum reachable zenith angle for a camera * rotation, in degrees. * @param {number} [options.maxZenithAngle=82.5] The maximum reachable zenith angle for a camera * rotation, in degrees. * @param {boolean} [options.focusOnMouseOver=true] Set the focus on the canvas if hovered. * @param {boolean} [options.focusOnMouseClick=true] Set the focus on the canvas if clicked. * @param {boolean} [options.handleCollision=true] */ exports.PLANAR_CONTROL_EVENT = PLANAR_CONTROL_EVENT; var PlanarControls = /*#__PURE__*/function (_THREE$EventDispatche) { (0, _inherits2["default"])(PlanarControls, _THREE$EventDispatche); var _super = _createSuper(PlanarControls); function PlanarControls(view) { var _this; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; (0, _classCallCheck2["default"])(this, PlanarControls); _this = _super.call(this); _this.view = view; _this.camera = view.camera.camera3D; if (_this.camera.isOrthographicCamera) { cameraInitialZoom = _this.camera.zoom; // enable rotation movements _this.enableRotation = false; // enable pan movements _this.enablePan = false; // Camera altitude is clamped under maxAltitude. // This is not relevant for an orthographic camera (since the orthographic camera altitude won't change). // Therefore, neutralizing by default the maxAltitude limit allows zooming out with an orthographic camera, // no matter its initial position. _this.maxAltitude = Infinity; // the zoom travel time (stored in `this.zoomTravelTime`) can't be `auto` with an orthographic camera _this.zoomTravelTime = typeof options.zoomTravelTime === 'number' ? options.zoomTravelTime : defaultOptions.zoomTravelTime; } else { // enable rotation movements _this.enableRotation = options.enableRotation === undefined ? defaultOptions.enableRotation : options.enableRotation; _this.rotateSpeed = options.rotateSpeed || defaultOptions.rotateSpeed; // enable pan movements _this.enablePan = options.enablePan === undefined ? defaultOptions.enablePan : options.enablePan; // minPanSpeed when close to the ground, maxPanSpeed when close to maxAltitude _this.minPanSpeed = options.minPanSpeed || defaultOptions.minPanSpeed; _this.maxPanSpeed = options.maxPanSpeed || defaultOptions.maxPanSpeed; // camera altitude is clamped under maxAltitude _this.maxAltitude = options.maxAltitude || defaultOptions.maxAltitude; // animation duration for the zoom _this.zoomTravelTime = options.zoomTravelTime || defaultOptions.zoomTravelTime; } // zoom movement is equal to the distance to the zoom target, multiplied by zoomFactor if (options.zoomInFactor) { console.warn('Controls zoomInFactor parameter is deprecated. Use zoomFactor instead.'); options.zoomFactor = options.zoomFactor || options.zoomInFactor; } if (options.zoomOutFactor) { console.warn('Controls zoomOutFactor parameter is deprecated. Use zoomFactor instead.'); options.zoomFactor = options.zoomFactor || options.zoomInFactor || 1 / options.zoomOutFactor; } if (options.zoomFactor === 0) { console.warn('Controls zoomFactor parameter can not be equal to 0. Its value will be set to default.'); options.zoomFactor = defaultOptions.zoomFactor; } _this.zoomInFactor = options.zoomFactor || defaultOptions.zoomFactor; _this.zoomOutFactor = 1 / (options.zoomFactor || defaultOptions.zoomFactor); // the maximum and minimum size (in meters) a pixel at the center of the view can represent _this.maxResolution = options.maxResolution || defaultOptions.maxResolution; _this.minResolution = options.minResolution || defaultOptions.minResolution; // approximate ground altitude value. Camera altitude is clamped above groundLevel _this.groundLevel = options.groundLevel || defaultOptions.groundLevel; // min and max duration in seconds, for animated travels with `auto` parameter _this.autoTravelTimeMin = options.autoTravelTimeMin || defaultOptions.autoTravelTimeMin; _this.autoTravelTimeMax = options.autoTravelTimeMax || defaultOptions.autoTravelTimeMax; // max travel duration is reached for this travel distance (empirical smoothing value) _this.autoTravelTimeDist = options.autoTravelTimeDist || defaultOptions.autoTravelTimeDist; // after a smartZoom, camera height above ground will be between these two values if (options.smartZoomHeightMin) { console.warn('Controls smartZoomHeightMin parameter is deprecated. Use smartTravelHeightMin instead.'); options.smartTravelHeightMin = options.smartTravelHeightMin || options.smartZoomHeightMin; } if (options.smartZoomHeightMax) { console.warn('Controls smartZoomHeightMax parameter is deprecated. Use smartTravelHeightMax instead.'); options.smartTravelHeightMax = options.smartTravelHeightMax || options.smartZoomHeightMax; } _this.smartTravelHeightMin = options.smartTravelHeightMin || defaultOptions.smartTravelHeightMin; _this.smartTravelHeightMax = options.smartTravelHeightMax || defaultOptions.smartTravelHeightMax; // if set to true, animated travels have 0 duration _this.instantTravel = options.instantTravel || defaultOptions.instantTravel; // the zenith angle for a camera rotation will be between these two values _this.minZenithAngle = (options.minZenithAngle || defaultOptions.minZenithAngle) * Math.PI / 180; // max value should be less than 90 deg (90 = parallel to the ground) _this.maxZenithAngle = (options.maxZenithAngle || defaultOptions.maxZenithAngle) * Math.PI / 180; // focus policy options _this.focusOnMouseOver = options.focusOnMouseOver || defaultOptions.focusOnMouseOver; _this.focusOnMouseClick = options.focusOnMouseClick || defaultOptions.focusOnMouseClick; // set collision options _this.handleCollision = options.handleCollision === undefined ? defaultOptions.handleCollision : options.handleCollision; _this.minDistanceCollision = defaultOptions.minDistanceCollision; // enable smart travel _this.enableSmartTravel = options.enableSmartTravel === undefined ? defaultOptions.enableSmartTravel : options.enableSmartTravel; startPosition.copy(_this.camera.position); startQuaternion.copy(_this.camera.quaternion); // control state _this.state = STATE.NONE; _this.cursor = cursor; if (_this.view.controls) { // esLint-disable-next-line no-console console.warn('Deprecated use of PlanarControls. See examples to correct PlanarControls implementation.'); _this.view.controls.dispose(); } _this.view.controls = (0, _assertThisInitialized2["default"])(_this); // eventListeners handlers _this._handlerOnKeyDown = _this.onKeyDown.bind((0, _assertThisInitialized2["default"])(_this)); _this._handlerOnMouseDown = _this.onMouseDown.bind((0, _assertThisInitialized2["default"])(_this)); _this._handlerOnMouseUp = _this.onMouseUp.bind((0, _assertThisInitialized2["default"])(_this)); _this._handlerOnMouseMove = _this.onMouseMove.bind((0, _assertThisInitialized2["default"])(_this)); _this._handlerOnMouseWheel = _this.onMouseWheel.bind((0, _assertThisInitialized2["default"])(_this)); _this._handlerFocusOnMouseClick = _this.onMouseClick.bind((0, _assertThisInitialized2["default"])(_this)); _this._handlerFocusOnMouseOver = _this.onMouseOver.bind((0, _assertThisInitialized2["default"])(_this)); _this._handlerContextMenu = _this.onContextMenu.bind((0, _assertThisInitialized2["default"])(_this)); _this._handlerUpdate = _this.update.bind((0, _assertThisInitialized2["default"])(_this)); // add this PlanarControl instance to the view's frameRequesters // with this, PlanarControl.update() will be called each frame _this.view.addFrameRequester(_MainLoop.MAIN_LOOP_EVENTS.AFTER_CAMERA_UPDATE, _this._handlerUpdate); // event listeners for user input (to activate the controls) _this.addInputListeners(); return _this; } (0, _createClass2["default"])(PlanarControls, [{ key: "dispose", value: function dispose() { this.removeInputListeners(); this.view.removeFrameRequester(_MainLoop.MAIN_LOOP_EVENTS.AFTER_CAMERA_UPDATE, this._handlerUpdate); } /** * update the view and camera if needed, and handles the animated travel * @param {number} dt the delta time between two updates in millisecond * @param {boolean} updateLoopRestarted true if we just started rendering * @ignore */ }, { key: "update", value: function update(dt, updateLoopRestarted) { // dt will not be relevant when we just started rendering. We consider a 1-frame move in this case if (updateLoopRestarted) { dt = 16; } var onMovement = this.state !== STATE.NONE; switch (this.state) { case STATE.TRAVEL: this.handleTravel(dt); this.view.notifyChange(this.camera); break; case STATE.ORTHO_ZOOM: this.handleZoomOrtho(dt); this.view.notifyChange(this.camera); break; case STATE.DRAG: this.handleDragMovement(); this.view.notifyChange(this.camera); break; case STATE.ROTATE: this.handleRotation(); this.view.notifyChange(this.camera); break; case STATE.PAN: this.handlePanMovement(); this.view.notifyChange(this.camera); break; case STATE.NONE: default: break; } // We test if camera collides to the geometry layer or is too close to the ground, and adjust its altitude in // case if (this.handleCollision) { // check distance to the ground/surface geometry (could be another geometry layer) this.view.camera.adjustAltitudeToAvoidCollisionWithLayer(this.view, this.view.tileLayer, this.minDistanceCollision); } if (onMovement) { this.view.dispatchEvent({ type: PLANAR_CONTROL_EVENT.MOVED }); } deltaMousePosition.set(0, 0); } /** * Initiate a drag movement (translation on (xy) plane). The movement value is derived from the actual world * point under the mouse cursor. This allows user to 'grab' a world point and drag it to move. * * @ignore */ }, { key: "initiateDrag", value: function initiateDrag() { this.state = STATE.DRAG; // the world point under mouse cursor when the drag movement is started dragStart.copy(this.getWorldPointAtScreenXY(mousePosition)); // the difference between start and end cursor position dragDelta.set(0, 0, 0); } /** * Handle the drag movement (translation on (xy) plane) when user moves the mouse while in STATE.DRAG. The * drag movement is previously initiated by [initiateDrag]{@link PlanarControls#initiateDrag}. Compute the * drag value and update the camera controls. The movement value is derived from the actual world point under * the mouse cursor. This allows the user to 'grab' a world point and drag it to move. * * @ignore */ }, { key: "handleDragMovement", value: function handleDragMovement() { // the world point under the current mouse cursor position, at same altitude than dragStart this.getWorldPointFromMathPlaneAtScreenXY(mousePosition, dragStart.z, dragEnd); // the difference between start and end cursor position dragDelta.subVectors(dragStart, dragEnd); // update the camera position this.camera.position.add(dragDelta); dragDelta.set(0, 0, 0); } /** * Initiate a pan movement (local translation on (xz) plane). * * @ignore */ }, { key: "initiatePan", value: function initiatePan() { this.state = STATE.PAN; } /** * Handle the pan movement (translation on local x / world z plane) when user moves the mouse while * STATE.PAN. The drag movement is previously initiated by [initiatePan]{@link PlanarControls#initiatePan}. * Compute the pan value and update the camera controls. * * @ignore */ }, { key: "handlePanMovement", value: function handlePanMovement() { vect.set(-deltaMousePosition.x, deltaMousePosition.y, 0); this.camera.localToWorld(vect); this.camera.position.copy(vect); } /** * Initiate a rotate (orbit) movement. * * @ignore */ }, { key: "initiateRotation", value: function initiateRotation() { this.state = STATE.ROTATE; centerPoint.copy(this.getWorldPointAtScreenXY(new THREE.Vector2(0.5 * this.view.mainLoop.gfxEngine.width, 0.5 * this.view.mainLoop.gfxEngine.height))); var radius = this.camera.position.distanceTo(centerPoint); phi = Math.acos((this.camera.position.z - centerPoint.z) / radius); } /** * Handle the rotate movement (orbit) when user moves the mouse while in STATE.ROTATE. The movement is an * orbit around `centerPoint`, the camera focus point (ground point at screen center). The rotate movement * is previously initiated in [initiateRotation]{@link PlanarControls#initiateRotation}. * Compute the new position value and update the camera controls. * * @ignore */ }, { key: "handleRotation", value: function handleRotation() { // angle deltas // deltaMousePosition is computed in onMouseMove / onMouseDowns var thetaDelta = -this.rotateSpeed * deltaMousePosition.x / this.view.mainLoop.gfxEngine.width; var phiDelta = -this.rotateSpeed * deltaMousePosition.y / this.view.mainLoop.gfxEngine.height; // the vector from centerPoint (focus point) to camera position var offset = this.camera.position.clone().sub(centerPoint); if (thetaDelta !== 0 || phiDelta !== 0) { if (phi + phiDelta >= this.minZenithAngle && phi + phiDelta <= this.maxZenithAngle && phiDelta !== 0) { // rotation around X (altitude) phi += phiDelta; vect.set(0, 0, 1); quat.setFromUnitVectors(this.camera.up, vect); offset.applyQuaternion(quat); vect.setFromMatrixColumn(this.camera.matrix, 0); quat.setFromAxisAngle(vect, phiDelta); offset.applyQuaternion(quat); vect.set(0, 0, 1); quat.setFromUnitVectors(this.camera.up, vect).invert(); offset.applyQuaternion(quat); } if (thetaDelta !== 0) { // rotation around Z (azimuth) vect.set(0, 0, 1); quat.setFromAxisAngle(vect, thetaDelta); offset.applyQuaternion(quat); } } this.camera.position.copy(offset); // TODO : lookAt calls an updateMatrixWorld(). It should be replaced by a new method that does not. this.camera.lookAt(vectorZero); this.camera.position.add(centerPoint); this.camera.updateMatrixWorld(); } /** * Triggers a Zoom animated movement (travel) toward / away from the world point under the mouse cursor. The * zoom intensity varies according to the distance between the camera and the point. The closer to the ground, * the lower the intensity. Orientation will not change (null parameter in the call to * [initiateTravel]{@link PlanarControls#initiateTravel} function). * * @param {Event} event the mouse wheel event. * @ignore */ }, { key: "initiateZoom", value: function initiateZoom(event) { var delta = -event.deltaY; pointUnderCursor.copy(this.getWorldPointAtScreenXY(mousePosition)); var newPos = new THREE.Vector3(); if (delta > 0 || delta < 0 && this.maxAltitude > this.camera.position.z) { var zoomFactor = delta > 0 ? this.zoomInFactor : this.zoomOutFactor; // do not zoom if the resolution after the zoom is outside resolution limits var endResolution = this.view.getPixelsToMeters() / zoomFactor; if (this.maxResolution > endResolution || endResolution > this.minResolution) { return; } // change the camera field of view if the camera is orthographic if (this.camera.isOrthographicCamera) { // switch state to STATE.ZOOM this.state = STATE.ORTHO_ZOOM; this.view.notifyChange(this.camera); // camera zoom at the beginning of zoom movement startZoom = this.camera.zoom; // camera zoom at the end of zoom movement endZoom = startZoom * zoomFactor; // the altitude of the target must be the same as camera's pointUnderCursor.z = this.camera.position.z; travelAlpha = 0; travelDuration = this.zoomTravelTime; this.updateMouseCursorType(); } else { // target position newPos.lerpVectors(this.camera.position, pointUnderCursor, 1 - 1 / zoomFactor); // initiate travel this.initiateTravel(newPos, this.zoomTravelTime, null, false); } } } /** * Handle the animated zoom change for an orthographic camera, when state is `ZOOM`. * * @param {number} dt the delta time between two updates in milliseconds * @ignore */ }, { key: "handleZoomOrtho", value: function handleZoomOrtho(dt) { travelAlpha = Math.min(travelAlpha + dt / 1000 / travelDuration, 1); // new zoom var zoom = startZoom + travelAlpha * (endZoom - startZoom); if (this.camera.zoom !== zoom) { // zoom has changed this.camera.zoom = zoom; this.camera.updateProjectionMatrix(); // current world coordinates under the mouse this.view.viewToNormalizedCoords(mousePosition, vect); vect.z = 0; vect.unproject(this.camera); // new camera position this.camera.position.x += pointUnderCursor.x - vect.x; this.camera.position.y += pointUnderCursor.y - vect.y; this.camera.updateMatrixWorld(true); } // completion test this.testAnimationEnd(); } /** * Triggers a 'smart zoom' animated movement (travel) toward the point under mouse cursor. The camera will be * smoothly moved and oriented close to the target, at a determined height and distance. * * @ignore */ }, { key: "initiateSmartTravel", value: function initiateSmartTravel() { var pointUnderCursor = this.getWorldPointAtScreenXY(mousePosition); // direction of the movement, projected on xy plane and normalized var dir = new THREE.Vector3(); dir.copy(pointUnderCursor).sub(this.camera.position); dir.z = 0; dir.normalize(); var distanceToPoint = this.camera.position.distanceTo(pointUnderCursor); // camera height (altitude above ground) at the end of the travel, 5000 is an empirical smoothing distance var targetHeight = THREE.MathUtils.lerp(this.smartTravelHeightMin, this.smartTravelHeightMax, Math.min(distanceToPoint / 5000, 1)); // camera position at the end of the travel var moveTarget = new THREE.Vector3(); moveTarget.copy(pointUnderCursor); if (this.enableRotation) { moveTarget.add(dir.multiplyScalar(-targetHeight * 2)); } moveTarget.z = pointUnderCursor.z + targetHeight; if (this.camera.isOrthographicCamera) { startZoom = this.camera.zoom; // camera zoom at the end of the travel, 5000 is an empirical smoothing distance endZoom = startZoom * (1 + Math.min(distanceToPoint / 5000, 1)); moveTarget.z = this.camera.position.z; } // initiate the travel this.initiateTravel(moveTarget, 'auto', pointUnderCursor, true); } /** * Triggers an animated movement and rotation for the camera. * * @param {THREE.Vector3} targetPos The target position of the camera (reached at the end). * @param {number|string} travelTime Set to `auto` or set to a duration in seconds. If set to `auto`, * travel time will be set to a duration between `autoTravelTimeMin` and `autoTravelTimeMax` according to * the distance and the angular difference between start and finish. * @param {(string|THREE.Vector3|THREE.Quaternion)} targetOrientation define the target rotation of * the camera : * <ul> * <li>if targetOrientation is a world point (Vector3) : the camera will lookAt() this point</li> * <li>if targetOrientation is a quaternion : this quaternion will define the final camera orientation </li> * <li>if targetOrientation is neither a world point nor a quaternion : the camera will keep its starting * orientation</li> * </ul> * @param {boolean} useSmooth animation is smoothed using the `smooth(value)` function (slower * at start and finish). * * @ignore */ }, { key: "initiateTravel", value: function initiateTravel(targetPos, travelTime, targetOrientation, useSmooth) { this.state = STATE.TRAVEL; this.view.notifyChange(this.camera); // the progress of the travel (animation alpha) travelAlpha = 0; // update cursor this.updateMouseCursorType(); travelUseRotation = this.enableRotation && targetOrientation && (targetOrientation.isQuaternion || targetOrientation.isVector3); travelUseSmooth = useSmooth; // start position (current camera position) travelStartPos.copy(this.camera.position); // start rotation (current camera rotation) travelStartRot.copy(this.camera.quaternion); // setup the end rotation : if (travelUseRotation) { if (targetOrientation.isQuaternion) { // case where targetOrientation is a quaternion travelEndRot.copy(targetOrientation); } else if (targetOrientation.isVector3) { // case where targetOrientation is a Vector3 if (targetPos === targetOrientation) { this.camera.lookAt(targetOrientation); travelEndRot.copy(this.camera.quaternion); this.camera.quaternion.copy(travelStartRot); } else { this.camera.position.copy(targetPos); this.camera.lookAt(targetOrientation); travelEndRot.copy(this.camera.quaternion); this.camera.quaternion.copy(travelStartRot); this.camera.position.copy(travelStartPos); } } } // end position travelEndPos.copy(targetPos); // beginning of the travel duration setup if (this.instantTravel) { travelDuration = 0; } else if (travelTime === 'auto') { // case where travelTime is set to `auto` : travelDuration will be a value between autoTravelTimeMin and // autoTravelTimeMax depending on travel distance and travel angular difference // a value between 0 and 1 according to the travel distance. Adjusted by autoTravelTimeDist parameter var normalizedDistance = Math.min(1, targetPos.distanceTo(this.camera.position) / this.autoTravelTimeDist); travelDuration = THREE.MathUtils.lerp(this.autoTravelTimeMin, this.autoTravelTimeMax, normalizedDistance); // if travel changes camera orientation, travel duration is adjusted according to angularDifference // this allows for a smoother travel (more time for the camera to rotate) // final duration will not exceed autoTravelTimeMax if (travelUseRotation) { // value is normalized between 0 and 1 var angularDifference = 0.5 - 0.5 * travelEndRot.normalize().dot(this.camera.quaternion.normalize()); travelDuration *= 1 + 2 * angularDifference; travelDuration = Math.min(travelDuration, this.autoTravelTimeMax); } } else { // case where travelTime !== `auto` : travelTime is a duration in seconds given as parameter travelDuration = travelTime; } } /** * Handle the animated movement and rotation of the camera in `travel` state. * * @param {number} dt the delta time between two updates in milliseconds * @ignore */ }, { key: "handleTravel", value: function handleTravel(dt) { travelAlpha = Math.min(travelAlpha + dt / 1000 / travelDuration, 1); // the animation alpha, between 0 (start) and 1 (finish) var alpha = travelUseSmooth ? this.smooth(travelAlpha) : travelAlpha; // new position this.camera.position.lerpVectors(travelStartPos, travelEndPos, alpha); var zoom = startZoom + alpha * (endZoom - startZoom); // new zoom if (this.camera.isOrthographicCamera && this.camera.zoom !== zoom) { this.camera.zoom = zoom; this.camera.updateProjectionMatrix(); } // new rotation if (travelUseRotation === true) { this.camera.quaternion.slerpQuaternions(travelStartRot, travelEndRot, alpha); } // completion test this.testAnimationEnd(); } /** * Test if the currently running animation is finished (travelAlpha reached 1). * If it is, reset controls to state NONE. * * @ignore */ }, { key: "testAnimationEnd", value: function testAnimationEnd() { if (travelAlpha === 1) { // Resume normal behaviour after animation is completed this.state = STATE.NONE; this.updateMouseCursorType(); } } /** * Triggers an animated movement (travel) to set the camera to top view, above the focus point, * at altitude = distanceToFocusPoint. * * @ignore */ }, { key: "goToTopView", value: function goToTopView() { var topViewPos = new THREE.Vector3(); var targetQuat = new THREE.Quaternion(); // the top view position is above the camera focus point, at an altitude = distanceToPoint topViewPos.copy(this.getWorldPointAtScreenXY(new THREE.Vector2(0.5 * this.view.mainLoop.gfxEngine.width, 0.5 * this.view.mainLoop.gfxEngine.height))); topViewPos.z += Math.min(this.maxAltitude, this.camera.position.distanceTo(topViewPos)); targetQuat.setFromAxisAngle(new THREE.Vector3(1, 0, 0), 0); // initiate the travel this.initiateTravel(topViewPos, 'auto', targetQuat, true); } /** * Triggers an animated movement (travel) to set the camera to starting view * * @ignore */ }, { key: "goToStartView", value: function goToStartView() { // if startZoom and endZoom have not been set yet, give them neutral values if (this.camera.isOrthographicCamera) { startZoom = this.camera.zoom; endZoom = cameraInitialZoom; } this.initiateTravel(startPosition, 'auto', startQuaternion, true); } /** * Returns the world point (xyz) under the posXY screen point. The point belong to an abstract mathematical * plane of specified altitude (does not us actual geometry). * * @param {THREE.Vector2} posXY the mouse position in screen space (unit : pixel) * @param {number} altitude the altitude (z) of the mathematical plane * @param {THREE.Vector3} target the target vector3 * @return {THREE.Vector3} * @ignore */ }, { key: "getWorldPointFromMathPlaneAtScreenXY", value: function getWorldPointFromMathPlaneAtScreenXY(posXY, altitude) { var target = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : new THREE.Vector3(); vect2.copy(this.view.viewToNormalizedCoords(posXY)); rayCaster.setFromCamera(vect2, this.camera); plane.constant = altitude; rayCaster.ray.intersectPlane(plane, target); return target; } /** * Returns the world point (xyz) under the posXY screen point. If geometry is under the cursor, the point is * obtained with getPickingPositionFromDepth. If no geometry is under the cursor, the point is obtained with * [getWorldPointFromMathPlaneAtScreenXY]{@link PlanarControls#getWorldPointFromMathPlaneAtScreenXY}. * * @param {THREE.Vector2} posXY the mouse position in screen space (unit : pixel) * @param {THREE.Vector3} target the target World coordinates. * @return {THREE.Vector3} * @ignore */ }, { key: "getWorldPointAtScreenXY", value: function getWorldPointAtScreenXY(posXY) { var target = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : new THREE.Vector3(); // check if there is a valid geometry under cursor if (this.view.getPickingPositionFromDepth(posXY, target)) { return target; } else { // if not, we use the mathematical plane at altitude = groundLevel this.getWorldPointFromMathPlaneAtScreenXY(posXY, this.groundLevel, target); return target; } } /** * Add all the input event listeners (activate the controls). * * @ignore */ }, { key: "addInputListeners", value: function addInputListeners() { this.view.domElement.addEventListener('keydown', this._handlerOnKeyDown, false); this.view.domElement.addEventListener('mousedown', this._handlerOnMouseDown, false); this.view.domElement.addEventListener('mouseup', this._handlerOnMouseUp, false); this.view.domElement.addEventListener('mouseleave', this._handlerOnMouseUp, false); this.view.domElement.addEventListener('mousemove', this._handlerOnMouseMove, false); this.view.domElement.addEventListener('wheel', this._handlerOnMouseWheel, false); // focus policy if (this.focusOnMouseOver) { this.view.domElement.addEventListener('mouseover', this._handlerFocusOnMouseOver, false); } if (this.focusOnMouseClick) { this.view.domElement.addEventListener('click', this._handlerFocusOnMouseClick, false); } // prevent the default context menu from appearing when right-clicking // this allows to use right-click for input without the menu appearing this.view.domElement.addEventListener('contextmenu', this._handlerContextMenu, false); } /** * Removes all the input listeners (deactivate the controls). * * @ignore */ }, { key: "removeInputListeners", value: function removeInputListeners() { this.view.domElement.removeEventListener('keydown', this._handlerOnKeyDown, true); this.view.domElement.removeEventListener('mousedown', this._handlerOnMouseDown, false); this.view.domElement.removeEventListener('mouseup', this._handlerOnMouseUp, false); this.view.domElement.removeEventListener('mouseleave', this._handlerOnMouseUp, false); this.view.domElement.removeEventListener('mousemove', this._handlerOnMouseMove, false); this.view.domElement.removeEventListener('wheel', this._handlerOnMouseWheel, false); this.view.domElement.removeEventListener('mouseover', this._handlerFocusOnMouseOver, false); this.view.domElement.removeEventListener('click', this._handlerFocusOnMouseClick, false); this.view.domElement.removeEventListener('contextmenu', this._handlerContextMenu, false); } /** * Update the cursor image according to the control state. * * @ignore */ }, { key: "updateMouseCursorType", value: function updateMouseCursorType() { switch (this.state) { case STATE.NONE: this.view.domElement.style.cursor = this.cursor["default"]; break; case STATE.DRAG: this.view.domElement.style.cursor = this.cursor.drag; break; case STATE.PAN: this.view.domElement.style.cursor = this.cursor.pan; break; case STATE.TRAVEL: this.view.domElement.style.cursor = this.cursor.travel; break; case STATE.ORTHO_ZOOM: this.view.domElement.style.cursor = this.cursor.ortho_zoom; break; case STATE.ROTATE: this.view.domElement.style.cursor = this.cursor.rotate; break; default: break; } } }, { key: "updateMousePositionAndDelta", value: function updateMousePositionAndDelta(event) { this.view.eventToViewCoords(event, mousePosition); deltaMousePosition.copy(mousePosition).sub(lastMousePosition); lastMousePosition.copy(mousePosition); } /** * cursor modification for a specifique state. * * @param {string} state the state in which we want to change the cursor ('default', 'drag', 'pan', 'travel', 'rotate'). * @param {string} newCursor the css cursor we want to have for the specified state. * @ignore */ }, { key: "setCursor", value: function setCursor(state, newCursor) { this.cursor[state] = newCursor; this.updateMouseCursorType(); } /** * Catch and manage the event when a touch on the mouse is downs. * * @param {Event} event the current event (mouse left or right button clicked, mouse wheel button actioned). * @ignore */ }, { key: "onMouseDown", value: function onMouseDown(event) { event.preventDefault(); if (STATE.NONE !== this.state) { return; } currentPressedButton = event.button; this.updateMousePositionAndDelta(event); if (mouseButtons.LEFTCLICK === event.button) { if (event.ctrlKey) { if (this.enableRotation) { this.initiateRotation(); } else { return; } } else { this.initiateDrag(); } } else if (mouseButtons.MIDDLECLICK === event.button) { if (this.enableSmartTravel) { this.initiateSmartTravel(); } else { return; } } else if (mouseButtons.RIGHTCLICK === event.button) { if (this.enablePan) { this.initiatePan(); } else { return; } } this.updateMouseCursorType(); } /** * Catch and manage the event when a touch on the mouse is released. * * @param {Event} event the current event * @ignore */ }, { key: "onMouseUp", value: function onMouseUp(event) { event.preventDefault(); // Does not interrupt ongoing camera action if state is TRAVEL or CAMERA_OTHO. This prevents interrupting a zoom // movement or a smart travel by pressing any movement key. // The camera action is also uninterrupted if the released button does not match the button triggering the // ongoing action. This prevents for instance exiting drag mode when right-clicking while dragging the view. if (STATE.TRAVEL !== this.state && STATE.ORTHO_ZOOM !== this.state && currentPressedButton === event.button) { this.state = STATE.NONE; } this.updateMouseCursorType(); } /** * Catch and manage the event when the mouse is moved. * * @param {Event} event the current event. * @ignore */ }, { key: "onMouseMove", value: function onMouseMove(event) { event.preventDefault(); this.updateMousePositionAndDelta(event); // notify change if moving if (STATE.NONE !== this.state) { this.view.notifyChange(); } } /** * Catch and manage the event when a key is down. * * @param {Event} event the current event * @ignore */ }, { key: "onKeyDown", value: function onKeyDown(event) { if (STATE.NONE !== this.state) { return; } switch (event.keyCode) { case keys.T: // going to top view is not relevant for an orthographic camera, since it is always top view if (!this.camera.isOrthographicCamera) { this.goToTopView(); } break; case keys.Y: this.goToStartView(); break; case keys.SPACE: if (this.enableSmartTravel) { this.initiateSmartTravel(event); } break; default: break; } } /** * Catch and manage the event when the mouse wheel is rolled. * * @param {Event} event the current event * @ignore */ }, { key: "onMouseWheel", value: function onMouseWheel(event) { event.preventDefault(); event.stopPropagation(); if (STATE.NONE === this.state) { this.initiateZoom(event); } } /** * Set the focus on view's domElement according to focus policy regarding MouseOver * * @ignore */ }, { key: "onMouseOver", value: function onMouseOver() { this.view.domElement.focus(); } /** * Set the focus on view's domElement according to focus policy regarding MouseClick * * @ignore */ }, { key: "onMouseClick", value: function onMouseClick() { this.view.domElement.focus(); } /** * Catch and manage the event when the context menu is called (by a right-click on the window). We use this * to prevent the context menu from appearing so we can use right click for other inputs. * * @param {Event} event the current event * @ignore */ }, { key: "onContextMenu", value: function onContextMenu(event) { event.preventDefault(); } /** * Smoothing function (sigmoid) : based on h01 Hermite function. * * @param {number} value the value to be smoothed, between 0 and 1. * @return {number} a value between 0 and 1. * @ignore */ }, { key: "smooth", value: function smooth(value) { // p between 1.0 and 1.5 (empirical) return Math.pow(Math.pow(value, 2) * (3 - 2 * value), 1.20); } }]); return PlanarControls; }(THREE.EventDispatcher); var _default = PlanarControls; exports["default"] = _default;