• 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

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

5
import {Layer, project32, color, picking, UNIT} from '@deck.gl/core';
6
import {Parameters} from '@luma.gl/core';
7
import {Geometry} from '@luma.gl/engine';
8
import {Model} from '@luma.gl/engine';
9
import PathTesselator from './path-tesselator';
10

11
import {pathUniforms, PathProps} from './path-layer-uniforms';
12
import source from './path-layer.wgsl';
13
import vs from './path-layer-vertex.glsl';
14
import fs from './path-layer-fragment.glsl';
15

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

30
type _PathLayerProps<DataT> = {
31
  data: LayerDataSource<DataT>;
32
  /** The units of the line width, one of `'meters'`, `'common'`, and `'pixels'`
33
   * @default 'meters'
34
   */
35
  widthUnits?: Unit;
36
  /**
37
   * Path width multiplier.
38
   * @default 1
39
   */
40
  widthScale?: number;
41
  /**
42
   * The minimum path width in pixels. This prop can be used to prevent the path from getting too thin when zoomed out.
43
   * @default 0
44
   */
45
  widthMinPixels?: number;
46
  /**
47
   * The maximum path width in pixels. This prop can be used to prevent the path from getting too thick when zoomed in.
48
   * @default Number.MAX_SAFE_INTEGER
49
   */
50
  widthMaxPixels?: number;
51
  /**
52
   * Type of joint. If `true`, draw round joints. Otherwise draw miter joints.
53
   * @default false
54
   */
55
  jointRounded?: boolean;
56
  /**
57
   * Type of caps. If `true`, draw round caps. Otherwise draw square caps.
58
   * @default false
59
   */
60
  capRounded?: boolean;
61
  /**
62
   * The maximum extent of a joint in ratio to the stroke width. Only works if `jointRounded` is `false`.
63
   * @default 4
64
   */
65
  miterLimit?: number;
66
  /**
67
   * If `true`, extrude the path in screen space (width always faces the camera).
68
   * If `false`, the width always faces up (z).
69
   * @default false
70
   */
71
  billboard?: boolean;
72
  /**
73
   * (Experimental) If `'loop'` or `'open'`, will skip normalizing the coordinates returned by `getPath` and instead assume all paths are to be loops or open paths.
74
   * When normalization is disabled, paths must be specified in the format of flat array. Open paths must contain at least 2 vertices and closed paths must contain at least 3 vertices.
75
   * @default null
76
   */
77
  _pathType?: null | 'loop' | 'open';
78
  /**
79
   * Path geometry accessor.
80
   */
81
  getPath?: AccessorFunction<DataT, PathGeometry>;
82
  /**
83
   * Path color accessor.
84
   * @default [0, 0, 0, 255]
85
   */
86
  getColor?: Accessor<DataT, Color | Color[]>;
87
  /**
88
   * Path width accessor.
89
   * @default 1
90
   */
91
  getWidth?: Accessor<DataT, number | number[]>;
92
  /**
93
   * @deprecated Use `jointRounded` and `capRounded` instead
94
   */
95
  rounded?: boolean;
96
};
97

98
export type PathLayerProps<DataT = unknown> = _PathLayerProps<DataT> & LayerProps;
99

100
const DEFAULT_COLOR = [0, 0, 0, 255] as const;
5✔
101

102
const defaultProps: DefaultProps<PathLayerProps> = {
5✔
103
  widthUnits: 'meters',
104
  widthScale: {type: 'number', min: 0, value: 1},
105
  widthMinPixels: {type: 'number', min: 0, value: 0},
106
  widthMaxPixels: {type: 'number', min: 0, value: Number.MAX_SAFE_INTEGER},
107
  jointRounded: false,
108
  capRounded: false,
109
  miterLimit: {type: 'number', min: 0, value: 4},
110
  billboard: false,
111
  _pathType: null,
112

113
  getPath: {type: 'accessor', value: (object: any) => object.path},
8✔
114
  getColor: {type: 'accessor', value: DEFAULT_COLOR},
115
  getWidth: {type: 'accessor', value: 1},
116

117
  // deprecated props
118
  rounded: {deprecatedFor: ['jointRounded', 'capRounded']}
119
};
120

121
const ATTRIBUTE_TRANSITION = {
5✔
122
  enter: (value, chunk) => {
123
    return chunk.length ? chunk.subarray(chunk.length - value.length) : value;
×
124
  }
125
};
126

127
/** Render lists of coordinate points as extruded polylines with mitering. */
128
export default class PathLayer<DataT = any, ExtraPropsT extends {} = {}> extends Layer<
129
  ExtraPropsT & Required<_PathLayerProps<DataT>>
