UNPKG

@deck.gl/core

Version:

deck.gl core library

127 lines 6 kB
// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import MapController from "./map-controller.js"; /** * Controller that extends MapController with terrain-aware behavior. * The camera smoothly follows terrain elevation during pan/zoom. */ export default class TerrainController extends MapController { constructor() { super(...arguments); /** Cached terrain altitude from depth picking at viewport center (smoothed) */ this._terrainAltitude = undefined; /** Raw (unsmoothed) terrain altitude from latest pick */ this._terrainAltitudeTarget = undefined; /** rAF handle for periodic terrain altitude picking */ this._pickFrameId = null; /** Timestamp of last pick */ this._lastPickTime = 0; } setProps(props) { super.setProps({ rotationPivot: '3d', ...props }); // Periodically pick terrain altitude at the viewport center using rAF. // Keeps the altitude cache warm so interactions don't need expensive // synchronous GPU readbacks. rAF naturally pauses when tab is backgrounded. if (this._pickFrameId === null) { const loop = () => { const now = Date.now(); if (now - this._lastPickTime > 500 && !this.isDragging()) { this._lastPickTime = now; this._pickTerrainCenterAltitude(); // On first successful pick, rebase viewport to terrain altitude. // Runs from rAF (outside React render) so onViewStateChange won't loop. if (this._terrainAltitude === undefined && this._terrainAltitudeTarget !== undefined) { this._terrainAltitude = this._terrainAltitudeTarget; const controllerState = new this.ControllerState({ makeViewport: this.makeViewport, ...this.props, ...this.state }); const rebaseProps = this._rebaseViewport(this._terrainAltitudeTarget, controllerState); if (rebaseProps) { // Build a controllerState that includes the rebase adjustments so // internal state matches the rebased viewState after React round-trip. const rebasedState = new this.ControllerState({ makeViewport: this.makeViewport, ...this.props, ...this.state, ...rebaseProps }); super.updateViewport(rebasedState); } } } this._pickFrameId = requestAnimationFrame(loop); }; this._pickFrameId = requestAnimationFrame(loop); } } finalize() { if (this._pickFrameId !== null) { cancelAnimationFrame(this._pickFrameId); this._pickFrameId = null; } super.finalize(); } updateViewport(newControllerState, extraProps = null, interactionState = {}) { // Not initialized yet — pass through to MapController if (this._terrainAltitude === undefined) { super.updateViewport(newControllerState, extraProps, interactionState); return; } // Smoothly blend toward target altitude const SMOOTHING = 0.05; this._terrainAltitude += (this._terrainAltitudeTarget - this._terrainAltitude) * SMOOTHING; const viewportProps = newControllerState.getViewportProps(); const pos = viewportProps.position || [0, 0, 0]; extraProps = { ...extraProps, position: [pos[0], pos[1], this._terrainAltitude] }; super.updateViewport(newControllerState, extraProps, interactionState); } _pickTerrainCenterAltitude() { if (!this.pickPosition) { return; } const { x, y, width, height } = this.props; const pickResult = this.pickPosition(x + width / 2, y + height / 2); if (pickResult?.coordinate && pickResult.coordinate.length >= 3) { this._terrainAltitudeTarget = pickResult.coordinate[2]; } } /** * Compute viewport adjustments to keep the view visually the same * when shifting position to [0, 0, altitude]. */ _rebaseViewport(altitude, newControllerState) { const viewportProps = newControllerState.getViewportProps(); const oldViewport = this.makeViewport({ ...viewportProps, position: [0, 0, 0] }); const oldCameraPos = oldViewport.cameraPosition; const centerZOffset = altitude * oldViewport.distanceScales.unitsPerMeter[2]; const cameraHeightAboveOldCenter = oldCameraPos[2]; const newCameraHeightAboveCenter = cameraHeightAboveOldCenter - centerZOffset; if (newCameraHeightAboveCenter <= 0) { return null; } const zoomDelta = Math.log2(cameraHeightAboveOldCenter / newCameraHeightAboveCenter); const newZoom = viewportProps.zoom + zoomDelta; const newViewport = this.makeViewport({ ...viewportProps, zoom: newZoom, position: [0, 0, altitude] }); const { width, height } = viewportProps; const screenCenter = [width / 2, height / 2]; const worldPoint = oldViewport.unproject(screenCenter, { targetZ: altitude }); if (worldPoint && 'panByPosition3D' in newViewport && typeof newViewport.panByPosition3D === 'function') { const adjusted = newViewport.panByPosition3D(worldPoint, screenCenter); return { position: [0, 0, altitude], zoom: newZoom, ...adjusted }; } return null; } } //# sourceMappingURL=terrain-controller.js.map