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

visgl / deck.gl / 29648040731

18 Jul 2026 02:26PM UTC coverage: 83.034%. First build
29648040731

Pull #10450

github

web-flow
Merge bb1e6794d into 95f6e3e7e
Pull Request #10450: feat(layers): extend WebGPU layer support

8144 of 10311 branches covered (78.98%)

Branch coverage included in aggregate %.

63 of 120 new or added lines in 23 files covered. (52.5%)

14525 of 16990 relevant lines covered (85.49%)

18926.57 hits per line

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

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

5
import {Layer, color, project32, picking, gouraudMaterial} from '@deck.gl/core';
6
import {Model, Geometry} from '@luma.gl/engine';
7
import type {BufferLayout} from '@luma.gl/core';
8

9
// Polygon geometry generation is managed by the polygon tesselator
10
import PolygonTesselator from './polygon-tesselator';
11

12
import {solidPolygonUniforms, SolidPolygonProps} from './solid-polygon-layer-uniforms';
13
import vsTop from './solid-polygon-layer-vertex-top.glsl';
14
import vsSide from './solid-polygon-layer-vertex-side.glsl';
15
import fs from './solid-polygon-layer-fragment.glsl';
16
import {getSolidPolygonShaderWGSL} from './solid-polygon-layer.wgsl';
17

18
import type {
19
  LayerProps,
20
  LayerDataSource,
21
  Color,
22
  Material,
23
  Accessor,
24
  AccessorFunction,
25
  UpdateParameters,
26
  GetPickingInfoParams,
27
  PickingInfo,
28
  DefaultProps
29
} from '@deck.gl/core';
30
import type {PolygonGeometry} from './polygon';
31

32
type _SolidPolygonLayerProps<DataT> = {
33
  data: LayerDataSource<DataT>;
34
  /** Whether to fill the polygons
35
   * @default true
36
   */
37
  filled?: boolean;
38
  /** Whether to extrude the polygons
39
   * @default false
40
   */
41
  extruded?: boolean;
42
  /** Whether to generate a line wireframe of the polygon.
43
   * @default false
44
   */
45
  wireframe?: boolean;
46
  /**
47
   * (Experimental) If `false`, will skip normalizing the coordinates returned by `getPolygon`.
48
   * @default true
49
   */
50
  _normalize?: boolean;
51
  /**
52
   * (Experimental) This prop is only effective with `_normalize: false`.
53
   * It specifies the winding order of rings in the polygon data, one of 'CW' (clockwise) and 'CCW' (counter-clockwise)
54
   */
55
  _windingOrder?: 'CW' | 'CCW';
56

57
  /**
58
   * (Experimental) This prop is only effective with `XYZ` data.
59
   * When true, polygon tesselation will be performed on the plane with the largest area, instead of the xy plane.
60
   * @default false
61
   */
62
  _full3d?: boolean;
63

64
  /** Elevation multiplier.
65
   * @default 1
66
   */
67
  elevationScale?: number;
68

69
  /** Polygon geometry accessor. */
70
  getPolygon?: AccessorFunction<DataT, PolygonGeometry>;
71
  /** Extrusion height accessor.
72
   * @default 1000
73
   */
74
  getElevation?: Accessor<DataT, number>;
75
  /** Fill color accessor.
76
   * @default [0, 0, 0, 255]
77
   */
78
  getFillColor?: Accessor<DataT, Color>;
79
  /** Stroke color accessor.
80
   * @default [0, 0, 0, 255]
81
   */
82
  getLineColor?: Accessor<DataT, Color>;
83

84
  /**
85
   * Material settings for lighting effect. Applies if `extruded: true`
86
   *
87
   * @default true
88
   * @see https://deck.gl/docs/developer-guide/using-lighting
89
   */
90
  material?: Material;
91
};
92

93
/** Render filled and/or extruded polygons. */
94
export type SolidPolygonLayerProps<DataT = unknown> = _SolidPolygonLayerProps<DataT> & LayerProps;
95

96
const DEFAULT_COLOR = [0, 0, 0, 255] as const;
5✔
97

98
const defaultProps: DefaultProps<SolidPolygonLayerProps> = {
5✔
99
  filled: true,
100
  extruded: false,
101
  wireframe: false,
102
  _normalize: true,
103
  _windingOrder: 'CW',
104
  _full3d: false,
105

106
  elevationScale: {type: 'number', min: 0, value: 1},
107

108
  getPolygon: {type: 'accessor', value: (f: any) => f.polygon},
6✔
109
  getElevation: {type: 'accessor', value: 1000},
110
  getFillColor: {type: 'accessor', value: DEFAULT_COLOR},
111
  getLineColor: {type: 'accessor', value: DEFAULT_COLOR},
112

113
  material: true
114
};
115

