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

visgl / deck.gl / 23980348825

04 Apr 2026 01:58PM UTC coverage: 80.375% (+0.006%) from 80.369%
23980348825

Pull #10152

github

web-flow
Merge a194fb188 into 8dec4eee4
Pull Request #10152: feat(core): AttributeManager allocates Buffers for constant WebGPU at…

3117 of 3766 branches covered (82.77%)

Branch coverage included in aggregate %.

5 of 30 new or added lines in 1 file covered. (16.67%)

78 existing lines in 14 files now uncovered.

14289 of 17890 relevant lines covered (79.87%)

26621.23 hits per line

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

97.17
/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, project32, picking, COORDINATE_SYSTEM, 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

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

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

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

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

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

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

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

94
const DEFAULT_COLOR = [0, 0, 0, 255] as const;
4✔
95

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

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

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

111
  material: true
112
};
113

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

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

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

135
  getShaders(type) {
136
    return super.getShaders({
213✔
137
      vs: type === 'top' ? vsTop : vsSide,
138
      fs,
139
      defines: {
140
        RING_WINDING_ORDER_CW: !this.props._normalize && this.props._windingOrder === 'CCW' ? 0 : 1
141
      },
142
      modules: [project32, gouraudMaterial, picking, solidPolygonUniforms]
143
    });
144
  }
145

146
  get wrapLongitude(): boolean {
147
    return false;
425✔
148
  }
149

150
  getBounds(): [number[], number[]] | null {
151
    return this.getAttributeManager()?.getBounds(['vertexPositions']);
38✔
152
  }
153

154
  initializeState() {
155
    const {viewport} = this.context;
164✔
156
    let {coordinateSystem} = this.props;
164✔
157
    const {_full3d} = this.props;
164✔
158
    if (viewport.isGeospatial && coordinateSystem === COORDINATE_SYSTEM.DEFAULT) {
164✔
159
      coordinateSystem = COORDINATE_SYSTEM.LNGLAT;
110✔
160
    }
161

162
    let preproject: ((xy: number[]) => number[]) | undefined;
163

164
    if (coordinateSystem === COORDINATE_SYSTEM.LNGLAT) {
164✔
165
      if (_full3d) {
111✔
UNCOV
166
        preproject = viewport.projectPosition.bind(viewport);
×
167
      } else {
168
        preproject = viewport.projectFlat.bind(viewport);
111✔
169
      }
170
    }
171

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

183
    const attributeManager = this.getAttributeManager()!;
164✔
184
    const noAlloc = true;
164✔
185

186
    attributeManager.remove(['instancePickingColors']);
164✔
187

188
    /* eslint-disable max-len */
189
    attributeManager.add({
164✔
190
      indices: {
191
        size: 1,
192
        isIndexed: true,
193
        // eslint-disable-next-line @typescript-eslint/unbound-method
194
        update: this.calculateIndices,
195
        noAlloc
196
      },
197
      vertexPositions: {
198
        size: 3,
199
        type: 'float64',
200
        stepMode: 'dynamic',
201
        fp64: this.use64bitPositions(),
202
        transition: ATTRIBUTE_TRANSITION,
203
        accessor: 'getPolygon',
204
        // eslint-disable-next-line @typescript-eslint/unbound-method
205
        update: this.calculatePositions,
206
        noAlloc,
207
        shaderAttributes: {
208
          nextVertexPositions: {
209
            vertexOffset: 1
210
          }
211
        }
212
      },
213
      instanceVertexValid: {
214
        size: 1,
215
        type: 'uint16',
216
        stepMode: 'instance',
217
        // eslint-disable-next-line @typescript-eslint/unbound-method
218
        update: this.calculateVertexValid,
219
        noAlloc
220
      },
221
      elevations: {
222
        size: 1,
223
        stepMode: 'dynamic',
224
        transition: ATTRIBUTE_TRANSITION,
225
        accessor: 'getElevation'
226
      },
227
      fillColors: {
228
        size: this.props.colorFormat.length,
229
        type: 'unorm8',
230
        stepMode: 'dynamic',
231
        transition: ATTRIBUTE_TRANSITION,
232
        accessor: 'getFillColor',
233
        defaultValue: DEFAULT_COLOR
234
      },
235
      lineColors: {
236
        size: this.props.colorFormat.length,
237
        type: 'unorm8',
238
        stepMode: 'dynamic',
239
        transition: ATTRIBUTE_TRANSITION,
240
        accessor: 'getLineColor',
241
        defaultValue: DEFAULT_COLOR
242
      },
243
      pickingColors: {
244
        size: 4,
245
        type: 'uint8',
246
        stepMode: 'dynamic',
247
        accessor: (object, {index, target: value}) =>
248
          this.encodePickingColor(object && object.__source ? object.__source.index : index, value)
22,475✔
249
      }
250
    });
251
    /* eslint-enable max-len */
252
  }