130
> {
131
  static defaultProps = defaultProps;
5✔
132
  static layerName = 'PathLayer';
5✔
133

134
  state!: {
135
    model?: Model;
136
    pathTesselator: PathTesselator;
137
  };
138

139
  getShaders() {
140
    return super.getShaders({vs, fs, source, modules: [project32, color, picking, pathUniforms]}); // 'project' module added by default.
155✔
141
  }
142

143
  get wrapLongitude(): boolean {
144
    return false;
537✔
145
  }
146

147
  getBounds(): [number[], number[]] | null {
NEW
148
    if (this.context.device.type === 'webgpu') {
×
NEW
149
      return null;
×
150
    }
151
    return this.getAttributeManager()?.getBounds(['vertexPositions']);
×
152
  }
153

154
  initializeState() {
155
    const noAlloc = true;
152✔
156
    const attributeManager = this.getAttributeManager();
152✔
157
    const enableTransitions = this.context.device.type !== 'webgpu';
152✔
158
    /* eslint-disable max-len */
159
    if (this.context.device.type === 'webgpu') {
152!
NEW
160
      attributeManager!.addInstanced({
×
161
        instancePositions: {
162
          size: 12,
163
          type: 'float32',
164
          // WebGPU keeps the fp64 path by uploading float32 high parts and reconstructing
165
          // with the matching `instancePositions64Low` residuals in WGSL.
166
          transition: false,
167
          accessor: 'getPath',
168
          // eslint-disable-next-line @typescript-eslint/unbound-method
169
          update: this.calculateInstancePositions,
170
          shaderAttributes: {
171
            instanceLeftPositions: {size: 3, elementOffset: 0},
172
            instanceStartPositions: {size: 3, elementOffset: 3},
173
            instanceEndPositions: {size: 3, elementOffset: 6},
174
            instanceRightPositions: {size: 3, elementOffset: 9}
175
          },
176
          noAlloc
177
        },
178
        instancePositions64Low: {
179
          size: 12,
180
          type: 'float32',
181
          // This is the low-part companion to `instancePositions`, not a plain-fp32 downgrade.
182
          transition: false,
183
          accessor: 'getPath',
184
          // eslint-disable-next-line @typescript-eslint/unbound-method
185
          update: this.calculateInstancePositions64Low,
186
          shaderAttributes: {
187
            instanceLeftPositions64Low: {size: 3, elementOffset: 0},
188
            instanceStartPositions64Low: {size: 3, elementOffset: 3},
189
            instanceEndPositions64Low: {size: 3, elementOffset: 6},
190
            instanceRightPositions64Low: {size: 3, elementOffset: 9}
191
          },
192
          noAlloc
193
        },
194
        instanceTypes: {
195
          size: 1,
196
          // eslint-disable-next-line @typescript-eslint/unbound-method
197
          update: this.calculateSegmentTypes,
198
          noAlloc
199
        },
200
        instanceStrokeWidths: {
201
          size: 1,
202
          accessor: 'getWidth',
203
          transition: false,
204
          defaultValue: 1
205
        },
206
        instanceColors: {
207
          size: this.props.colorFormat.length,
208
          type: 'unorm8',
209
          accessor: 'getColor',
210
          transition: false,
211
          defaultValue: DEFAULT_COLOR
212
        },
213
        /** Source path row for each generated segment/joint instance. */
214
        rowIndexes: {
215
          size: 1,
216
          type: 'uint32',
NEW
217
          accessor: (object, {index}) => (object && object.__source ? object.__source.index : index)
×
218
        }
219
      });
220
    } else {
221
      attributeManager!.addInstanced({
152✔
222
        vertexPositions: {
223
          size: 3,
224
          // Start filling buffer from 1 vertex in
225
          vertexOffset: 1,
226
          type: 'float64',
227
          fp64: this.use64bitPositions(),
228
          transition: enableTransitions ? ATTRIBUTE_TRANSITION : false,
152!
229
          accessor: 'getPath',
230
          // eslint-disable-next-line @typescript-eslint/unbound-method
231
          update: this.calculatePositions,
232
          noAlloc,
233
          shaderAttributes: {
234
            instanceLeftPositions: {
235
              vertexOffset: 0
236
            },
237
            instanceStartPositions: {
238
              vertexOffset: 1
239
            },
240
            instanceEndPositions: {
241
              vertexOffset: 2
242
            },
243
            instanceRightPositions: {
244
              vertexOffset: 3
245
            }
246
          }
247
        },
248
        instanceTypes: {
249
          size: 1,
250
          type: 'uint8',
251
          // eslint-disable-next-line @typescript-eslint/unbound-method
252
          update: this.calculateSegmentTypes,
253
          noAlloc
254
        },
255
        instanceStrokeWidths: {
256
          size: 1,
257
          accessor: 'getWidth',
258
          transition: enableTransitions ? ATTRIBUTE_TRANSITION : false,
152!
259
          defaultValue: 1
260
        },
261
        instanceColors: {
262
          size: this.props.colorFormat.length,
263
          type: 'unorm8',
264
          accessor: 'getColor',
265
          transition: enableTransitions ? ATTRIBUTE_TRANSITION : false,
152!
266
          defaultValue: DEFAULT_COLOR
267
        },
268
        /** Source path row for each generated segment/joint instance. */
269
        rowIndexes: {
270
          size: 1,
271
          type: 'uint32',
272
          accessor: (object, {index}) => (object && object.__source ? object.__source.index : index)
10,174✔
273
        }
274
      });
275
    }
276
    /* eslint-enable max-len */
277

278
    this.setState({
152✔
279
      pathTesselator: new PathTesselator({
280
        fp64: this.use64bitPositions(),
281
        webgpu: this.context.device.type === 'webgpu'
282
      })
283
    });
284
  }
285

286
  updateState(params: UpdateParameters<this>) {
287
    super.updateState(params);
296✔
288
    const {props, changeFlags} = params;
296✔
289

290
    const attributeManager = this.getAttributeManager();
296✔
291

292
    const geometryChanged =
293
      changeFlags.dataChanged ||
296✔
294
      (changeFlags.updateTriggersChanged &&
295
        (changeFlags.updateTriggersChanged.all || changeFlags.updateTriggersChanged.getPath));
296

297
    if (geometryChanged) {
296✔
298
      const {pathTesselator} = this.state;
203✔
299
      const buffers = (props.data as any).attributes || {};
203✔
300

301
      pathTesselator.updateGeometry({
203✔
302
        data: props.data,
303
        geometryBuffer: buffers.getPath,
304
        buffers,
305
        normalize: !props._pathType,
306
        loop: props._pathType === 'loop',
307
        getGeometry: props.getPath,
308
        positionFormat: props.positionFormat,
309
        wrapLongitude: props.wrapLongitude,
310
        // TODO - move the flag out of the viewport
311
        resolution: this.context.viewport.resolution,
312
        dataChanged: changeFlags.dataChanged
313
      });
314
      this.setState({
203✔
315
        numInstances: pathTesselator.instanceCount,
316
        startIndices: pathTesselator.vertexStarts
317
      });
318
      if (!changeFlags.dataChanged) {
203!
319
        // Base `layer.updateState` only invalidates all attributes on data change
320
        // Cover the rest of the scenarios here
321
        attributeManager!.invalidateAll();
×
322
      }
323
    }
324

325
    if (changeFlags.extensionsChanged) {
296✔
326
      this.state.model?.destroy();
155✔
327
      this.state.model = this._getModel();
155✔
328
      attributeManager!.invalidateAll();
155✔
329
    }
330
  }
331

332
  getPickingInfo(params: GetPickingInfoParams): PickingInfo {
333
    const info = super.getPickingInfo(params);
38✔
334
    const {index} = info;
38✔
335
    const data = this.props.data as any[];
38✔
336

337
    // Check if data comes from a composite layer, wrapped with getSubLayerRow
338
    if (data[0] && data[0].__source) {
38✔
339
      // index decoded from picking color refers to the source index
340
      info.object = data.find(d => d.__source.index === index);
333✔
341
    }
342
    return info;
38✔
343
  }
344

345
  /** Override base Layer method */
346
  disablePickingIndex(objectIndex: number) {
347
    const data = this.props.data as any[];
3✔
348

349
    // Check if data comes from a composite layer, wrapped with getSubLayerRow
350
    if (data[0] && data[0].__source) {
3!
351
      // index decoded from picking color refers to the source index
352
      for (let i = 0; i < data.length; i++) {
×
353
        if (data[i].__source.index === objectIndex) {
×
354
          this._disablePickingIndex(i);
×
355
        }
356
      }
357
    } else {
358
      super.disablePickingIndex(objectIndex);
3✔
359
    }
360
  }
361

362
  draw({uniforms}) {
363
    const {
364
      jointRounded,
365
      capRounded,
366
      billboard,
367
      miterLimit,
368
      widthUnits,
369
      widthScale,
370
      widthMinPixels,
371
      widthMaxPixels
372
    } = this.props;
545✔
373

374
    const model = this.state.model!;
545✔
375
    const pathProps: PathProps = {
545✔
376
      jointType: Number(jointRounded),
377
      capType: Number(capRounded),
378
      billboard,
379
      widthUnits: UNIT[widthUnits],
380
      widthScale,
381
      miterLimit,
382
      widthMinPixels,
383
      widthMaxPixels
384
    };
385
    model.shaderInputs.setProps({path: pathProps});
545✔
386
    model.draw(this.context.renderPass);
545✔
387
  }
388

389
  protected _getModel(): Model {
390
    const parameters =
391
      this.context.device.type === 'webgpu'
155!
392
        ? ({
393
            depthWriteEnabled: true,
394
            depthCompare: 'less-equal'
395
          } satisfies Parameters)
396
        : undefined;
397

398
    /*
399
     *       _
400
     *        "-_ 1                   3                       5
401
     *     _     "o---------------------o-------------------_-o
402
     *       -   / ""--..__              '.             _.-' /
403
     *   _     "@- - - - - ""--..__- - - - x - - - -_.@'    /
404
     *    "-_  /                   ""--..__ '.  _,-` :     /
405
     *       "o----------------------------""-o'    :     /
406
     *      0,2                            4 / '.  :     /
407
     *                                      /   '.:     /
408
     *                                     /     :'.   /
409
     *                                    /     :  ', /
410
     *                                   /     :     o
411
     */
412

413
    // prettier-ignore
414
    const SEGMENT_INDICES = [
155✔
415
      // start corner
416
      0, 1, 2,
417
      // body
418
      1, 4, 2,
419
      1, 3, 4,
420
      // end corner
421
      3, 5, 4
422
    ];
423

424
    // [0] position on segment - 0: start, 1: end
425
    // [1] side of path - -1: left, 0: center (joint), 1: right
426
    // prettier-ignore
427
    const SEGMENT_POSITIONS = [
155✔
428
      // bevel start corner
429
      0, 0,
430
      // start inner corner
431
      0, -1,
432
      // start outer corner
433
      0, 1,
434
      // end inner corner
435
      1, -1,
436
      // end outer corner
437
      1, 1,
438
      // bevel end corner
439
      1, 0
440
    ];
441

442
    return new Model(this.context.device, {
155✔
443
      ...this.getShaders(),
444
      id: this.props.id,
445
      bufferLayout: this.getAttributeManager()!.getBufferLayouts(),
446
      geometry: new Geometry({
447
        topology: 'triangle-list',
448
        attributes: {
449
          indices: new Uint16Array(SEGMENT_INDICES),
450
          positions: {value: new Float32Array(SEGMENT_POSITIONS), size: 2}
451
        }
452
      }),
453
      parameters,
454
      isInstanced: true
455
    });
456
  }
457

458
  protected calculatePositions(attribute) {
459
    const {pathTesselator} = this.state;
177✔
460

461
    attribute.startIndices = pathTesselator.vertexStarts;
177✔
462
    attribute.value = pathTesselator.get('positions');
177✔
463
  }
464

465
  protected calculateInstancePositions(attribute) {
NEW
466
    this._calculateInterleavedInstancePositions(attribute, false);
×
467
  }
468

469
  protected calculateInstancePositions64Low(attribute) {
NEW
470
    this._calculateInterleavedInstancePositions(attribute, true);
×
471
  }
472

473
  protected calculateSegmentTypes(attribute) {
474
    const {pathTesselator} = this.state;
203✔
475

476
    attribute.startIndices = pathTesselator.vertexStarts;
203✔
477
    attribute.value = pathTesselator.get('segmentTypes');
203✔
478
  }
479

480
  protected _calculateInterleavedInstancePositions(attribute, lowPart: boolean) {
NEW
481
    const {pathTesselator} = this.state;
×
NEW
482
    const value = pathTesselator.get('positions');
×
483

NEW
484
    if (!value) {
×
NEW
485
      attribute.value = null;
×
NEW
486
      return;
×
487
    }
488

NEW
489
    const numInstances = pathTesselator.instanceCount;
×
NEW
490
    const result = new Float32Array(numInstances * 12);
×
NEW
491
    const neighborOffsets = [-1, 0, 1, 2];
×
492

NEW
493
    for (let i = 0; i < numInstances; i++) {
×
NEW
494
      const targetIndex = i * 12;
×
NEW
495
      for (let vertexOffset = 0; vertexOffset < 4; vertexOffset++) {
×
NEW
496
        const sourceVertex = i + neighborOffsets[vertexOffset];
×
NEW
497
        const targetOffset = targetIndex + vertexOffset * 3;
×
NEW
498
        for (let j = 0; j < 3; j++) {
×
499
          const position =
NEW
500
            sourceVertex >= 0 && sourceVertex < numInstances ? value[sourceVertex * 3 + j] : 0;
×
NEW
501
          result[targetOffset + j] = lowPart ? position - Math.fround(position) : position;
×
502
        }
503
      }
504
    }
505

NEW
506
    attribute.startIndices = pathTesselator.vertexStarts;
×
NEW
507
    attribute.value = result;
×
508
  }
509
}
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