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

visgl / deck.gl / 30157453033

25 Jul 2026 12:08PM UTC coverage: 83.181% (-0.2%) from 83.386%
30157453033

Pull #10450

github

web-flow
Merge ee50fb74b into f1abf6d19
Pull Request #10450: feat(layers): extend WebGPU layer support

8186 of 10333 branches covered (79.22%)

Branch coverage included in aggregate %.

36 of 78 new or added lines in 11 files covered. (46.15%)

1 existing line in 1 file now uncovered.

14545 of 16994 relevant lines covered (85.59%)

18901.66 hits per line

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

79.79
/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

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

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

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

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

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

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

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

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

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

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

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

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

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

112
  material: true
113
};
114

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

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

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

136
  getShaders(type) {
137
    const ringWindingOrderCW = !this.props._normalize && this.props._windingOrder === 'CCW' ? 0 : 1;
213✔
138

139
    return super.getShaders({
213✔
140
      vs: type === 'top' ? vsTop : vsSide,
213✔
141
      fs,
142
      source: getSolidPolygonShaderWGSL(type, Boolean(ringWindingOrderCW)),
143
      defines: {
144
        RING_WINDING_ORDER_CW: ringWindingOrderCW
145
      },
146
      modules: [project32, color, gouraudMaterial, picking, solidPolygonUniforms]
147
    });
148
  }
149

150
  get wrapLongitude(): boolean {
151
    return false;
392✔
152
  }
153

154
  getBounds(): [number[], number[]] | null {
155
    return this.getAttributeManager()?.getBounds(['vertexPositions']);
38✔
156
  }
157

158
  initializeState() {
159
    const {viewport} = this.context;
164✔
160
    let {coordinateSystem} = this.props;
164✔
161
    const {_full3d} = this.props;
164✔
162
    if (viewport.isGeospatial && coordinateSystem === 'default') {
164✔
163
      coordinateSystem = 'lnglat';
110✔
164
    }
165

166
    let preproject: ((xy: number[]) => number[]) | undefined;
167

168
    if (coordinateSystem === 'lnglat') {
164✔
169
      if (_full3d) {
111!
170
        preproject = viewport.projectPosition.bind(viewport);
×
171
      } else {
172
        preproject = viewport.projectFlat.bind(viewport);
111✔
173
      }
174
    }
175

176
    this.setState({
164✔
177
      numInstances: 0,
178
      polygonTesselator: new PolygonTesselator({
179
        // Lnglat coordinates are usually projected non-linearly, which affects tesselation results
180
        // Provide a preproject function if the coordinates are in lnglat
181
        preproject,
182
        fp64: this.use64bitPositions(),
183
        IndexType: Uint32Array
184
      })
185
    });
186

187
    const attributeManager = this.getAttributeManager()!;
164✔
188
    const noAlloc = true;
164✔
189
    const isWebGPU = this.context.device.type === 'webgpu';
164✔
190

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

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

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

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

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

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

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

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

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

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

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

343
    this.updateGeometry(updateParams);
282✔
344

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

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

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

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

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

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

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

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

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

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

407
    if (filled) {
175✔
408
      const shaders = this.getShaders('top');
165✔
409
      shaders.defines = {...shaders.defines, NON_INSTANCED_MODEL: 1};
165✔
410
      let bufferLayout = this.getAttributeManager()!.getBufferLayouts({isInstanced: false});
165✔
411
      if (this.context.device.type === 'webgpu') {
165!
412
        // Indices are bound separately, and the top model does not bind side-only attributes.
NEW
413
        bufferLayout = bufferLayout.filter(
×
414
          layout =>
NEW
415
            layout.name !== 'indices' &&
×
416
            layout.name !== 'instanceVertexValid' &&
417
            layout.name !== 'nextVertexPositions'
418
        );
419
      }
420

421
      topModel = new Model(this.context.device, {
165✔
422
        ...shaders,
423
        id: `${id}-top`,
424
        topology: 'triangle-list',
425
        bufferLayout,
426
        isIndexed: true,
427
        userData: {
428
          excludeAttributes: {instanceVertexValid: true, nextVertexPositions: true}
429
        }
430
      });
431
    }
432
    if (extruded) {
175✔
433
      let bufferLayout = this.getAttributeManager()!.getBufferLayouts({isInstanced: true});
24✔
434
      if (this.context.device.type === 'webgpu') {
24!
435
        // Indices are owned by the top model; WebGPU vertex layouts cannot include index buffers.
NEW
436
        bufferLayout = bufferLayout.filter(layout => layout.name !== 'indices');
×
437
      }
438

439
      sideModel = new Model(this.context.device, {
24✔
440
        ...this.getShaders('side'),
441
        id: `${id}-side`,
442
        bufferLayout,
443
        geometry: new Geometry({
444
          topology: 'triangle-strip',
445
          attributes: {
446
            // top right - top left - bottom right - bottom left
447
            positions: {
448
              size: 2,
449
              value: new Float32Array([1, 0, 0, 0, 1, 1, 0, 1])
450
            }
451
          }
452
        }),
453
        isInstanced: true,
454
        userData: {
455
          excludeAttributes: {indices: true}
456
        }
457
      });
458

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

480
    return {
175✔
481
      models: [sideModel, wireframeModel, topModel].filter(Boolean),
482
      topModel,
483
      sideModel,
484
      wireframeModel
485
    };
486
  }
487

488
  protected calculateIndices(attribute) {
489
    const {polygonTesselator} = this.state;
187✔
490
    attribute.startIndices = polygonTesselator.indexStarts;
187✔
491
    attribute.value = polygonTesselator.get('indices');
187✔
492
  }
493

494
  protected calculatePositions(attribute) {
495
    const {polygonTesselator} = this.state;
187✔
496
    attribute.startIndices = polygonTesselator.vertexStarts;
187✔
497
    attribute.value = polygonTesselator.get('positions');
187✔
498
  }
499

500
  protected calculateVertexValid(attribute) {
501
    const vertexValid = this.state.polygonTesselator.get('vertexValid');
188✔
502
    attribute.value =
188✔
503
      this.context.device.type === 'webgpu' && vertexValid
376!
504
        ? Float32Array.from(vertexValid)
505
        : vertexValid;
506
  }
507

508
  protected calculateNextPositions(attribute) {
NEW
509
    const {polygonTesselator} = this.state;
×
NEW
510
    const positions = polygonTesselator.get('positions');
×
NEW
511
    const vertexValid = polygonTesselator.get('vertexValid');
×
NEW
512
    attribute.startIndices = polygonTesselator.vertexStarts;
×
513

NEW
514
    if (!positions) {
×
NEW
515
      attribute.value = positions;
×
NEW
516
      return;
×
517
    }
518

NEW
519
    const vertexCount = positions.length / 3;
×
NEW
520
    const nextPositions = new (positions.constructor as typeof Float32Array)(positions.length);
×
NEW
521
    for (let vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) {
×
NEW
522
      const sourceIndex = vertexIndex * 3;
×
523
      const nextSourceIndex =
NEW
524
        vertexValid?.[vertexIndex] && vertexIndex + 1 < vertexCount ? sourceIndex + 3 : sourceIndex;
×
NEW
525
      for (let componentIndex = 0; componentIndex < 3; componentIndex++) {
×
NEW
526
        nextPositions[sourceIndex + componentIndex] = positions[nextSourceIndex + componentIndex];
×
527
      }
528
    }
529

NEW
530
    attribute.value = nextPositions;
×
531
  }
532
}
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