253

254
  getPickingInfo(params: GetPickingInfoParams): PickingInfo {
255
    const info = super.getPickingInfo(params);
52✔
256
    const {index} = info;
52✔
257
    const data = this.props.data as any[];
52✔
258

259
    // Check if data comes from a composite layer, wrapped with getSubLayerRow
260
    if (data[0] && data[0].__source) {
52✔
261
      // index decoded from picking color refers to the source index
262
      info.object = data.find(d => d.__source.index === index);
20✔
263
    }
264
    return info;
52✔
265
  }
266

267
  disablePickingIndex(objectIndex: number) {
268
    const data = this.props.data as any[];
3✔
269

270
    // Check if data comes from a composite layer, wrapped with getSubLayerRow
271
    if (data[0] && data[0].__source) {
3✔
272
      // index decoded from picking color refers to the source index
273
      for (let i = 0; i < data.length; i++) {
1✔
274
        if (data[i].__source.index === objectIndex) {
5✔
275
          this._disablePickingIndex(i);
3✔
276
        }
277
      }
278
    } else {
279
      super.disablePickingIndex(objectIndex);
2✔
280
    }
281
  }
282

283
  draw({uniforms}) {
284
    const {extruded, filled, wireframe, elevationScale} = this.props;
435✔
285
    const {topModel, sideModel, wireframeModel, polygonTesselator} = this.state;
435✔
286

287
    const renderUniforms: SolidPolygonProps = {
435✔
288
      extruded: Boolean(extruded),
289
      elevationScale,
290
      isWireframe: false
291
    };
292

293
    // Note - the order is important
294
    if (wireframeModel && wireframe) {
435✔
295
      wireframeModel.setInstanceCount(polygonTesselator.instanceCount - 1);
6✔
296
      wireframeModel.shaderInputs.setProps({solidPolygon: {...renderUniforms, isWireframe: true}});
6✔
297
      wireframeModel.draw(this.context.renderPass);
6✔
298
    }
299

300
    if (sideModel && filled) {
435✔
301
      sideModel.setInstanceCount(polygonTesselator.instanceCount - 1);
27✔
302
      sideModel.shaderInputs.setProps({solidPolygon: renderUniforms});
27✔
303
      sideModel.draw(this.context.renderPass);
27✔
304
    }
305

306
    if (topModel && filled) {
435✔
307
      topModel.setVertexCount(polygonTesselator.vertexCount);
417✔
308
      topModel.shaderInputs.setProps({solidPolygon: renderUniforms});
417✔
309
      topModel.draw(this.context.renderPass);
417✔
310
    }
311
  }
312

313
  updateState(updateParams: UpdateParameters<this>) {
314
    super.updateState(updateParams);
283✔
315

316
    this.updateGeometry(updateParams);
283✔
317

318
    const {props, oldProps, changeFlags} = updateParams;
283✔
319
    const attributeManager = this.getAttributeManager();
283✔
320

321
    const regenerateModels =
322
      changeFlags.extensionsChanged ||
283✔
323
      props.filled !== oldProps.filled ||
324
      props.extruded !== oldProps.extruded;
325

326
    if (regenerateModels) {
283✔
327
      this.state.models?.forEach(model => model.destroy());
175✔
328

329
      this.setState(this._getModels());
175✔
330
      attributeManager!.invalidateAll();
175✔
331
    }
332
  }
333

