UNPKG

@deck.gl/core

Version:

deck.gl core library

392 lines (338 loc) 10.7 kB
// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import Controller from './controller'; import ViewState from './view-state'; import {mod} from '../utils/math-utils'; import type Viewport from '../viewports/viewport'; import LinearInterpolator from '../transitions/linear-interpolator'; import {Vector3, _SphericalCoordinates as SphericalCoordinates, clamp} from '@math.gl/core'; const MOVEMENT_SPEED = 20; const PAN_SPEED = 500; type FirstPersonStateProps = { width: number; height: number; position?: number[]; bearing?: number; pitch?: number; // Geospatial anchor longitude?: number | null; latitude?: number | null; maxPitch?: number; minPitch?: number; }; type FirstPersonStateInternal = { startRotatePos?: [number, number]; startBearing?: number; startPitch?: number; startZoomPosition?: number[]; startPanPos?: [number, number]; startPanPosition?: number[]; }; class FirstPersonState extends ViewState< FirstPersonState, FirstPersonStateProps, FirstPersonStateInternal > { makeViewport: (props: Record<string, any>) => Viewport; constructor( options: FirstPersonStateProps & FirstPersonStateInternal & { makeViewport: (props: Record<string, any>) => Viewport; } ) { const { /* Viewport arguments */ width, // Width of viewport height, // Height of viewport // Position and orientation position = [0, 0, 0], // typically in meters from anchor point bearing = 0, // Rotation around y axis pitch = 0, // Rotation around x axis // Geospatial anchor longitude = null, latitude = null, maxPitch = 90, minPitch = -90, // Model state when the rotate operation first started startRotatePos, startBearing, startPitch, startZoomPosition, startPanPos, startPanPosition } = options; super( { width, height, position, bearing, pitch, longitude, latitude, maxPitch, minPitch }, { startRotatePos, startBearing, startPitch, startZoomPosition, startPanPos, startPanPosition } ); this.makeViewport = options.makeViewport; } /* Public API */ /** * Start panning * @param {[Number, Number]} pos - position on screen where the pointer grabs */ panStart({pos}): FirstPersonState { const {position} = this.getViewportProps(); return this._getUpdatedState({ startPanPos: pos, startPanPosition: position }); } /** * Pan * @param {[Number, Number]} pos - position on screen where the pointer is */ pan({pos}): FirstPersonState { if (!pos) { return this; } const {startPanPos = [0, 0], startPanPosition = [0, 0]} = this.getState(); const {width, height, bearing, pitch} = this.getViewportProps(); const deltaScaleX = (PAN_SPEED * (pos[0] - startPanPos[0])) / width; const deltaScaleY = (PAN_SPEED * (pos[1] - startPanPos[1])) / height; const up = new SphericalCoordinates({bearing, pitch}); const forward = new SphericalCoordinates({bearing, pitch: -90}); const yDirection = up.toVector3().normalize(); const xDirection = forward.toVector3().cross(yDirection).normalize(); return this._getUpdatedState({ position: new Vector3(startPanPosition) .add(xDirection.scale(deltaScaleX)) .add(yDirection.scale(deltaScaleY)) }); } /** * End panning * Must call if `panStart()` was called */ panEnd(): FirstPersonState { return this._getUpdatedState({ startPanPos: null, startPanPosition: null }); } /** * Start rotating * @param {[Number, Number]} pos - position on screen where the pointer grabs */ rotateStart({pos}: {pos: [number, number]}): FirstPersonState { return this._getUpdatedState({ startRotatePos: pos, startBearing: this.getViewportProps().bearing, startPitch: this.getViewportProps().pitch }); } /** * Rotate * @param {[Number, Number]} pos - position on screen where the pointer is */ rotate({ pos, deltaAngleX = 0, deltaAngleY = 0 }: { pos?: [number, number]; deltaAngleX?: number; deltaAngleY: number; }): FirstPersonState { const {startRotatePos, startBearing, startPitch} = this.getState(); const {width, height} = this.getViewportProps(); if (!startRotatePos || startBearing === undefined || startPitch === undefined) { return this; } let newRotation; if (pos) { const deltaScaleX = (pos[0] - startRotatePos[0]) / width; const deltaScaleY = (pos[1] - startRotatePos[1]) / height; newRotation = { bearing: startBearing - deltaScaleX * 180, pitch: startPitch - deltaScaleY * 90 }; } else { newRotation = { bearing: startBearing - deltaAngleX, pitch: startPitch - deltaAngleY }; } return this._getUpdatedState(newRotation); } /** * End rotating * Must call if `rotateStart()` was called */ rotateEnd(): FirstPersonState { return this._getUpdatedState({ startRotatePos: null, startBearing: null, startPitch: null }); } /** * Start zooming * @param {[Number, Number]} pos - position on screen where the pointer grabs */ zoomStart(): FirstPersonState { return this._getUpdatedState({ startZoomPosition: this.getViewportProps().position }); } /** * Zoom * @param {[Number, Number]} pos - position on screen where the current center is * @param {[Number, Number]} startPos - the center position at * the start of the operation. Must be supplied of `zoomStart()` was not called * @param {Number} scale - a number between [0, 1] specifying the accumulated * relative scale. */ zoom({pos, scale}: {pos: [number, number]; scale: number}): FirstPersonState { const viewportProps = this.getViewportProps(); const startZoomPosition = this.getState().startZoomPosition || viewportProps.position; const viewport = this.makeViewport(viewportProps); const {projectionMatrix, width} = viewport; const fovxRadians = 2.0 * Math.atan(1.0 / projectionMatrix[0]); const angle = fovxRadians * (pos[0] / width - 0.5); const direction = this.getDirection(true); return this._move( direction.rotateZ({radians: -angle}), Math.log2(scale) * MOVEMENT_SPEED, startZoomPosition ); } /** * End zooming * Must call if `zoomStart()` was called */ zoomEnd(): FirstPersonState { return this._getUpdatedState({ startZoomPosition: null }); } moveLeft(speed: number = MOVEMENT_SPEED): FirstPersonState { const direction = this.getDirection(true); return this._move(direction.rotateZ({radians: Math.PI / 2}), speed); } moveRight(speed: number = MOVEMENT_SPEED): FirstPersonState { const direction = this.getDirection(true); return this._move(direction.rotateZ({radians: -Math.PI / 2}), speed); } // forward moveUp(speed: number = MOVEMENT_SPEED): FirstPersonState { const direction = this.getDirection(true); return this._move(direction, speed); } // backward moveDown(speed: number = MOVEMENT_SPEED): FirstPersonState { const direction = this.getDirection(true); return this._move(direction.negate(), speed); } rotateLeft(speed: number = 15): FirstPersonState { return this._getUpdatedState({ bearing: this.getViewportProps().bearing - speed }); } rotateRight(speed: number = 15): FirstPersonState { return this._getUpdatedState({ bearing: this.getViewportProps().bearing + speed }); } rotateUp(speed: number = 10): FirstPersonState { return this._getUpdatedState({ pitch: this.getViewportProps().pitch + speed }); } rotateDown(speed: number = 10): FirstPersonState { return this._getUpdatedState({ pitch: this.getViewportProps().pitch - speed }); } zoomIn(speed: number = MOVEMENT_SPEED): FirstPersonState { return this._move(new Vector3(0, 0, 1), speed); } zoomOut(speed: number = MOVEMENT_SPEED): FirstPersonState { return this._move(new Vector3(0, 0, -1), speed); } // shortest path between two view states shortestPathFrom(viewState: FirstPersonState) { const fromProps = viewState.getViewportProps(); const props = {...this.getViewportProps()}; const {bearing, longitude} = props; if (Math.abs(bearing - fromProps.bearing) > 180) { props.bearing = bearing < 0 ? bearing + 360 : bearing - 360; } if ( longitude !== null && fromProps.longitude !== null && Math.abs(longitude - fromProps.longitude) > 180 ) { props.longitude = longitude < 0 ? longitude + 360 : longitude - 360; } return props; } /* Private methods */ _move( direction: Vector3, speed: number, fromPosition: number[] = this.getViewportProps().position ) { const delta = direction.scale(speed); return this._getUpdatedState({ position: new Vector3(fromPosition).add(delta) }); } getDirection(use2D: boolean = false): Vector3 { const spherical = new SphericalCoordinates({ bearing: this.getViewportProps().bearing, pitch: use2D ? 90 : 90 + this.getViewportProps().pitch }); const direction = spherical.toVector3().normalize(); return direction; } _getUpdatedState(newProps: Record<string, any>): FirstPersonState { // Update _viewportProps return new FirstPersonState({ makeViewport: this.makeViewport, ...this.getViewportProps(), ...this.getState(), ...newProps }); } // Apply any constraints (mathematical or defined by _viewportProps) to map state applyConstraints(props: Required<FirstPersonStateProps>): Required<FirstPersonStateProps> { // Ensure pitch and zoom are within specified range const {pitch, maxPitch, minPitch, longitude, bearing} = props; props.pitch = clamp(pitch, minPitch, maxPitch); // Normalize degrees if (longitude !== null && (longitude < -180 || longitude > 180)) { props.longitude = mod(longitude + 180, 360) - 180; } if (bearing < -180 || bearing > 180) { props.bearing = mod(bearing + 180, 360) - 180; } return props; } } export default class FirstPersonController extends Controller<FirstPersonState> { ControllerState = FirstPersonState; transition = { transitionDuration: 300, transitionInterpolator: new LinearInterpolator(['position', 'pitch', 'bearing']) }; }