Skip to content
Merged
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
11 changes: 11 additions & 0 deletions modules/google-maps/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ export function createDeckInstance(
// The basemap owns the shared canvas in interleaved mode; Deck only forwards the preferred DPR.
// In non-interleaved mode this still feeds the luma canvas context that Deck creates.
useDevicePixels: props.useDevicePixels ?? true,
...(!props.gl && {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is pretty crazy that we have to write nested code like this just to override or pass a prop.
A result of the unfortunate prop type overloading in luma CanvasContext.
I will see if we can propose improved props in luma.gl

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

😬 yeah.. I held my breath a bit pushing this one up. Lmk if you figure something out

@chrisgervang chrisgervang Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Left comments on the RFC, thanks for following up!

deviceProps: {
...props.deviceProps,
createCanvasContext: {
...(typeof props.deviceProps?.createCanvasContext === 'object'
? props.deviceProps.createCanvasContext
: undefined),
pixelSizeSource: 'css-dpr'
}
}
}),
Comment thread
chrisgervang marked this conversation as resolved.
style: props.interleaved ? null : {pointerEvents: 'none'},
parent: getContainer(overlay, props.style),
views: new MapView({repeat: true}),
Expand Down
9 changes: 9 additions & 0 deletions modules/mapbox/src/mapbox-overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ export default class MapboxOverlay implements IControl {
this._deck = new Deck<any>({
...this._props,
parent: container,
deviceProps: {
...this._props.deviceProps,
createCanvasContext: {
...(typeof this._props.deviceProps?.createCanvasContext === 'object'
? this._props.deviceProps.createCanvasContext
: undefined),
pixelSizeSource: 'css-dpr'
}
},
parameters: {...getDefaultParameters(map, false), ...this._props.parameters},
views: this._getViews(map),
viewState: getViewState(map)
Expand Down
125 changes: 125 additions & 0 deletions test/apps/fractional-zoom-alignment/app.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
//
// Test app for https://github.com/visgl/deck.gl/issues/10173
// Change browser zoom (Cmd+/Cmd-) to verify deck.gl overlay stays aligned with Mapbox basemap.

import React from 'react';
import {createRoot} from 'react-dom/client';
import {Map, useControl} from 'react-map-gl/mapbox';
import {ScatterplotLayer} from 'deck.gl';
import {MapboxOverlay as DeckOverlay} from '@deck.gl/mapbox';
import 'mapbox-gl/dist/mapbox-gl.css';

const MAPBOX_TOKEN = process.env.MapboxAccessToken; // eslint-disable-line

const INITIAL_VIEW_STATE = {
longitude: -80,
latitude: 40,
zoom: 3
};

const data = [
{pos: [-80.0133149, 40.4554421]},
{pos: [-8.0133149, 40.4554421]},
{pos: [-84.0133149, 40.4554421]},
{pos: [-86.0133149, 40.4554421]},
{pos: [-88.0133149, 40.4554421]},
{pos: [-80.0133149, 42.4554421]},
{pos: [-80.0133149, 44.4554421]},
{pos: [-80.0133149, 46.4554421]},
{pos: [-80.0133149, 48.4554421]}
];

const layers = [
new ScatterplotLayer({
id: 'test',
data,
getPosition: d => d.pos,
pickable: true,
opacity: 1.0,
stroked: true,
filled: true,
radiusScale: 6,
radiusMinPixels: 1,
radiusMaxPixels: 1000,
lineWidthMinPixels: 1,
getRadius: 10000,
getFillColor: [255, 140, 0],
getLineColor: [0, 0, 0]
})
];

function DeckGLOverlay(props) {
const overlay = useControl(() => {
const o = new DeckOverlay(props);
window.__deckOverlay = o;
return o;
});
overlay.setProps(props);
return null;
}

function DiagPanel() {
const [info, setInfo] = React.useState('');
React.useEffect(() => {
const update = () => {
const deck = window.__deckOverlay?._deck;
const mapCanvas = document.querySelector('.mapboxgl-canvas');
const deckCanvas = document.getElementById('deckgl-overlay');
if (!deck || !mapCanvas || !deckCanvas) return;
const lines = [
`DPR: ${window.devicePixelRatio.toFixed(4)}`,
`Map draw: ${mapCanvas.width}x${mapCanvas.height}`,
`Deck draw: ${deckCanvas.width}x${deckCanvas.height}`,
`Map CSS: ${mapCanvas.clientWidth}x${mapCanvas.clientHeight}`,
`Deck CSS: ${deckCanvas.clientWidth}x${deckCanvas.clientHeight}`,
`Deck viewport: ${deck.width}x${deck.height}`,
`Match: ${mapCanvas.width === deckCanvas.width && mapCanvas.height === deckCanvas.height}`
];
setInfo(lines.join('\n'));
};
const id = setInterval(update, 200);
return () => clearInterval(id);
}, []);
return (
<pre
style={{
position: 'fixed',
top: 10,
left: 10,
zIndex: 9999,
background: 'rgba(0,0,0,0.8)',
color: '#0f0',
padding: 10,
fontSize: 12,
fontFamily: 'monospace',
pointerEvents: 'none'
}}
>
{info}
</pre>
);
}

function Root() {
return (
<>
<DiagPanel />
<Map
initialViewState={INITIAL_VIEW_STATE}
mapStyle="mapbox://styles/mapbox/streets-v9"
mapboxAccessToken={MAPBOX_TOKEN}
reuseMaps
dragRotate={false}
touchPitch={false}
>
<DeckGLOverlay layers={layers} />
</Map>
</>
);
}

/* global document */
createRoot(document.getElementById('root')).render(<Root />);
24 changes: 24 additions & 0 deletions test/apps/fractional-zoom-alignment/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>deck.gl Test: Fractional Zoom Alignment (#10173)</title>
<style>
body {
width: 100%;
max-width: 100%;
height: 100vh;
margin: 0;
padding: 0;
}
#root {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="root"></div>
</body>
<script type="module" src="app.jsx"></script>
</html>
16 changes: 16 additions & 0 deletions test/apps/fractional-zoom-alignment/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"scripts": {
"start": "vite",
"start-local": "vite --config ../vite.config.local.mjs"
},
"dependencies": {
"deck.gl": "^9.0.0",
"mapbox-gl": "^3.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-map-gl": "^8.0.0"
},
"devDependencies": {
"vite": "^7.3.3"
}
}
11 changes: 11 additions & 0 deletions test/apps/fractional-zoom-alignment/vite.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {defineConfig} from 'vite';

export default defineConfig({
define: {
'process.env.MapboxAccessToken': JSON.stringify(process.env.MapboxAccessToken)
},
server: {
open: true,
port: 8080
}
});
14 changes: 14 additions & 0 deletions test/modules/google-maps/google-maps-overlay.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ test('GoogleMapsOverlay#interleaved prop', () => {
expect(!overlay.props.interleaved, 'interleaved set to false').toBeTruthy();
});

test('GoogleMapsOverlay#pixelSizeSource css-dpr when no external gl', () => {
const map = new mapsApi.Map({width: 1, height: 1, longitude: 0, latitude: 0, zoom: 1});

const overlay = new GoogleMapsOverlay(withDevice({interleaved: false, layers: []}));
overlay.setMap(map);
map.emit({type: 'renderingtype_changed'});
expect(
overlay._deck.props.deviceProps?.createCanvasContext?.pixelSizeSource,
'pixelSizeSource is css-dpr when Deck creates its own context'
).toBe('css-dpr');

overlay.finalize();
});

test('GoogleMapsOverlay#useDevicePixels prop', () => {
const map = new mapsApi.Map({width: 1, height: 1, longitude: 0, latitude: 0, zoom: 1});

Expand Down
22 changes: 22 additions & 0 deletions test/modules/mapbox/mapbox-overlay.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,28 @@ class TestScatterplotLayer extends ScatterplotLayer {
}
TestScatterplotLayer.layerName = 'TestScatterplotLayer';

test('MapboxOverlay#overlaid passes pixelSizeSource css-dpr', () => {
const map = new MockMapboxMap({
center: {lng: -122.45, lat: 37.78},
zoom: 14
});
const overlay = new MapboxOverlay({
device,
layers: [new ScatterplotLayer()]
});

map.addControl(overlay);

const deck = overlay._deck;
expect(deck, 'Deck instance is created').toBeTruthy();
expect(
deck.props.deviceProps?.createCanvasContext?.pixelSizeSource,
'pixelSizeSource is css-dpr in overlaid mode'
).toBe('css-dpr');

map.removeControl(overlay);
});

test('MapboxOverlay#overlaid', async () => {
const map = new MockMapboxMap({
center: {lng: -122.45, lat: 37.78},
Expand Down