• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / deck.gl / 30126626340

24 Jul 2026 09:07PM UTC coverage: 83.386% (+0.02%) from 83.367%
30126626340

push

github

web-flow
refactor(core): make WebGPU shader props unconditional (#10479)

8154 of 10264 branches covered (79.44%)

Branch coverage included in aggregate %.

2 of 2 new or added lines in 2 files covered. (100.0%)

4 existing lines in 1 file now uncovered.

14522 of 16930 relevant lines covered (85.78%)

18969.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.07
/modules/layers/src/bitmap-layer/bitmap-layer.ts
1
// deck.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import {
6
  Layer,
7
  color as colorModule,
8
  project32,
9
  picking,
10
  CoordinateSystem,
11
  LayerProps,
12
  PickingInfo,
13
  GetPickingInfoParams,
14
  UpdateParameters,
15
  Color,
16
  TextureSource,
17
  Position,
18
  DefaultProps
19
} from '@deck.gl/core';
20
import {Model} from '@luma.gl/engine';
21
import type {SamplerProps, Texture} from '@luma.gl/core';
22
import {lngLatToWorld} from '@math.gl/web-mercator';
23

24
import createMesh from './create-mesh';
25

26
import {bitmapUniforms, BitmapProps} from './bitmap-layer-uniforms';
27
import source from './bitmap-layer.wgsl';
28
import vs from './bitmap-layer-vertex';
29
import fs from './bitmap-layer-fragment';
30

31
const defaultProps: DefaultProps<BitmapLayerProps> = {
5✔
32
  image: {type: 'image', value: null, async: true},
33
  bounds: {type: 'array', value: [1, 0, 0, 1], compare: true},
34
  _imageCoordinateSystem: 'default',
35

36
  desaturate: {type: 'number', min: 0, max: 1, value: 0},
37
  // More context: because of the blending mode we're using for ground imagery,
38
  // alpha is not effective when blending the bitmap layers with the base map.
39
  // Instead we need to manually dim/blend rgb values with a background color.
40
  transparentColor: {type: 'color', value: [0, 0, 0, 0]},
41
  tintColor: {type: 'color', value: [255, 255, 255]},
42

43
  textureParameters: {type: 'object', ignore: true, value: null}
44
};
45

46
/** All properties supported by BitmapLayer. */
47
export type BitmapLayerProps = _BitmapLayerProps & LayerProps;
48
export type BitmapBoundingBox =
49
  | [left: number, bottom: number, right: number, top: number]
50
  | [Position, Position, Position, Position];
51

52
/** Properties added by BitmapLayer. */
53
type _BitmapLayerProps = {
54
  data: never;
55
  /**
56
   * The image to display.
57
   *
58
   * @default null
59
   */
60
  image?: string | TextureSource | null;
61

62
  /**
63
   * Supported formats:
64
   *  - Coordinates of the bounding box of the bitmap `[left, bottom, right, top]`
65
   *  - Coordinates of four corners of the bitmap, should follow the sequence of `[[left, bottom], [left, top], [right, top], [right, bottom]]`.
66
   *   Each position could optionally contain a third component `z`.
67
   * @default [1, 0, 0, 1]
68
   */
69
  bounds?: BitmapBoundingBox;
70

71
  /**
72
   * > Note: this prop is experimental.
73
   *
74
   * Specifies how image coordinates should be geographically interpreted.
75
   * @default COORDINATE_SYSTEM.DEFAULT
76
   */
77
  _imageCoordinateSystem?: CoordinateSystem;
78

79
  /**
80
   * The desaturation of the bitmap. Between `[0, 1]`.
81
   * @default 0
82
   */
83
  desaturate?: number;
84

85
  /**
86
   * The color to use for transparent pixels, in `[r, g, b, a]`.
87
   * @default [0, 0, 0, 0]
88
   */
89
  transparentColor?: Color;
90

91
  /**
92
   * The color to tint the bitmap by, in `[r, g, b]`.
93
   * @default [255, 255, 255]
94
   */
95
  tintColor?: Color;
96

97
  /** Customize the [texture parameters](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texParameter). */
98
  textureParameters?: SamplerProps | null;
99
};
100

101
export type BitmapLayerPickingInfo = PickingInfo<
102
  null,
103
  {
104
    bitmap: {
105
      /** Size of the original image */
106
      size: {
107
        width: number;
108
        height: number;
109
      };
110
      /** Hovered pixel uv in 0-1 range */
111
      uv: [number, number];
112
      /** Hovered pixel in the original image */
113
      pixel: [number, number];
114
    } | null;
115
  }
116
>;
117

118
/** Render a bitmap at specified boundaries. */
119
export default class BitmapLayer<ExtraPropsT extends {} = {}> extends Layer<
120
  ExtraPropsT & Required<_BitmapLayerProps>
121
> {
122
  static layerName = 'BitmapLayer';
5✔
123
  static defaultProps = defaultProps;
5✔
124

125
  state!: {
126
    disablePicking?: boolean;
127
    model?: Model;
128
    mesh?: any;
129
    coordinateConversion: number;
130
    bounds: [number, number, number, number];
131
  };
132

133
  getShaders() {
134
    return super.getShaders({
8✔
135
      vs,
136
      fs,
137
      source,
138
      modules: [colorModule, project32, picking, bitmapUniforms]
139
    });
140
  }
141

142
  initializeState() {
143
    const attributeManager = this.getAttributeManager()!;
8✔
144

145
    const noAlloc = true;
8✔
146

147
    attributeManager.add({
8✔
148
      indices: {
149
        size: 1,
150
        isIndexed: true,
151
        update: attribute => (attribute.value = this.state.mesh.indices),
7✔
152
        noAlloc
153
      },
154
      positions: {
155
        size: 3,
156
        type: 'float64',
157
        fp64: this.use64bitPositions(),
158
        update: attribute => (attribute.value = this.state.mesh.positions),
9✔
159
        noAlloc
160
      },
161
      texCoords: {
162
        size: 2,
163
        update: attribute => (attribute.value = this.state.mesh.texCoords),
7✔
164
        noAlloc
165
      }
166
    });
167
  }
168

169
  updateState({props, oldProps, changeFlags}: UpdateParameters<this>): void {
170
    // setup model first
171
    const attributeManager = this.getAttributeManager()!;
16✔
172

173
    if (changeFlags.extensionsChanged) {
16✔
174
      this.state.model?.destroy();
8✔
175
      this.state.model = this._getModel();
8✔
176
      attributeManager.invalidateAll();
8✔
177
    }
178

179
    if (props.bounds !== oldProps.bounds) {
16✔
180
      const oldMesh = this.state.mesh;
10✔
181
      const mesh = this._createMesh();
10✔
182
      this.state.model!.setVertexCount(mesh.vertexCount);
10✔
183
      for (const key in mesh) {
10✔
184
        if (oldMesh && oldMesh[key] !== mesh[key]) {
40✔
185
          attributeManager.invalidate(key);
2✔
186
        }
187
      }
188
      this.setState({mesh, ...this._getCoordinateUniforms()});
10✔
189
    } else if (props._imageCoordinateSystem !== oldProps._imageCoordinateSystem) {
6✔
190
      this.setState(this._getCoordinateUniforms());
4✔
191
    }
192
  }
193

194
  getPickingInfo(params: GetPickingInfoParams): BitmapLayerPickingInfo {
195
    const {image} = this.props;
2✔
196
    const info = params.info as BitmapLayerPickingInfo;
2✔
197

198
    if (!info.color || !image) {
2✔
199
      info.bitmap = null;
1✔
200
      return info;
1✔
201
    }
202

203
    const {width, height} = image as Texture;
1✔
204

205
    // Picking color doesn't represent object index in this layer
206
    info.index = 0;
1✔
207

208
    // Calculate uv and pixel in bitmap
209
    const uv = unpackUVsFromRGB(info.color);
1✔
210

211
    info.bitmap = {
1✔
212
      size: {width, height},
213
      uv,
214
      pixel: [Math.floor(uv[0] * width), Math.floor(uv[1] * height)]
215
    };
216

217
    return info;
1✔
218
  }
219

220
  // Override base Layer multi-depth picking logic
221
  disablePickingIndex() {
UNCOV
222
    this.setState({disablePicking: true});
×
223
  }
224

225
  restorePickingColors() {
UNCOV
226
    this.setState({disablePicking: false});
×
227
  }
228

229
  protected _updateAutoHighlight(info) {
230
    super._updateAutoHighlight({
2✔
231
      ...info,
232
      color: this.encodePickingColor(0)
233
    });
234
  }
235

236
  protected _createMesh() {
237
    const {bounds} = this.props;
10✔
238

239
    let normalizedBounds = bounds;
10✔
240
    // bounds as [minX, minY, maxX, maxY]
241
    if (isRectangularBounds(bounds)) {
10✔
242
      /*
243
        (minX0, maxY3) ---- (maxX2, maxY3)
244
               |                  |
245
               |                  |
246
               |                  |
247
        (minX0, minY1) ---- (maxX2, minY1)
248
     */
249
      normalizedBounds = [
8✔
250
        [bounds[0], bounds[1]],
251
        [bounds[0], bounds[3]],
252
        [bounds[2], bounds[3]],
253
        [bounds[2], bounds[1]]
254
      ];
255
    }
256

257
    return createMesh(normalizedBounds, this.context.viewport.resolution);
10✔
258
  }
259

260
  protected _getModel(): Model {
261
    /*
262
      0,0 --- 1,0
263
       |       |
264
      0,1 --- 1,1
265
    */
266
    const bufferLayout =
267
      this.context.device.type === 'webgpu'
8!
268
        ? this.getAttributeManager()!
269
            .getBufferLayouts({isInstanced: false})
270
            // WebGPU index buffers are bound separately from vertex buffer layouts.
UNCOV
271
            .filter(layout => layout.name !== 'indices')
×
272
        : this.getAttributeManager()!.getBufferLayouts();
273

274
    return new Model(this.context.device, {
8✔
275
      ...this.getShaders(),
276
      id: this.props.id,
277
      bufferLayout,
278
      topology: 'triangle-list',
279
      isInstanced: false
280
    });
281
  }
282

283
  draw(opts) {
284
    const {shaderModuleProps} = opts;
16✔
285
    const {model, coordinateConversion, bounds, disablePicking} = this.state;
16✔
286
    const {image, desaturate, transparentColor, tintColor} = this.props;
16✔
287

288
    if (shaderModuleProps.picking.isActive && disablePicking) {
16!
UNCOV
289
      return;
×
290
    }
291

292
    // // TODO fix zFighting
293
    // Render the image
294
    if (image && model) {
16✔
295
      const bitmapProps: BitmapProps = {
2✔
296
        bitmapTexture: image as Texture,
297
        bounds,
298
        coordinateConversion,
299
        desaturate,
300
        tintColor: tintColor.slice(0, 3).map(x => x / 255) as [number, number, number],
6✔
301
        transparentColor: transparentColor.map(x => x / 255) as [number, number, number, number]
8✔
302
      };
303
      model.shaderInputs.setProps({bitmap: bitmapProps});
2✔
304
      model.draw(this.context.renderPass);
2✔
305
    }
306
  }
307

308
  _getCoordinateUniforms() {
309
    let {_imageCoordinateSystem: imageCoordinateSystem} = this.props;
14✔
310
    if (imageCoordinateSystem !== 'default') {
14✔
311
      const {bounds} = this.props;
6✔
312
      if (!isRectangularBounds(bounds)) {
6✔
313
        throw new Error('_imageCoordinateSystem only supports rectangular bounds');
1✔
314
      }
315

316
      // The default behavior (linearly interpolated tex coords)
317
      const defaultImageCoordinateSystem = this.context.viewport.resolution
5✔
318
        ? 'lnglat'
319
        : 'cartesian';
320
      imageCoordinateSystem = imageCoordinateSystem === 'lnglat' ? 'lnglat' : 'cartesian';
6✔
321

322
      if (imageCoordinateSystem === 'lnglat' && defaultImageCoordinateSystem === 'cartesian') {
6✔
323
        // LNGLAT in Mercator, e.g. display LNGLAT-encoded image in WebMercator projection
324
        return {coordinateConversion: -1, bounds};
2✔
325
      }
326
      if (imageCoordinateSystem === 'cartesian' && defaultImageCoordinateSystem === 'lnglat') {
3✔
327
        // Mercator in LNGLAT, e.g. display WebMercator encoded image in Globe projection
328
        const bottomLeft = lngLatToWorld([bounds[0], bounds[1]]);
3✔
329
        const topRight = lngLatToWorld([bounds[2], bounds[3]]);
3✔
330
        return {
1✔
331
          coordinateConversion: 1,
332
          bounds: [bottomLeft[0], bottomLeft[1], topRight[0], topRight[1]]
333
        };
334
      }
335
    }
336
    return {
10✔
337
      coordinateConversion: 0,
338
      bounds: [0, 0, 0, 0]
339
    };
340
  }
341
}
342

343
/**
344
 * Decode uv floats from rgb bytes where b contains 4-bit fractions of uv
345
 * @param {number[]} color
346
 * @returns {number[]} uvs
347
 * https://stackoverflow.com/questions/30242013/glsl-compressing-packing-multiple-0-1-colours-var4-into-a-single-var4-variab
348
 */
349
function unpackUVsFromRGB(color: Uint8Array): [number, number] {
350
  const [u, v, fracUV] = color;
1✔
351
  const vFrac = (fracUV & 0xf0) / 256;
1✔
352
  const uFrac = (fracUV & 0x0f) / 16;
1✔
353
  return [(u + uFrac) / 256, (v + vFrac) / 256];
1✔
354
}
355

356
function isRectangularBounds(
357
  bounds: [number, number, number, number] | [Position, Position, Position, Position]
358
): bounds is [number, number, number, number] {
359
  return Number.isFinite(bounds[0]);
16✔
360
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc