@deck.gl/core
Version:
deck.gl core library
127 lines • 6 kB
JavaScript
// 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