334
  protected updateGeometry({props, oldProps, changeFlags}: UpdateParameters<this>) {
335
    const geometryConfigChanged =
336
      changeFlags.dataChanged ||
283✔
337
      (changeFlags.updateTriggersChanged &&
338
        (changeFlags.updateTriggersChanged.all || changeFlags.updateTriggersChanged.getPolygon));
339

340
    // When the geometry config  or the data is changed,
341
    // tessellator needs to be invoked
342
    if (geometryConfigChanged) {
283✔
343
      const {polygonTesselator} = this.state;
201✔
344
      const buffers = (props.data as any).attributes || {};
201✔
345
      polygonTesselator.updateGeometry({
201✔
346
        data: props.data,
347
        normalize: props._normalize,
348
        geometryBuffer: buffers.getPolygon,
349
        buffers,
350
        getGeometry: props.getPolygon,
351
        positionFormat: props.positionFormat,
352
        wrapLongitude: props.wrapLongitude,
353
        // TODO - move the flag out of the viewport
354
        resolution: this.context.viewport.resolution,
355
        fp64: this.use64bitPositions(),
356
        dataChanged: changeFlags.dataChanged,
357
        full3d: props._full3d
358
      });
359

360
      this.setState({
201✔
361
        numInstances: polygonTesselator.instanceCount,
362
        startIndices: polygonTesselator.vertexStarts
363
      });
364

365
      if (!changeFlags.dataChanged) {
201✔
366
        // Base `layer.updateState` only invalidates all attributes on data change
367
        // Cover the rest of the scenarios here
368
        this.getAttributeManager()!.invalidateAll();
4✔
369
      }
370
    }
371
  }
372

373
  protected _getModels() {
374
    const {id, filled, extruded} = this.props;
175✔
375

376
    let topModel;
377
    let sideModel;
378
    let wireframeModel;
379

380
    if (filled) {
175✔
381
      const shaders = this.getShaders('top');
165✔
382
      shaders.defines.NON_INSTANCED_MODEL = 1;
165✔
383
      const bufferLayout = this.getAttributeManager()!.getBufferLayouts({isInstanced: false});
165✔
384

385
      topModel = new Model(this.context.device, {
165✔
386
        ...shaders,
387
        id: `${id}-top`,
388
        topology: 'triangle-list',
389
        bufferLayout,
390
        isIndexed: true,
391
        userData: {
392
          excludeAttributes: {instanceVertexValid: true}
393
        }
394
      });
395
    }
396
    if (extruded) {
175✔
397
      const bufferLayout = this.getAttributeManager()!.getBufferLayouts({isInstanced: true});
24✔
398

399
      sideModel = new Model(this.context.device, {
24✔
400
        ...this.getShaders('side'),
401
        id: `${id}-side`,
402
        bufferLayout,
403
        geometry: new Geometry({
404
          topology: 'triangle-strip',
405
          attributes: {
406
            // top right - top left - bottom right - bottom left
407
            positions: {
408
              size: 2,
409
              value: new Float32Array([1, 0, 0, 0, 1, 1, 0, 1])
410
            }
411
          }
412
        }),
413
        isInstanced: true,
414
        userData: {
415
          excludeAttributes: {indices: true}
416
        }
417
      });
418

419
      wireframeModel = new Model(this.context.device, {
24✔
420
        ...this.getShaders('side'),
421
        id: `${id}-wireframe`,
422
        bufferLayout,
423
        geometry: new Geometry({
424
          topology: 'line-strip',
425
          attributes: {
426
            // top right - top left - bottom left - bottom right
427
            positions: {
428
              size: 2,
429
              value: new Float32Array([1, 0, 0, 0, 0, 1, 1, 1])
430
            }
431
          }
432
        }),
433
        isInstanced: true,
434
        userData: {
435
          excludeAttributes: {indices: true}
436
        }
437
      });
438
    }
439

440
    return {
175✔
441
      models: [sideModel, wireframeModel, topModel].filter(Boolean),
442
      topModel,
443
      sideModel,
444
      wireframeModel
445
    };
446
  }
447

448
  protected calculateIndices(attribute) {
449
    const {polygonTesselator} = this.state;
187✔
450
    attribute.startIndices = polygonTesselator.indexStarts;
187✔
451
    attribute.value = polygonTesselator.get('indices');
187✔
452
  }
453

454
  protected calculatePositions(attribute) {
455
    const {polygonTesselator} = this.state;
187✔
456
    attribute.startIndices = polygonTesselator.vertexStarts;
187✔
457
    attribute.value = polygonTesselator.get('positions');
187✔
458
  }
459

460
  protected calculateVertexValid(attribute) {
461
    attribute.value = this.state.polygonTesselator.get('vertexValid');
188✔
462
  }
463
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc