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

visgl / deck.gl / 23407443036

22 Mar 2026 04:32PM UTC coverage: 90.972% (-0.09%) from 91.061%
23407443036

Pull #10113

github

web-flow
Merge 1fffe3259 into e61bc7d3d
Pull Request #10113: feat(layers) Port PathLayer to WebGPU

7080 of 7837 branches covered (90.34%)

Branch coverage included in aggregate %.

369 of 466 new or added lines in 4 files covered. (79.18%)

147 existing lines in 9 files now uncovered.

58788 of 64568 relevant lines covered (91.05%)

13914.49 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

130✔
280
    this.setState({
130✔
281
      pathTesselator: new PathTesselator({
130✔
282
        fp64: this.use64bitPositions()
130✔
283
      })
130✔
284
    });
130✔
285
  }
130✔
286

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

271✔
291
    const attributeManager = this.getAttributeManager();
271✔
292

271✔
293
    const geometryChanged =
271✔
294
      changeFlags.dataChanged ||
271✔
295
      (changeFlags.updateTriggersChanged &&
85✔
296
        (changeFlags.updateTriggersChanged.all || changeFlags.updateTriggersChanged.getPath));
18✔
297

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

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

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

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

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

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

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

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

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

1✔
390
  protected _getModel(): Model {
1✔
391
    const parameters =
133✔
392
      this.context.device.type === 'webgpu'
133!
NEW
393
        ? ({
×
NEW
394
            depthWriteEnabled: true,
×
NEW
395
            depthCompare: 'less-equal'
×
NEW
396
          } satisfies Parameters)
×
397
        : undefined;
133✔
398
    const bufferLayout =
133✔
399
      this.context.device.type === 'webgpu'
133!
NEW
400
        ? this.getAttributeManager()!.getBufferLayouts()
×
401
        : this.getAttributeManager()!
133✔
402
            .getBufferLayouts()
133✔
403
            .map(layout =>
133✔
404
              layout.name === 'vertexPositions'
676✔
405
                ? {
133✔
406
                    ...layout,
133✔
407
                    attributes: (layout.attributes || []).filter(
133!
408
                      attribute =>
133✔
409
                        attribute.attribute !== 'vertexPositions' &&
1,330✔
410
                        attribute.attribute !== 'vertexPositions64Low'
1,197✔
411
                    )
133✔
412
                  }
133✔
413
                : layout
543✔
414
            );
133✔
415

133✔
416
    /*
133✔
417
     *       _
133✔
418
     *        "-_ 1                   3                       5
133✔
419
     *     _     "o---------------------o-------------------_-o
133✔
420
     *       -   / ""--..__              '.             _.-' /
133✔
421
     *   _     "@- - - - - ""--..__- - - - x - - - -_.@'    /
133✔
422
     *    "-_  /                   ""--..__ '.  _,-` :     /
133✔
423
     *       "o----------------------------""-o'    :     /
133✔
424
     *      0,2                            4 / '.  :     /
133✔
425
     *                                      /   '.:     /
133✔
426
     *                                     /     :'.   /
133✔
427
     *                                    /     :  ', /
133✔
428
     *                                   /     :     o
133✔
429
     */
133✔
430

133✔
431
    // prettier-ignore
133✔
432
    const SEGMENT_INDICES = [
133✔
433
      // start corner
133✔
434
      0, 1, 2,
133✔
435
      // body
133✔
436
      1, 4, 2,
133✔
437
      1, 3, 4,
133✔
438
      // end corner
133✔
439
      3, 5, 4
133✔
440
    ];
133✔
441

133✔
442
    // [0] position on segment - 0: start, 1: end
133✔
443
    // [1] side of path - -1: left, 0: center (joint), 1: right
133✔
444
    // prettier-ignore
133✔
445
    const SEGMENT_POSITIONS = [
133✔
446
      // bevel start corner
133✔
447
      0, 0,
133✔
448
      // start inner corner
133✔
449
      0, -1,
133✔
450
      // start outer corner
133✔
451
      0, 1,
133✔
452
      // end inner corner
133✔
453
      1, -1,
133✔
454
      // end outer corner
133✔
455
      1, 1,
133✔
456
      // bevel end corner
133✔
457
      1, 0
133✔
458
    ];
133✔
459

133✔
460
    return new Model(this.context.device, {
133✔
461
      ...this.getShaders(),
133✔
462
      id: this.props.id,
133✔
463
      bufferLayout,
133✔
464
      geometry: new Geometry({
133✔
465
        topology: 'triangle-list',
133✔
466
        attributes: {
133✔
467
          indices: new Uint16Array(SEGMENT_INDICES),
133✔
468
          positions: {value: new Float32Array(SEGMENT_POSITIONS), size: 2}
133✔
469
        }
133✔
470
      }),
133✔
471
      parameters,
133✔
472
      isInstanced: true
133✔
473
    });
133✔
474
  }
133✔
475

1✔
476
  protected calculatePositions(attribute) {
1✔
477
    const {pathTesselator} = this.state;
160✔
478

160✔
479
    attribute.startIndices = pathTesselator.vertexStarts;
160✔
480
    attribute.value = pathTesselator.get('positions');
160✔
481
  }
160✔
482

1✔
483
  protected calculateInstancePositions(attribute) {
1✔
NEW
484
    this._calculateInterleavedInstancePositions(attribute, false);
×
NEW
485
  }
×
486

1✔
487
  protected calculateInstancePositions64Low(attribute) {
1✔
NEW
488
    this._calculateInterleavedInstancePositions(attribute, true);
×
NEW
489
  }
×
490

1✔
491
  protected calculateSegmentTypes(attribute) {
1✔
492
    const {pathTesselator} = this.state;
186✔
493

186✔
494
    attribute.startIndices = pathTesselator.vertexStarts;
186✔
495
    attribute.value = pathTesselator.get('segmentTypes');
186✔
496
  }
186✔
497

1✔
498
  protected _calculateInterleavedInstancePositions(attribute, lowPart: boolean) {
1✔
NEW
499
    const {pathTesselator} = this.state;
×
NEW
500
    const value = pathTesselator.get('positions');
×
NEW
501

×
NEW
502
    if (!value) {
×
NEW
503
      attribute.value = null;
×
NEW
504
      return;
×
NEW
505
    }
×
NEW
506

×
NEW
507
    const numInstances = pathTesselator.instanceCount;
×
NEW
508
    const result = new Float32Array(numInstances * 12);
×
NEW
509

×
NEW
510
    for (let i = 0; i < numInstances; i++) {
×
NEW
511
      const sourceIndex = i * 3;
×
NEW
512
      const targetIndex = i * 12;
×
NEW
513
      for (let vertexOffset = 0; vertexOffset < 4; vertexOffset++) {
×
NEW
514
        const sourceOffset = sourceIndex + vertexOffset * 3;
×
NEW
515
        const targetOffset = targetIndex + vertexOffset * 3;
×
NEW
516
        for (let j = 0; j < 3; j++) {
×
NEW
517
          const position = value[sourceOffset + j];
×
NEW
518
          result[targetOffset + j] = lowPart ? position - Math.fround(position) : position;
×
NEW
519
        }
×
NEW
520
      }
×
NEW
521
    }
×
NEW
522

×
NEW
523
    attribute.startIndices = pathTesselator.vertexStarts;
×
NEW
524
    attribute.value = result;
×
NEW
525
  }
×
526
}
1✔
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