116
const ATTRIBUTE_TRANSITION = {
5✔
117
  enter: (value, chunk) => {
118
    return chunk.length ? chunk.subarray(chunk.length - value.length) : value;
×
119
  }
120
};
121

122
export default class SolidPolygonLayer<DataT = any, ExtraPropsT extends {} = {}> extends Layer<
123
  ExtraPropsT & Required<_SolidPolygonLayerProps<DataT>>
124
> {
125
  static defaultProps = defaultProps;
5✔
126
  static layerName = 'SolidPolygonLayer';
5✔
127

128
  state!: {
129
    topModel?: Model;
130
    sideModel?: Model;
131
    wireframeModel?: Model;
132
    models?: Model[];
133
    numInstances: number;
134
    polygonTesselator: PolygonTesselator;
135
  };
136

137
  getShaders(type) {
138
    const ringWindingOrderCW = !this.props._normalize && this.props._windingOrder === 'CCW' ? 0 : 1;
213✔
139
    const isWebGPU = this.context.device.type === 'webgpu';
213✔
140

141
    return super.getShaders({
213✔
142
      vs: type === 'top' ? vsTop : vsSide,
213✔
143
      fs,
144
      source: isWebGPU ? getSolidPolygonShaderWGSL(type, Boolean(ringWindingOrderCW)) : undefined,
426!
145
      ...(isWebGPU
213!
146
        ? {}
147
        : {
148
            defines: {
149
              RING_WINDING_ORDER_CW: ringWindingOrderCW
150
            }
151
          }),
152
      modules: [project32, color, gouraudMaterial, picking, solidPolygonUniforms]
153
    });
154
  }
155

156
  get wrapLongitude(): boolean {
157
    return false;
394✔
158
  }
159

160
  getBounds(): [number[], number[]] | null {
161
    return this.getAttributeManager()?.getBounds(['vertexPositions']);
38✔
162
  }
163

164
  initializeState() {
165
    const {viewport} = this.context;
164✔
166
    let {coordinateSystem} = this.props;
164✔
167
    const {_full3d} = this.props;
164✔
168
    if (viewport.isGeospatial && coordinateSystem === 'default') {
164✔
169
      coordinateSystem = 'lnglat';
110✔
170
    }
171

172
    let preproject: ((xy: number[]) => number[]) | undefined;
173

174
    if (coordinateSystem === 'lnglat') {
164✔
175
      if (_full3d) {
111!
176
        preproject = viewport.projectPosition.bind(viewport);
×
177
      } else {
178
        preproject = viewport.projectFlat.bind(viewport);
111✔
179
      }
180
    }
181

182
    this.setState({
164✔
183
      numInstances: 0,
184
      polygonTesselator: new PolygonTesselator({
185
        // Lnglat coordinates are usually projected non-linearly, which affects tesselation results
186
        // Provide a preproject function if the coordinates are in lnglat
187
        preproject,
188
        fp64: this.use64bitPositions(),
189
        IndexType: Uint32Array
190
      })
191
    });
192

193
    const attributeManager = this.getAttributeManager()!;
164✔
194
    const noAlloc = true;
164✔
195
    const isWebGPU = this.context.device.type === 'webgpu';
164✔
196

197
    /* eslint-disable max-len */
198
    attributeManager.add({
164✔
199
      indices: {
200
        size: 1,
201
        isIndexed: true,
202
        // eslint-disable-next-line @typescript-eslint/unbound-method
203
        update: this.calculateIndices,
204
        noAlloc
205
      },
206
      vertexPositions: {
207
        size: 3,
208
        type: 'float64',
209
        stepMode: 'dynamic',
210
        fp64: this.use64bitPositions(),
211
        transition: ATTRIBUTE_TRANSITION,
212
        accessor: 'getPolygon',
213
        // eslint-disable-next-line @typescript-eslint/unbound-method
214
        update: this.calculatePositions,
215
        noAlloc,
216
        ...(isWebGPU
164!
217
          ? {}
218
          : {
219
              shaderAttributes: {
220
                nextVertexPositions: {
221
                  vertexOffset: 1
222
                }
223
              }
224
            })
225
      },
226
      ...(isWebGPU
164!
227
        ? {
228
            nextVertexPositions: {
229
              size: 3,
230
              type: 'float64',
231
              stepMode: 'dynamic',
232
              fp64: this.use64bitPositions(),
233
              transition: ATTRIBUTE_TRANSITION,
234
              accessor: 'getPolygon',
235
              // eslint-disable-next-line @typescript-eslint/unbound-method
236
              update: this.calculateNextPositions,
237
              noAlloc
238
            }
239
          }
240
        : {}),
241
      instanceVertexValid: {
242
        size: 1,
243
        ...(isWebGPU ? {} : {type: 'uint16'}),
164!
244
        stepMode: 'instance',
245
        // eslint-disable-next-line @typescript-eslint/unbound-method
246
        update: this.calculateVertexValid,
247
        noAlloc
248
      },
249
      elevations: {
250
        size: 1,
251
        stepMode: 'dynamic',
252
        transition: ATTRIBUTE_TRANSITION,
253
        accessor: 'getElevation'
254
      },
255
      fillColors: {
256
        size: this.props.colorFormat.length,
257
        type: 'unorm8',
258
        stepMode: 'dynamic',
259
        transition: ATTRIBUTE_TRANSITION,
260
        accessor: 'getFillColor',
261
        defaultValue: DEFAULT_COLOR
262
      },
263
      lineColors: {
264
        size: this.props.colorFormat.length,
265
        type: 'unorm8',
266
        stepMode: 'dynamic',
267
        transition: ATTRIBUTE_TRANSITION,
268
        accessor: 'getLineColor',
269
        defaultValue: DEFAULT_COLOR
270
      },
271
      /** Source polygon row, including __source.index for composite data. */
272
      rowIndexes: {
273
        size: 1,
274
        type: 'uint32',
275
        stepMode: 'dynamic',
276
        accessor: (object, {index}) => (object && object.__source ? object.__source.index : index)
22,475✔
277
      }
278
    });
279
    /* eslint-enable max-len */
280
  }
281

282
  getPickingInfo(params: GetPickingInfoParams): PickingInfo {
283
    const info = super.getPickingInfo(params);
52✔
284
    const {index} = info;
52✔
285
    const data = this.props.data as any[];
52✔
286

287
    // Check if data comes from a composite layer, wrapped with getSubLayerRow
288
    if (data[0] && data[0].__source) {
52✔
289
      // index decoded from picking color refers to the source index
290
      info.object = data.find(d => d.__source.index === index);
20✔
291
    }
292
    return info;
52✔
293
  }
294

295
  disablePickingIndex(objectIndex: number) {
296
    const data = this.props.data as any[];
3✔
297

298
    // Check if data comes from a composite layer, wrapped with getSubLayerRow
299
    if (data[0] && data[0].__source) {
3✔
300
      // index decoded from picking color refers to the source index
301
      for (let i = 0; i < data.length; i++) {
1✔
302
        if (data[i].__source.index === objectIndex) {
5✔
303
          this._disablePickingIndex(i);
3✔
304
        }
305
      }
306
    } else {
307
      super.disablePickingIndex(objectIndex);
2✔
308
    }
309
  }
310

311
  draw({uniforms}) {
312
    const {extruded, filled, wireframe, elevationScale} = this.props;
404✔
313
    const {topModel, sideModel, wireframeModel, polygonTesselator} = this.state;
404✔
314

315
    const renderUniforms: SolidPolygonProps = {
404✔
316
      extruded: Boolean(extruded),
317
      elevationScale,
318
      isWireframe: false
319
    };
320

321
    // Note - the order is important
322
    if (wireframeModel && wireframe) {
404✔
323
      wireframeModel.setInstanceCount(polygonTesselator.instanceCount - 1);
6✔
324
      wireframeModel.shaderInputs.setProps({solidPolygon: {...renderUniforms, isWireframe: true}});
6✔
325
      wireframeModel.draw(this.context.renderPass);
6✔
326
    }
327

328
    if (sideModel && filled) {
404✔
329
      sideModel.setInstanceCount(polygonTesselator.instanceCount - 1);
27✔
330
      sideModel.shaderInputs.setProps({solidPolygon: renderUniforms});
27✔
331
      sideModel.draw(this.context.renderPass);
27✔
332
    }
333

334
    if (topModel && filled) {
404✔
335
      topModel.setVertexCount(polygonTesselator.vertexCount);
386✔
336
      topModel.shaderInputs.setProps({solidPolygon: renderUniforms});
386✔
337
      topModel.draw(this.context.renderPass);
386✔
338
    }
339
  }
340

341
  updateState(updateParams: UpdateParameters<this>) {
342
    super.updateState(updateParams);
282✔
343

344
    this.updateGeometry(updateParams);
282✔
345

346
    const {props, oldProps, changeFlags} = updateParams;
282✔
347
    const attributeManager = this.getAttributeManager();
282✔
348

349
    const regenerateModels =
350
      changeFlags.extensionsChanged ||
282✔
351
      props.filled !== oldProps.filled ||
352
      props.extruded !== oldProps.extruded;
353

354
    if (regenerateModels) {
282✔
355
      this.state.models?.forEach(model => model.destroy());
175✔
356

357
      this.setState(this._getModels());
175✔
358
      attributeManager!.invalidateAll();
175✔
359
    }
360
  }
361

362
  protected updateGeometry({props, oldProps, changeFlags}: UpdateParameters<this>) {
363
    const geometryConfigChanged =
364
      changeFlags.dataChanged ||
282✔
365
      (changeFlags.updateTriggersChanged &&
366
        (changeFlags.updateTriggersChanged.all || changeFlags.updateTriggersChanged.getPolygon));
367

368
    // When the geometry config  or the data is changed,
369
    // tessellator needs to be invoked
370
    if (geometryConfigChanged) {
282✔
371
      const {polygonTesselator} = this.state;
201✔
372
      const buffers = (props.data as any).attributes || {};
201✔
373
      polygonTesselator.updateGeometry({
201✔
374
        data: props.data,
375
        normalize: props._normalize,
376
        geometryBuffer: buffers.getPolygon,
377
        buffers,
378
        getGeometry: props.getPolygon,
379
        positionFormat: props.positionFormat,
380
        wrapLongitude: props.wrapLongitude,
381
        // TODO - move the flag out of the viewport
382
        resolution: this.context.viewport.resolution,
383
        fp64: this.use64bitPositions(),
384
        dataChanged: changeFlags.dataChanged,
385
        full3d: props._full3d
386
      });
387

388
      this.setState({
201✔
389
        numInstances: polygonTesselator.instanceCount,
390
        startIndices: polygonTesselator.vertexStarts
391
      });
392

393
      if (!changeFlags.dataChanged) {
201✔
394
        // Base `layer.updateState` only invalidates all attributes on data change
395
        // Cover the rest of the scenarios here
396
        this.getAttributeManager()!.invalidateAll();
4✔
397
      }
398
    }
399
  }
400

401
  protected _getModels() {
402
    const {id, filled, extruded} = this.props;
175✔
403

404
    let topModel;
405
    let sideModel;
406
    let wireframeModel;
407

408
    if (filled) {
175✔
409
      const shaders = this.getShaders('top');
165✔
410
      shaders.defines = {...shaders.defines, NON_INSTANCED_MODEL: 1};
165✔
411
      let bufferLayout = this.getAttributeManager()!.getBufferLayouts({isInstanced: false});
165✔
412
      if (this.context.device.type === 'webgpu') {
165!
NEW
413
        bufferLayout = filterBufferLayout(
×
414
          bufferLayout,
415
          new Set([
416
            'vertexPositions',
417
            'vertexPositions64Low',
418
            'elevations',
419
            'fillColors',
420
            'lineColors',
421
            'rowIndexes'
422
          ])
423
        );
424
      }
425

426
      topModel = new Model(this.context.device, {
165✔
427
        ...shaders,
428
        id: `${id}-top`,
429
        topology: 'triangle-list',
430
        bufferLayout,
431
        isIndexed: true,
432
        userData: {
433
          excludeAttributes: {instanceVertexValid: true}
434
        }
435
      });
436
    }
437
    if (extruded) {
175✔
438
      let bufferLayout = this.getAttributeManager()!.getBufferLayouts({isInstanced: true});
24✔
439
      if (this.context.device.type === 'webgpu') {
24!
NEW
440
        bufferLayout = filterBufferLayout(
×
441
          bufferLayout,
442
          new Set([
443
            'vertexPositions',
444
            'vertexPositions64Low',
445
            'nextVertexPositions',
446
            'nextVertexPositions64Low',
447
            'instanceVertexValid',
448
            'elevations',
449
            'fillColors',
450
            'lineColors',
451
            'rowIndexes'
452
          ])
453
        );
454
      }
455

456
      sideModel = new Model(this.context.device, {
24✔
457
        ...this.getShaders('side'),
458
        id: `${id}-side`,
459
        bufferLayout,
460
        geometry: new Geometry({
461
          topology: 'triangle-strip',
462
          attributes: {
463
            // top right - top left - bottom right - bottom left
464
            positions: {
465
              size: 2,
466
              value: new Float32Array([1, 0, 0, 0, 1, 1, 0, 1])
467
            }
468
          }
469
        }),
470
        isInstanced: true,
471
        userData: {
472
          excludeAttributes: {indices: true}
473
        }
474
      });
475

476
      wireframeModel = new Model(this.context.device, {
24✔
477
        ...this.getShaders('side'),
478
        id: `${id}-wireframe`,
479
        bufferLayout,
480
        geometry: new Geometry({
481
          topology: 'line-strip',
482
          attributes: {
483
            // top right - top left - bottom left - bottom right
484
            positions: {
485
              size: 2,
486
              value: new Float32Array([1, 0, 0, 0, 0, 1, 1, 1])
487
            }
488
          }
489
        }),
490
        isInstanced: true,
491
        userData: {
492
          excludeAttributes: {indices: true}
493
        }
494
      });
495
    }
496

497
    return {
175✔
498
      models: [sideModel, wireframeModel, topModel].filter(Boolean),
499
      topModel,
500
      sideModel,
501
      wireframeModel
502
    };
503
  }
504

505
  protected calculateIndices(attribute) {
506
    const {polygonTesselator} = this.state;
187✔
507
    attribute.startIndices = polygonTesselator.indexStarts;
187✔
508
    attribute.value = polygonTesselator.get('indices');
187✔
509
  }
510

511
  protected calculatePositions(attribute) {
512
    const {polygonTesselator} = this.state;
187✔
513
    attribute.startIndices = polygonTesselator.vertexStarts;
187✔
514
    attribute.value = polygonTesselator.get('positions');
187✔
515
  }
516

517
  protected calculateVertexValid(attribute) {
518
    const vertexValid = this.state.polygonTesselator.get('vertexValid');
188✔
519
    attribute.value =
188✔
520
      this.context.device.type === 'webgpu' && vertexValid
376!
521
        ? Float32Array.from(vertexValid)
522
        : vertexValid;
523
  }
524

525
  protected calculateNextPositions(attribute) {
NEW
526
    const {polygonTesselator} = this.state;
×
NEW
527
    const positions = polygonTesselator.get('positions');
×
NEW
528
    attribute.startIndices = polygonTesselator.vertexStarts;
×
529

NEW
530
    if (!positions) {
×
NEW
531
      attribute.value = positions;
×
NEW
532
      return;
×
533
    }
534

NEW
535
    const ArrayType = positions.constructor as typeof Float32Array;
×
NEW
536
    const nextPositions = new ArrayType(positions.length);
×
537

NEW
538
    for (let i = 0; i < positions.length; i += 3) {
×
NEW
539
      const nextIndex = i + 3 < positions.length ? i + 3 : i;
×
NEW
540
      nextPositions[i] = positions[nextIndex];
×
NEW
541
      nextPositions[i + 1] = positions[nextIndex + 1];
×
NEW
542
      nextPositions[i + 2] = positions[nextIndex + 2];
×
543
    }
544

NEW
545
    attribute.value = nextPositions;
×
546
  }
547
}
548

549
function filterBufferLayout(
550
  bufferLayout: BufferLayout[],
551
  allowedAttributes: Set<string>
552
): BufferLayout[] {
NEW
553
  const filteredLayouts: BufferLayout[] = [];
×
554

NEW
555
  for (const layout of bufferLayout) {
×
NEW
556
    if (layout.attributes) {
×
NEW
557
      const attributes = layout.attributes.filter(attribute =>
×
NEW
558
        allowedAttributes.has(attribute.attribute)
×
559
      );
NEW
560
      if (attributes.length) {
×
NEW
561
        filteredLayouts.push({...layout, attributes});
×
562
      }
NEW
563
    } else if (allowedAttributes.has(layout.name)) {
×
NEW
564
      filteredLayouts.push(layout);
×
565
    }
566
  }
567

NEW
568
  return filteredLayouts;
×
569
}
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