Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
09a2210
PoC 3D picking
felixpalmer Jan 13, 2026
5e3749b
Augment pickable prop
felixpalmer Jan 13, 2026
f59fb30
Tidy
felixpalmer Jan 13, 2026
a2e343b
hover support
felixpalmer Jan 13, 2026
97e0ce4
Add elevation label to terrain app
felixpalmer Jan 14, 2026
d1c1c76
Rotation pivot point fixed
felixpalmer Jan 13, 2026
85b1fcf
3d enabled rotation pivot point
felixpalmer Jan 13, 2026
068c4cc
prototype rotation pivot
felixpalmer Jan 13, 2026
50743f2
Hook up picking with rotation pivot
felixpalmer Jan 13, 2026
771c3f3
rotation pivot helper
felixpalmer Jan 13, 2026
e614ac3
Map3dcontroller
felixpalmer Jan 14, 2026
e367799
rotatePivot API
felixpalmer Jan 14, 2026
67914ba
Use planes in demo
felixpalmer Jan 14, 2026
79d2d7f
Shorthand
felixpalmer Jan 15, 2026
9544d5d
Revert vite config
felixpalmer Jan 15, 2026
9eebac1
remove extra override
felixpalmer Jan 15, 2026
90352d0
Unify naming
felixpalmer Jan 15, 2026
11d9dde
Lint
felixpalmer Jan 15, 2026
3b3da3d
Merge branch 'master' into felix/rotate-pivot-3d
felixpalmer Jan 15, 2026
447bdcb
Merge branch 'master' into felix/rotate-pivot-3d
felixpalmer Feb 23, 2026
207e042
tidy
felixpalmer Feb 25, 2026
91b45cb
docs
felixpalmer Feb 25, 2026
549bbdb
Move rotationPivot into MapController
felixpalmer Feb 25, 2026
e4d3713
Move pickPosition
felixpalmer Feb 25, 2026
c400256
typing
felixpalmer Feb 25, 2026
830e939
Revert test app
felixpalmer Feb 25, 2026
ba3ea71
Succinct panByPosition3D
felixpalmer Feb 25, 2026
c8b5d6f
Merge branch 'master' into felix/rotate-pivot-3d
felixpalmer Feb 25, 2026
e929374
Merge branch 'master' into felix/rotate-pivot-3d
felixpalmer Feb 25, 2026
69ce703
Docs
felixpalmer Feb 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
285 changes: 239 additions & 46 deletions examples/get-started/pure-js/basic/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,256 @@
// Copyright (c) vis.gl contributors

import {Deck} from '@deck.gl/core';
import {GeoJsonLayer, ArcLayer} from '@deck.gl/layers';
import {Tile3DLayer} from '@deck.gl/geo-layers';
import {ScenegraphLayer} from '@deck.gl/mesh-layers';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test app for demo only, will not be commiting

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the layers all have a nice mdx pattern of rendering sample data and interactivity - should we consider doing the same per controller? Theres over 5 controllers now.

import {ScatterplotLayer} from '@deck.gl/layers';

// source: Natural Earth http://www.naturalearthdata.com/ via geojson.xyz
const COUNTRIES =
'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_admin_0_scale_rank.geojson'; //eslint-disable-line
const AIR_PORTS =
'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson';
const GOOGLE_MAPS_API_KEY = process.env.GoogleMapsAPIKey; // eslint-disable-line
const TILESET_URL = 'https://tile.googleapis.com/v1/3dtiles/root.json';
const AIRPLANE_MODEL_URL =
'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/scenegraph-layer/airplane.glb';

// NYC area airports
const AIRPORTS = [
{name: 'JFK', coordinates: [-73.7781, 40.6413]},
{name: 'LaGuardia', coordinates: [-73.874, 40.7769]},
{name: 'Newark', coordinates: [-74.1745, 40.6895]},
{name: 'Teterboro', coordinates: [-74.0608, 40.8501]},
{name: 'Westchester County', coordinates: [-73.7076, 41.067]}
];

const INITIAL_VIEW_STATE = {
latitude: 51.47,
longitude: 0.45,
zoom: 4,
latitude: 40.7128,
longitude: -74.006,
zoom: 12,
bearing: 0,
pitch: 30
pitch: 60,
position: [0, 0, 0] // Keep camera at ground level
};

new Deck({
// Track rotation pivot for visual feedback
let rotationPivotPosition = null;
let currentRotationPivot = '3d';

const deck = new Deck({
initialViewState: INITIAL_VIEW_STATE,
controller: true,
controller: {rotationPivot: currentRotationPivot},
onInteractionStateChange: state => {
rotationPivotPosition = state.rotationPivotPosition || null;
updateLayers();
},
onClick: info => {
console.log('=== CLICK INFO ===');
console.log('picked:', info.picked);
console.log('coordinate:', info.coordinate);
console.log('object:', info.object);
console.log('layer:', info.layer?.id);

if (info.picked && info.coordinate && info.coordinate.length === 3) {
console.log('3D coordinate - altitude:', info.coordinate[2], 'meters');
}
},
getTooltip: info => {
if (info.picked && info.coordinate && info.coordinate.length === 3) {
const altitude = info.coordinate[2];
return {
html: `<div style="background: rgba(0, 0, 0, 0.8); color: white; padding: 8px 12px; border-radius: 4px; font-family: monospace;">
Altitude: ${altitude.toFixed(1)} m
</div>`,
style: {
padding: '0'
}
};
}
return null;
},
layers: [
new GeoJsonLayer({
id: 'base-map',
data: COUNTRIES,
// Styles
stroked: true,
filled: true,
lineWidthMinPixels: 2,
opacity: 0.4,
getLineColor: [60, 60, 60],
getFillColor: [200, 200, 200]
new Tile3DLayer({
id: 'google-3d-tiles',
data: TILESET_URL,
pickable: '3d',
onTilesetLoad: tileset3d => {
tileset3d.options.onTraversalComplete = selectedTiles => {
return selectedTiles;
const uniqueCredits = new Set();
selectedTiles.forEach(tile => {
const {copyright} = tile.content.gltf.asset;
copyright.split(';').forEach(uniqueCredits.add, uniqueCredits);
});
const creditsText = [...uniqueCredits].join('; ');
// Display credits in console
return selectedTiles;
};
},
loadOptions: {
fetch: {headers: {'X-GOOG-API-KEY': GOOGLE_MAPS_API_KEY}}
},
operation: 'terrain+draw'
}),
new GeoJsonLayer({
new ScenegraphLayer({
id: 'airports',
data: AIR_PORTS,
// Styles
filled: true,
pointRadiusMinPixels: 2,
pointRadiusScale: 2000,
getPointRadius: f => 11 - f.properties.scalerank,
getFillColor: [200, 0, 80, 180],
// Interactive props
data: AIRPORTS,
pickable: true,
autoHighlight: true,
onClick: info =>
// eslint-disable-next-line
info.object && alert(`${info.object.properties.name} (${info.object.properties.abbrev})`)
}),
new ArcLayer({
id: 'arcs',
data: AIR_PORTS,
dataTransform: d => d.features.filter(f => f.properties.scalerank < 4),
// Styles
getSourcePosition: f => [-0.4531566, 51.4709959], // London
getTargetPosition: f => f.geometry.coordinates,
getSourceColor: [0, 128, 200],
getTargetColor: [200, 0, 80],
getWidth: 1
scenegraph: AIRPLANE_MODEL_URL,
sizeScale: 10,
sizeMinPixels: 2,
sizeMaxPixels: 50,
_animations: {
'*': {speed: 1}
},
getPosition: d => [...d.coordinates, 2000],
getOrientation: d => [0, 0, 90], // pitch, yaw, roll
getScale: [1, 1, 1],
_lighting: 'pbr'
})
]
});

// Export for debugging
window.deck = deck;

// Add toggle controls
const controls = document.createElement('div');
controls.style.position = 'absolute';
controls.style.top = '10px';
controls.style.left = '10px';
controls.style.zIndex = '1000';
controls.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
controls.style.padding = '10px';
controls.style.borderRadius = '4px';
controls.style.fontFamily = 'monospace';
controls.style.color = 'white';

const tilesToggle = document.createElement('button');
tilesToggle.textContent = 'Hide 3D Tiles';
tilesToggle.style.marginRight = '10px';
tilesToggle.style.padding = '5px 10px';
tilesToggle.style.cursor = 'pointer';

const airportsToggle = document.createElement('button');
airportsToggle.textContent = 'Hide Planes';
airportsToggle.style.padding = '5px 10px';
airportsToggle.style.cursor = 'pointer';

// Add rotation pivot mode selector
const pivotLabel = document.createElement('div');
pivotLabel.textContent = 'Rotation Pivot:';
pivotLabel.style.marginTop = '10px';
pivotLabel.style.marginBottom = '5px';

const pivotOptions = document.createElement('div');
pivotOptions.style.display = 'flex';
pivotOptions.style.flexDirection = 'column';
pivotOptions.style.gap = '5px';

['center', '2d', '3d'].forEach(mode => {
const label = document.createElement('label');
label.style.cursor = 'pointer';
label.style.display = 'flex';
label.style.alignItems = 'center';
label.style.gap = '5px';

const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'rotationPivot';
radio.value = mode;
radio.checked = mode === currentRotationPivot;
radio.onchange = () => {
currentRotationPivot = mode;
deck.setProps({
controller: {rotationPivot: mode}
});
};

const text = document.createElement('span');
text.textContent =
mode === 'center'
? 'Center (default)'
: mode === '2d'
? '2D (ground level)'
: '3D (picked altitude)';

label.appendChild(radio);
label.appendChild(text);
pivotOptions.appendChild(label);
});

controls.appendChild(tilesToggle);
controls.appendChild(airportsToggle);
controls.appendChild(pivotLabel);
controls.appendChild(pivotOptions);
document.body.appendChild(controls);

let tilesVisible = true;
let airportsVisible = true;

tilesToggle.onclick = () => {
tilesVisible = !tilesVisible;
tilesToggle.textContent = tilesVisible ? 'Hide 3D Tiles' : 'Show 3D Tiles';
updateLayers();
};

airportsToggle.onclick = () => {
airportsVisible = !airportsVisible;
airportsToggle.textContent = airportsVisible ? 'Hide Planes' : 'Show Planes';
updateLayers();
};

function updateLayers() {
const layers = [
new Tile3DLayer({
id: 'google-3d-tiles',
data: TILESET_URL,
pickable: '3d',
visible: tilesVisible,
onTilesetLoad: tileset3d => {
tileset3d.options.onTraversalComplete = selectedTiles => {
return selectedTiles;
};
},
loadOptions: {
fetch: {headers: {'X-GOOG-API-KEY': GOOGLE_MAPS_API_KEY}}
},
operation: 'terrain+draw'
}),
new ScenegraphLayer({
id: 'airports',
data: AIRPORTS,
pickable: true,
visible: airportsVisible,
scenegraph: AIRPLANE_MODEL_URL,
sizeScale: 10,
sizeMinPixels: 2,
sizeMaxPixels: 50,
_animations: {
'*': {speed: 1}
},
getPosition: d => [...d.coordinates, 2000],
getOrientation: d => [0, 0, 90], // pitch, yaw, roll
getScale: [1, 1, 1],
_lighting: 'pbr'
})
];

// Add rotation pivot indicator when rotating
if (rotationPivotPosition) {
layers.push(
new ScatterplotLayer({
id: 'rotation-pivot',
data: [rotationPivotPosition],
getPosition: d => d,
getRadius: 8,
radiusUnits: 'pixels',
getLineColor: [255, 255, 255, 180],
getLineWidth: 1,
lineWidthUnits: 'pixels',
stroked: true,
filled: false,
billboard: true,
parameters: {depthWriteEnabled: false, depthCompare: 'always'}
})
);
}

deck.setProps({layers});
}
18 changes: 16 additions & 2 deletions examples/website/terrain/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,24 @@ export default function App({
elevationData: TERRAIN_IMAGE,
texture,
wireframe,
color: [255, 255, 255]
color: [255, 255, 255],
pickable: '3d'
});

return <DeckGL initialViewState={initialViewState} controller={true} layers={[layer]} />;
return (
<DeckGL
initialViewState={initialViewState}
controller={true}
layers={[layer]}
getTooltip={info => {
if (info.picked && info.coordinate && info.coordinate.length === 3) {
const elevation = info.coordinate[2];
return `Elevation: ${elevation.toFixed(0)} m`;
}
return null;
}}
/>
);
}

export function renderToDOM(container: HTMLDivElement) {
Expand Down
Loading