Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 ===');

Check failure on line 45 in examples/get-started/pure-js/basic/app.js

View workflow job for this annotation

GitHub Actions / test-node

'console' is not defined
console.log('picked:', info.picked);

Check failure on line 46 in examples/get-started/pure-js/basic/app.js

View workflow job for this annotation

GitHub Actions / test-node

'console' is not defined
console.log('coordinate:', info.coordinate);

Check failure on line 47 in examples/get-started/pure-js/basic/app.js

View workflow job for this annotation

GitHub Actions / test-node

'console' is not defined
console.log('object:', info.object);

Check failure on line 48 in examples/get-started/pure-js/basic/app.js

View workflow job for this annotation

GitHub Actions / test-node

'console' is not defined
console.log('layer:', info.layer?.id);

Check failure on line 49 in examples/get-started/pure-js/basic/app.js

View workflow job for this annotation

GitHub Actions / test-node

'console' is not defined

if (info.picked && info.coordinate && info.coordinate.length === 3) {
console.log('3D coordinate - altitude:', info.coordinate[2], 'meters');

Check failure on line 52 in examples/get-started/pure-js/basic/app.js

View workflow job for this annotation

GitHub Actions / test-node

'console' is not defined
}
},
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();

Check failure on line 77 in examples/get-started/pure-js/basic/app.js

View workflow job for this annotation

GitHub Actions / test-node

Unreachable code
selectedTiles.forEach(tile => {
const {copyright} = tile.content.gltf.asset;
copyright.split(';').forEach(uniqueCredits.add, uniqueCredits);
});
const creditsText = [...uniqueCredits].join('; ');

Check failure on line 82 in examples/get-started/pure-js/basic/app.js

View workflow job for this annotation

GitHub Actions / test-node

'creditsText' is assigned a value but never used

Check failure on line 82 in examples/get-started/pure-js/basic/app.js

View workflow job for this annotation

GitHub Actions / test-node

Unreachable code
// 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;

Check failure on line 112 in examples/get-started/pure-js/basic/app.js

View workflow job for this annotation

GitHub Actions / test-node

'window' is not defined

// 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});
}
21 changes: 19 additions & 2 deletions modules/core/src/controllers/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ export type ControllerOptions = {
dragMode?: 'pan' | 'rotate';
/** Enable inertia after panning/pinching. If a number is provided, indicates the duration of time over which the velocity reduces to zero, in milliseconds. Default `false`. */
inertia?: boolean | number;
/**
* Rotation pivot behavior:
* - 'center': Rotate around viewport center (default)
* - '2d': Rotate around pointer position at ground level (z=0)
* - '3d': Rotate around 3D picked point (requires pickPosition callback)
*/
rotationPivot?: 'center' | '2d' | '3d';
};

export type ControllerProps = {
Expand Down Expand Up @@ -92,6 +99,8 @@ export type InteractionState = {
isRotating?: boolean;
/** If the view is being zoomed, either from user input or transition */
isZooming?: boolean;
/** World coordinate [lng, lat, altitude] of rotation pivot point when rotating */
rotationPivotPosition?: [number, number, number];
}

/** Parameters passed to the onViewStateChange callback */
Expand Down Expand Up @@ -119,7 +128,8 @@ export default abstract class Controller<ControllerState extends IViewState<Cont
protected eventManager: EventManager;
protected onViewStateChange: (params: ViewStateChangeParameters) => void;
protected onStateChange: (state: InteractionState) => void;
protected makeViewport: (opts: Record<string, any>) => Viewport
protected makeViewport: (opts: Record<string, any>) => Viewport;
protected pickPosition?: (x: number, y: number) => {coordinate?: number[]} | null

private _controllerState?: ControllerState;
private _events: Record<string, boolean> = {};
Expand All @@ -128,11 +138,12 @@ export default abstract class Controller<ControllerState extends IViewState<Cont
};
private _customEvents: string[] = [];
private _eventStartBlocked: any = null;
private _panMove: boolean = false;
protected _panMove: boolean = false;

protected invertPan: boolean = false;
protected dragMode: 'pan' | 'rotate' = 'rotate';
protected inertia: number = 0;
protected rotationPivot: 'center' | '2d' | '3d' = 'center';
protected scrollZoom: boolean | {speed?: number; smooth?: boolean} = true;
protected dragPan: boolean = true;
protected dragRotate: boolean = true;
Expand All @@ -154,6 +165,7 @@ export default abstract class Controller<ControllerState extends IViewState<Cont
makeViewport: (opts: Record<string, any>) => Viewport;
onViewStateChange: (params: ViewStateChangeParameters) => void;
onStateChange: (state: InteractionState) => void;
pickPosition?: (x: number, y: number) => {coordinate?: number[]} | null;
}) {
this.transitionManager = new TransitionManager<ControllerState>({
...opts,
Expand All @@ -168,6 +180,7 @@ export default abstract class Controller<ControllerState extends IViewState<Cont
this.onViewStateChange = opts.onViewStateChange || (() => {});
this.onStateChange = opts.onStateChange || (() => {});
this.makeViewport = opts.makeViewport;
this.pickPosition = opts.pickPosition;
}

set events(customEvents) {
Expand Down Expand Up @@ -288,6 +301,9 @@ export default abstract class Controller<ControllerState extends IViewState<Cont
if (props.dragMode) {
this.dragMode = props.dragMode;
}
if (props.rotationPivot) {
this.rotationPivot = props.rotationPivot;
}
this.props = props;

if (!('transitionInterpolator' in props)) {
Expand Down Expand Up @@ -396,6 +412,7 @@ export default abstract class Controller<ControllerState extends IViewState<Cont
// invertPan is replaced by props.dragMode, keeping for backward compatibility
alternateMode = !alternateMode;
}

const newControllerState = this.controllerState[alternateMode ? 'panStart' : 'rotateStart']({
pos
});
Expand Down
Loading
Loading