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

visgl / deck.gl / 16250138754

13 Jul 2025 02:25PM UTC coverage: 91.678% (-0.005%) from 91.683%
16250138754

push

github

web-flow
chore: Bump to luma.gl@9.2.0-alpha (#9241)

6765 of 7433 branches covered (91.01%)

Branch coverage included in aggregate %.

107 of 112 new or added lines in 26 files covered. (95.54%)

16 existing lines in 4 files now uncovered.

55842 of 60857 relevant lines covered (91.76%)

14490.8 hits per line

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

95.27
/modules/core/src/lib/deck.ts
1
// deck.gl
1✔
2
// SPDX-License-Identifier: MIT
1✔
3
// Copyright (c) vis.gl contributors
1✔
4

1✔
5
import LayerManager from './layer-manager';
1✔
6
import ViewManager from './view-manager';
1✔
7
import MapView from '../views/map-view';
1✔
8
import EffectManager from './effect-manager';
1✔
9
import DeckRenderer from './deck-renderer';
1✔
10
import DeckPicker from './deck-picker';
1✔
11
import {Widget} from './widget';
1✔
12
import {WidgetManager} from './widget-manager';
1✔
13
import {TooltipWidget} from './tooltip-widget';
1✔
14
import log from '../utils/log';
1✔
15
import {deepEqual} from '../utils/deep-equal';
1✔
16
import typedArrayManager from '../utils/typed-array-manager';
1✔
17
import {VERSION} from './init';
1✔
18

1✔
19
import {luma} from '@luma.gl/core';
1✔
20
import {webgl2Adapter} from '@luma.gl/webgl';
1✔
21
import {Timeline} from '@luma.gl/engine';
1✔
22
import {AnimationLoop} from '@luma.gl/engine';
1✔
23
import {GL} from '@luma.gl/constants';
1✔
24
import type {Device, DeviceProps, Framebuffer, Parameters} from '@luma.gl/core';
1✔
25
import type {ShaderModule} from '@luma.gl/shadertools';
1✔
26

1✔
27
import {Stats} from '@probe.gl/stats';
1✔
28
import {EventManager} from 'mjolnir.js';
1✔
29

1✔
30
import assert from '../utils/assert';
1✔
31
import {EVENT_HANDLERS, RECOGNIZERS, RecognizerOptions} from './constants';
1✔
32

1✔
33
import type {Effect} from './effect';
1✔
34
import type {FilterContext} from '../passes/layers-pass';
1✔
35
import type Layer from './layer';
1✔
36
import type View from '../views/view';
1✔
37
import type Viewport from '../viewports/viewport';
1✔
38
import type {EventManagerOptions, MjolnirGestureEvent, MjolnirPointerEvent} from 'mjolnir.js';
1✔
39
import type {TypedArrayManagerOptions} from '../utils/typed-array-manager';
1✔
40
import type {ViewStateChangeParameters, InteractionState} from '../controllers/controller';
1✔
41
import type {PickingInfo} from './picking/pick-info';
1✔
42
import type {PickByPointOptions, PickByRectOptions} from './deck-picker';
1✔
43
import type {LayersList} from './layer-manager';
1✔
44
import type {TooltipContent} from './tooltip-widget';
1✔
45
import type {ViewStateMap, AnyViewStateOf, ViewOrViews, ViewStateObject} from './view-manager';
1✔
46
import {CreateDeviceProps} from '@luma.gl/core';
1✔
47

1✔
48
/* global document */
1✔
49

1✔
50
// eslint-disable-next-line @typescript-eslint/no-empty-function
1✔
51
function noop() {}
632✔
52

1✔
53
const getCursor = ({isDragging}) => (isDragging ? 'grabbing' : 'grab');
1✔
54

1✔
55
export type DeckMetrics = {
1✔
56
  fps: number;
1✔
57
  setPropsTime: number;
1✔
58
  updateAttributesTime: number;
1✔
59
  framesRedrawn: number;
1✔
60
  pickTime: number;
1✔
61
  pickCount: number;
1✔
62
  gpuTime: number;
1✔
63
  gpuTimePerFrame: number;
1✔
64
  cpuTime: number;
1✔
65
  cpuTimePerFrame: number;
1✔
66
  bufferMemory: number;
1✔
67
  textureMemory: number;
1✔
68
  renderbufferMemory: number;
1✔
69
  gpuMemory: number;
1✔
70
};
1✔
71

1✔
72
type CursorState = {
1✔
73
  /** Whether the cursor is over a pickable object */
1✔
74
  isHovering: boolean;
1✔
75
  /** Whether the cursor is down */
1✔
76
  isDragging: boolean;
1✔
77
};
1✔
78

1✔
79
export type DeckProps<ViewsT extends ViewOrViews = null> = {
1✔
80
  /** Id of this Deck instance */
1✔
81
  id?: string;
1✔
82
  /** Width of the canvas, a number in pixels or a valid CSS string.
1✔
83
   * @default `'100%'`
1✔
84
   */
1✔
85
  width?: string | number | null;
1✔
86
  /** Height of the canvas, a number in pixels or a valid CSS string.
1✔
87
   * @default `'100%'`
1✔
88
   */
1✔
89
  height?: string | number | null;
1✔
90
  /** Additional CSS styles for the canvas. */
1✔
91
  style?: Partial<CSSStyleDeclaration> | null;
1✔
92

1✔
93
  /** Controls the resolution of drawing buffer used for rendering.
1✔
94
   * @default `true` (use browser devicePixelRatio)
1✔
95
   */
1✔
96
  useDevicePixels?: boolean;
1✔
97
  /** Extra pixels around the pointer to include while picking.
1✔
98
   * @default `0`
1✔
99
   */
1✔
100
  pickingRadius?: number;
1✔
101

1✔
102
  /** WebGL parameters to be set before each frame is rendered. */
1✔
103
  parameters?: Parameters;
1✔
104
  /** If supplied, will be called before a layer is drawn to determine whether it should be rendered. */
1✔
105
  layerFilter?: ((context: FilterContext) => boolean) | null;
1✔
106

1✔
107
  /** The container to append the auto-created canvas to.
1✔
108
   * @default `document.body`
1✔
109
   */
1✔
110
  parent?: HTMLDivElement | null;
1✔
111

1✔
112
  /** The canvas to render into.
1✔
113
   * Can be either a HTMLCanvasElement or the element id.
1✔
114
   * Will be auto-created if not supplied.
1✔
115
   */
1✔
116
  canvas?: HTMLCanvasElement | string | null;
1✔
117

1✔
118
  /** Use an existing luma.gl GPU device. @note If not supplied, a new device will be created using props.deviceProps */
1✔
119
  device?: Device | null;
1✔
120

1✔
121
  /** A new device will be created using these props, assuming that an existing device is not supplied using props.device) */
1✔
122
  deviceProps?: CreateDeviceProps;
1✔
123

1✔
124
  /** WebGL context @deprecated Use props.deviceProps.webgl. Also note that preserveDrawingBuffers is true by default */
1✔
125
  gl?: WebGL2RenderingContext | null;
1✔
126

1✔
127
  /**
1✔
128
   * The array of Layer instances to be rendered.
1✔
129
   * Nested arrays are accepted, as well as falsy values (`null`, `false`, `undefined`)
1✔
130
   */
1✔
131
  layers?: LayersList;
1✔
132
  /** The array of effects to be rendered. A lighting effect will be added if an empty array is supplied. */
1✔
133
  effects?: Effect[];
1✔
134
  /** A single View instance, or an array of `View` instances.
1✔
135
   * @default `new MapView()`
1✔
136
   */
1✔
137
  views?: ViewsT;
1✔
138
  /** Options for viewport interactivity, e.g. pan, rotate and zoom with mouse, touch and keyboard.
1✔
139
   * This is a shorthand for defining interaction with the `views` prop if you are using the default view (i.e. a single `MapView`)
1✔
140
   */
1✔
141
  controller?: View['props']['controller'];
1✔
142
  /**
1✔
143
   * An object that describes the view state for each view in the `views` prop.
1✔
144
   * Use if the camera state should be managed external to the `Deck` instance.
1✔
145
   */
1✔
146
  viewState?: ViewStateMap<ViewsT> | null;
1✔
147
  /**
1✔
148
   * If provided, the `Deck` instance will track camera state changes automatically,
1✔
149
   * with `initialViewState` as its initial settings.
1✔
150
   */
1✔
151
  initialViewState?: ViewStateMap<ViewsT> | null;
1✔
152

1✔
153
  /** Allow browser default touch actions.
1✔
154
   * @default `'none'`
1✔
155
   */
1✔
156
  touchAction?: EventManagerOptions['touchAction'];
1✔
157
  /**
1✔
158
   * Optional mjolnir.js recognizer options
1✔
159
   */
1✔
160
  eventRecognizerOptions?: RecognizerOptions;
1✔
161

1✔
162
  /** (Experimental) Render to a custom frame buffer other than to screen. */
1✔
163
  _framebuffer?: Framebuffer | null;
1✔
164
  /** (Experimental) Forces deck.gl to redraw layers every animation frame. */
1✔
165
  _animate?: boolean;
1✔
166
  /** (Experimental) If set to `false`, force disables all picking features, disregarding the `pickable` prop set in any layer. */
1✔
167
  _pickable?: boolean;
1✔
168
  /** (Experimental) Fine-tune attribute memory usage. See documentation for details. */
1✔
169
  _typedArrayManagerProps?: TypedArrayManagerOptions;
1✔
170
  /** An array of Widget instances to be added to the parent element. */
1✔
171
  widgets?: Widget[];
1✔
172

1✔
173
  /** Called once the GPU Device has been initiated. */
1✔
174
  onDeviceInitialized?: (device: Device) => void;
1✔
175
  /** @deprecated Called once the WebGL context has been initiated. */
1✔
176
  onWebGLInitialized?: (gl: WebGL2RenderingContext) => void;
1✔
177
  /** Called when the canvas resizes. */
1✔
178
  onResize?: (dimensions: {width: number; height: number}) => void;
1✔
179
  /** Called when the user has interacted with the deck.gl canvas, e.g. using mouse, touch or keyboard. */
1✔
180
  onViewStateChange?: <ViewStateT extends AnyViewStateOf<ViewsT>>(
1✔
181
    params: ViewStateChangeParameters<ViewStateT>
1✔
182
  ) => ViewStateT | null | void;
1✔
183
  /** Called when the user has interacted with the deck.gl canvas, e.g. using mouse, touch or keyboard. */
1✔
184
  onInteractionStateChange?: (state: InteractionState) => void;
1✔
185
  /** Called just before the canvas rerenders. */
1✔
186
  onBeforeRender?: (context: {device: Device; gl: WebGL2RenderingContext}) => void;
1✔
187
  /** Called right after the canvas rerenders. */
1✔
188
  onAfterRender?: (context: {device: Device; gl: WebGL2RenderingContext}) => void;
1✔
189
  /** Called once after gl context and all Deck components are created. */
1✔
190
  onLoad?: () => void;
1✔
191
  /** Called if deck.gl encounters an error.
1✔
192
   * If this callback is set to `null`, errors are silently ignored.
1✔
193
   * @default `console.error`
1✔
194
   */
1✔
195
  onError?: ((error: Error, layer?: Layer) => void) | null;
1✔
196
  /** Called when the pointer moves over the canvas. */
1✔
197
  onHover?: ((info: PickingInfo, event: MjolnirPointerEvent) => void) | null;
1✔
198
  /** Called when clicking on the canvas. */
1✔
199
  onClick?: ((info: PickingInfo, event: MjolnirGestureEvent) => void) | null;
1✔
200
  /** Called when the user starts dragging on the canvas. */
1✔
201
  onDragStart?: ((info: PickingInfo, event: MjolnirGestureEvent) => void) | null;
1✔
202
  /** Called when dragging the canvas. */
1✔
203
  onDrag?: ((info: PickingInfo, event: MjolnirGestureEvent) => void) | null;
1✔
204
  /** Called when the user releases from dragging the canvas. */
1✔
205
  onDragEnd?: ((info: PickingInfo, event: MjolnirGestureEvent) => void) | null;
1✔
206

1✔
207
  /** (Experimental) Replace the default redraw procedure */
1✔
208
  _customRender?: ((reason: string) => void) | null;
1✔
209
  /** (Experimental) Called once every second with performance metrics. */
1✔
210
  _onMetrics?: ((metrics: DeckMetrics) => void) | null;
1✔
211

1✔
212
  /** A custom callback to retrieve the cursor type. */
1✔
213
  getCursor?: (state: CursorState) => string;
1✔
214
  /** Callback that takes a hovered-over point and renders a tooltip. */
1✔
215
  getTooltip?: ((info: PickingInfo) => TooltipContent) | null;
1✔
216

1✔
217
  /** (Debug) Flag to enable WebGL debug mode. Requires importing `@luma.gl/debug`. */
1✔
218
  debug?: boolean;
1✔
219
  /** (Debug) Render the picking buffer to screen. */
1✔
220
  drawPickingColors?: boolean;
1✔
221
};
1✔
222

1✔
223
const defaultProps: DeckProps = {
1✔
224
  id: '',
1✔
225
  width: '100%',
1✔
226
  height: '100%',
1✔
227
  style: null,
1✔
228
  viewState: null,
1✔
229
  initialViewState: null,
1✔
230
  pickingRadius: 0,
1✔
231
  layerFilter: null,
1✔
232
  parameters: {},
1✔
233
  parent: null,
1✔
234
  device: null,
1✔
235
  deviceProps: {} as DeviceProps,
1✔
236
  gl: null,
1✔
237
  canvas: null,
1✔
238
  layers: [],
1✔
239
  effects: [],
1✔
240
  views: null,
1✔
241
  controller: null, // Rely on external controller, e.g. react-map-gl
1✔
242
  useDevicePixels: true,
1✔
243
  touchAction: 'none',
1✔
244
  eventRecognizerOptions: {},
1✔
245
  _framebuffer: null,
1✔
246
  _animate: false,
1✔
247
  _pickable: true,
1✔
248
  _typedArrayManagerProps: {},
1✔
249
  _customRender: null,
1✔
250
  widgets: [],
1✔
251

1✔
252
  onDeviceInitialized: noop,
1✔
253
  onWebGLInitialized: noop,
1✔
254
  onResize: noop,
1✔
255
  onViewStateChange: noop,
1✔
256
  onInteractionStateChange: noop,
1✔
257
  onBeforeRender: noop,
1✔
258
  onAfterRender: noop,
1✔
259
  onLoad: noop,
1✔
260
  onError: (error: Error) => log.error(error.message, error.cause)(),
1✔
261
  onHover: null,
1✔
262
  onClick: null,
1✔
263
  onDragStart: null,
1✔
264
  onDrag: null,
1✔
265
  onDragEnd: null,
1✔
266
  _onMetrics: null,
1✔
267

1✔
268
  getCursor,
1✔
269
  getTooltip: null,
1✔
270

1✔
271
  debug: false,
1✔
272
  drawPickingColors: false
1✔
273
};
1✔
274

1✔
275
/* eslint-disable max-statements */
1✔
276
export default class Deck<ViewsT extends ViewOrViews = null> {
1✔
277
  static defaultProps = defaultProps;
1✔
278
  // This is used to defeat tree shaking of init.js
1✔
279
  // https://github.com/visgl/deck.gl/issues/3213
1✔
280
  static VERSION = VERSION;
1✔
281

1✔
282
  readonly props: Required<DeckProps<ViewsT>>;
1✔
283
  readonly width: number = 0;
1✔
284
  readonly height: number = 0;
1✔
285
  // Allows attaching arbitrary data to the instance
1✔
286
  readonly userData: Record<string, any> = {};
1✔
287

1✔
288
  protected device: Device | null = null;
1✔
289

1✔
290
  protected canvas: HTMLCanvasElement | null = null;
1✔
291
  protected viewManager: ViewManager<View[]> | null = null;
1✔
292
  protected layerManager: LayerManager | null = null;
1✔
293
  protected effectManager: EffectManager | null = null;
1✔
294
  protected deckRenderer: DeckRenderer | null = null;
1✔
295
  protected deckPicker: DeckPicker | null = null;
1✔
296
  protected eventManager: EventManager | null = null;
1✔
297
  protected widgetManager: WidgetManager | null = null;
1✔
298
  protected tooltip: TooltipWidget | null = null;
1✔
299
  protected animationLoop: AnimationLoop | null = null;
1✔
300

1✔
301
  /** Internal view state if no callback is supplied */
1✔
302
  protected viewState: ViewStateObject<ViewsT> | null;
1✔
303
  protected cursorState: CursorState = {
1✔
304
    isHovering: false,
1✔
305
    isDragging: false
1✔
306
  };
1✔
307

1✔
308
  protected stats = new Stats({id: 'deck.gl'});
1✔
309
  protected metrics: DeckMetrics = {
1✔
310
    fps: 0,
1✔
311
    setPropsTime: 0,
1✔
312
    updateAttributesTime: 0,
1✔
313
    framesRedrawn: 0,
1✔
314
    pickTime: 0,
1✔
315
    pickCount: 0,
1✔
316
    gpuTime: 0,
1✔
317
    gpuTimePerFrame: 0,
1✔
318
    cpuTime: 0,
1✔
319
    cpuTimePerFrame: 0,
1✔
320
    bufferMemory: 0,
1✔
321
    textureMemory: 0,
1✔
322
    renderbufferMemory: 0,
1✔
323
    gpuMemory: 0
1✔
324
  };
1✔
325
  private _metricsCounter: number = 0;
1✔
326

1✔
327
  private _needsRedraw: false | string = 'Initial render';
1✔
328
  private _pickRequest: {
1✔
329
    mode: string;
1✔
330
    event: MjolnirPointerEvent | null;
1✔
331
    x: number;
1✔
332
    y: number;
1✔
333
    radius: number;
1✔
334
  } = {
1✔
335
    mode: 'hover',
1✔
336
    x: -1,
1✔
337
    y: -1,
1✔
338
    radius: 0,
1✔
339
    event: null
1✔
340
  };
1✔
341

1✔
342
  /**
1✔
343
   * Pick and store the object under the pointer on `pointerdown`.
1✔
344
   * This object is reused for subsequent `onClick` and `onDrag*` callbacks.
1✔
345
   */
1✔
346
  private _lastPointerDownInfo: PickingInfo | null = null;
1✔
347

1✔
348
  constructor(props: DeckProps<ViewsT>) {
1✔
349
    // @ts-ignore views
40✔
350
    this.props = {...defaultProps, ...props};
40✔
351
    props = this.props;
40✔
352

40✔
353
    if (props.viewState && props.initialViewState) {
40!
354
      log.warn(
×
355
        'View state tracking is disabled. Use either `initialViewState` for auto update or `viewState` for manual update.'
×
356
      )();
×
357
    }
×
358
    this.viewState = this.props.initialViewState;
40✔
359

40✔
360
    // See if we already have a device
40✔
361
    if (props.device) {
40✔
362
      this.device = props.device;
13✔
363
    }
13✔
364

40✔
365
    let deviceOrPromise: Device | Promise<Device> | null = this.device;
40✔
366

40✔
367
    // Attach a new luma.gl device to a WebGL2 context if supplied
40✔
368
    if (!deviceOrPromise && props.gl) {
40✔
369
      if (props.gl instanceof WebGLRenderingContext) {
7!
370
        log.error('WebGL1 context not supported.')();
×
371
      }
×
372
      deviceOrPromise = webgl2Adapter.attach(props.gl);
7✔
373
    }
7✔
374

40✔
375
    // Create a new device
40✔
376
    if (!deviceOrPromise) {
40✔
377
      // Create the "best" device supported from the registered adapters
20✔
378
      deviceOrPromise = luma.createDevice({
20✔
379
        type: 'webgl',
20✔
380
        // luma by default throws if a device is already attached
20✔
381
        // asynchronous device creation could happen after finalize() is called
20✔
382
        // TODO - createDevice should support AbortController?
20✔
383
        _reuseDevices: true,
20✔
384
        adapters: [webgl2Adapter],
20✔
385
        ...props.deviceProps,
20✔
386
        createCanvasContext: {
20✔
387
          canvas: this._createCanvas(props),
20✔
388
          useDevicePixels: this.props.useDevicePixels,
20✔
389
          autoResize: true
20✔
390
        }
20✔
391
      });
20✔
392
    }
20✔
393

40✔
394
    this.animationLoop = this._createAnimationLoop(deviceOrPromise, props);
40✔
395

40✔
396
    this.setProps(props);
40✔
397

40✔
398
    // UNSAFE/experimental prop: only set at initialization to avoid performance hit
40✔
399
    if (props._typedArrayManagerProps) {
40✔
400
      typedArrayManager.setOptions(props._typedArrayManagerProps);
40✔
401
    }
40✔
402

40✔
403
    this.animationLoop.start();
40✔
404
  }
40✔
405

1✔
406
  /** Stop rendering and dispose all resources */
1✔
407
  finalize() {
1✔
408
    this.animationLoop?.stop();
38✔
409
    this.animationLoop?.destroy();
38✔
410
    this.animationLoop = null;
38✔
411
    this._lastPointerDownInfo = null;
38✔
412

38✔
413
    this.layerManager?.finalize();
38✔
414
    this.layerManager = null;
38✔
415

38✔
416
    this.viewManager?.finalize();
38✔
417
    this.viewManager = null;
38✔
418

38✔
419
    this.effectManager?.finalize();
38✔
420
    this.effectManager = null;
38✔
421

38✔
422
    this.deckRenderer?.finalize();
38✔
423
    this.deckRenderer = null;
38✔
424

38✔
425
    this.deckPicker?.finalize();
38✔
426
    this.deckPicker = null;
38✔
427

38✔
428
    this.eventManager?.destroy();
38✔
429
    this.eventManager = null;
38✔
430

38✔
431
    this.widgetManager?.finalize();
38✔
432
    this.widgetManager = null;
38✔
433

38✔
434
    if (!this.props.canvas && !this.props.device && !this.props.gl && this.canvas) {
38✔
435
      // remove internally created canvas
6✔
436
      this.canvas.parentElement?.removeChild(this.canvas);
6✔
437
      this.canvas = null;
6✔
438
    }
6✔
439
  }
38✔
440

1✔
441
  /** Partially update props */
1✔
442
  setProps(props: DeckProps<ViewsT>): void {
1✔
443
    this.stats.get('setProps Time').timeStart();
266✔
444

266✔
445
    if ('onLayerHover' in props) {
266!
446
      log.removed('onLayerHover', 'onHover')();
×
447
    }
×
448
    if ('onLayerClick' in props) {
266!
449
      log.removed('onLayerClick', 'onClick')();
×
450
    }
×
451
    if (
266✔
452
      props.initialViewState &&
266✔
453
      // depth = 3 when comparing viewStates: viewId.position.0
24✔
454
      !deepEqual(this.props.initialViewState, props.initialViewState, 3)
24✔
455
    ) {
266!
456
      // Overwrite internal view state
×
457
      this.viewState = props.initialViewState;
×
458
    }
×
459

266✔
460
    // Merge with existing props
266✔
461
    Object.assign(this.props, props);
266✔
462

266✔
463
    // Update CSS size of canvas
266✔
464
    this._setCanvasSize(this.props);
266✔
465

266✔
466
    // We need to overwrite CSS style width and height with actual, numeric values
266✔
467
    const resolvedProps: Required<DeckProps> & {
266✔
468
      width: number;
266✔
469
      height: number;
266✔
470
      views: View[];
266✔
471
      viewState: ViewStateObject<ViewsT> | null;
266✔
472
    } = Object.create(this.props);
266✔
473
    Object.assign(resolvedProps, {
266✔
474
      views: this._getViews(),
266✔
475
      width: this.width,
266✔
476
      height: this.height,
266✔
477
      viewState: this._getViewState()
266✔
478
    });
266✔
479

266✔
480
    // Update the animation loop
266✔
481
    this.animationLoop?.setProps(resolvedProps);
266✔
482

266✔
483
    if (
266✔
484
      props.useDevicePixels !== undefined &&
266✔
485
      this.device?.canvasContext?.canvas instanceof HTMLCanvasElement
240✔
486
    ) {
266✔
487
      // TODO: It would be much cleaner if CanvasContext had a setProps method
207✔
488
      this.device.canvasContext.props.useDevicePixels = props.useDevicePixels;
207✔
489
      const canvas = this.device.canvasContext.canvas;
207✔
490
      const entry = {
207✔
491
        target: canvas,
207✔
492
        contentBoxSize: [{inlineSize: canvas.clientWidth, blockSize: canvas.clientHeight}],
207✔
493
        devicePixelContentBoxSize: [
207✔
494
          {inlineSize: canvas.clientWidth, blockSize: canvas.clientHeight}
207✔
495
        ],
207✔
496
        borderBoxSize: [{inlineSize: canvas.clientWidth, blockSize: canvas.clientHeight}]
207✔
497
      };
207✔
498
      // Access the protected _handleResize method through the canvas context
207✔
499
      (this.device.canvasContext as any)._handleResize([entry]);
207✔
500
    }
207✔
501

266✔
502
    // If initialized, update sub manager props
266✔
503
    if (this.layerManager) {
266✔
504
      this.viewManager!.setProps(resolvedProps);
200✔
505
      // Make sure that any new layer gets initialized with the current viewport
200✔
506
      this.layerManager.activateViewport(this.getViewports()[0]);
200✔
507
      this.layerManager.setProps(resolvedProps);
200✔
508
      this.effectManager!.setProps(resolvedProps);
200✔
509
      this.deckRenderer!.setProps(resolvedProps);
200✔
510
      this.deckPicker!.setProps(resolvedProps);
200✔
511
      this.widgetManager!.setProps(resolvedProps);
200✔
512
    }
200✔
513

266✔
514
    this.stats.get('setProps Time').timeEnd();
266✔
515
  }
266✔
516

1✔
517
  // Public API
1✔
518

1✔
519
  /**
1✔
520
   * Check if a redraw is needed
1✔
521
   * @returns `false` or a string summarizing the redraw reason
1✔
522
   */
1✔
523
  needsRedraw(
1✔
524
    opts: {
5,662✔
525
      /** Reset the redraw flag afterwards. Default `true` */
5,662✔
526
      clearRedrawFlags: boolean;
5,662✔
527
    } = {clearRedrawFlags: false}
5,662✔
528
  ): false | string {
5,662✔
529
    if (!this.layerManager) {
5,662!
530
      // Not initialized or already finalized
×
531
      return false;
×
532
    }
×
533
    if (this.props._animate) {
5,662!
534
      return 'Deck._animate';
×
535
    }
×
536

5,662✔
537
    let redraw: false | string = this._needsRedraw;
5,662✔
538

5,662✔
539
    if (opts.clearRedrawFlags) {
5,662✔
540
      this._needsRedraw = false;
5,662✔
541
    }
5,662✔
542

5,662✔
543
    const viewManagerNeedsRedraw = this.viewManager!.needsRedraw(opts);
5,662✔
544
    const layerManagerNeedsRedraw = this.layerManager.needsRedraw(opts);
5,662✔
545
    const effectManagerNeedsRedraw = this.effectManager!.needsRedraw(opts);
5,662✔
546
    const deckRendererNeedsRedraw = this.deckRenderer!.needsRedraw(opts);
5,662✔
547

5,662✔
548
    redraw =
5,662✔
549
      redraw ||
5,662✔
550
      viewManagerNeedsRedraw ||
5,637✔
551
      layerManagerNeedsRedraw ||
5,400✔
552
      effectManagerNeedsRedraw ||
5,190✔
553
      deckRendererNeedsRedraw;
5,190✔
554
    return redraw;
5,662✔
555
  }
5,662✔
556

1✔
557
  /**
1✔
558
   * Redraw the GL context
1✔
559
   * @param reason If not provided, only redraw if deemed necessary. Otherwise redraw regardless of internal states.
1✔
560
   * @returns
1✔
561
   */
1✔
562
  redraw(reason?: string): void {
1✔
563
    if (!this.layerManager) {
5,663✔
564
      // Not yet initialized
1✔
565
      return;
1✔
566
    }
1✔
567
    // Check if we need to redraw
5,662✔
568
    let redrawReason = this.needsRedraw({clearRedrawFlags: true});
5,662✔
569
    // User-supplied should take precedent, however the redraw flags get cleared regardless
5,662✔
570
    redrawReason = reason || redrawReason;
5,663✔
571

5,663✔
572
    if (!redrawReason) {
5,663✔
573
      return;
5,188✔
574
    }
5,188✔
575

474✔
576
    this.stats.get('Redraw Count').incrementCount();
474✔
577
    if (this.props._customRender) {
5,663✔
578
      this.props._customRender(redrawReason);
12✔
579
    } else {
5,663✔
580
      this._drawLayers(redrawReason);
462✔
581
    }
462✔
582
  }
5,663✔
583

1✔
584
  /** Flag indicating that the Deck instance has initialized its resources and it's safe to call public methods. */
1✔
585
  get isInitialized(): boolean {
1✔
586
    return this.viewManager !== null;
35✔
587
  }
35✔
588

1✔
589
  /** Get a list of views that are currently rendered */
1✔
590
  getViews(): View[] {
1✔
591
    assert(this.viewManager);
×
592
    return this.viewManager.views;
×
593
  }
×
594

1✔
595
  /** Get a list of viewports that are currently rendered.
1✔
596
   * @param rect If provided, only returns viewports within the given bounding box.
1✔
597
   */
1✔
598
  getViewports(rect?: {x: number; y: number; width?: number; height?: number}): Viewport[] {
1✔
599
    assert(this.viewManager);
350✔
600
    return this.viewManager.getViewports(rect);
350✔
601
  }
350✔
602

1✔
603
  /** Get the current canvas element. */
1✔
604
  getCanvas(): HTMLCanvasElement | null {
1✔
605
    return this.canvas;
152✔
606
  }
152✔
607

1✔
608
  /** Query the object rendered on top at a given point */
1✔
609
  pickObject(opts: {
1✔
610
    /** x position in pixels */
18✔
611
    x: number;
18✔
612
    /** y position in pixels */
18✔
613
    y: number;
18✔
614
    /** Radius of tolerance in pixels. Default `0`. */
18✔
615
    radius?: number;
18✔
616
    /** A list of layer ids to query from. If not specified, then all pickable and visible layers are queried. */
18✔
617
    layerIds?: string[];
18✔
618
    /** If `true`, `info.coordinate` will be a 3D point by unprojecting the `x, y` screen coordinates onto the picked geometry. Default `false`. */
18✔
619
    unproject3D?: boolean;
18✔
620
  }): PickingInfo | null {
18✔
621
    const infos = this._pick('pickObject', 'pickObject Time', opts).result;
18✔
622
    return infos.length ? infos[0] : null;
18✔
623
  }
18✔
624

1✔
625
  /* Query all rendered objects at a given point */
1✔
626
  pickMultipleObjects(opts: {
1✔
627
    /** x position in pixels */
16✔
628
    x: number;
16✔
629
    /** y position in pixels */
16✔
630
    y: number;
16✔
631
    /** Radius of tolerance in pixels. Default `0`. */
16✔
632
    radius?: number;
16✔
633
    /** Specifies the max number of objects to return. Default `10`. */
16✔
634
    depth?: number;
16✔
635
    /** A list of layer ids to query from. If not specified, then all pickable and visible layers are queried. */
16✔
636
    layerIds?: string[];
16✔
637
    /** If `true`, `info.coordinate` will be a 3D point by unprojecting the `x, y` screen coordinates onto the picked geometry. Default `false`. */
16✔
638
    unproject3D?: boolean;
16✔
639
  }): PickingInfo[] {
16✔
640
    opts.depth = opts.depth || 10;
16✔
641
    return this._pick('pickObject', 'pickMultipleObjects Time', opts).result;
16✔
642
  }
16✔
643

1✔
644
  /* Query all objects rendered on top within a bounding box */
1✔
645
  pickObjects(opts: {
1✔
646
    /** Left of the bounding box in pixels */
18✔
647
    x: number;
18✔
648
    /** Top of the bounding box in pixels */
18✔
649
    y: number;
18✔
650
    /** Width of the bounding box in pixels. Default `1` */
18✔
651
    width?: number;
18✔
652
    /** Height of the bounding box in pixels. Default `1` */
18✔
653
    height?: number;
18✔
654
    /** A list of layer ids to query from. If not specified, then all pickable and visible layers are queried. */
18✔
655
    layerIds?: string[];
18✔
656
    /** If specified, limits the number of objects that can be returned. */
18✔
657
    maxObjects?: number | null;
18✔
658
  }): PickingInfo[] {
18✔
659
    return this._pick('pickObjects', 'pickObjects Time', opts);
18✔
660
  }
18✔
661

1✔
662
  /** Experimental
1✔
663
   * Add a global resource for sharing among layers
1✔
664
   */
1✔
665
  _addResources(
1✔
666
    resources: {
2✔
667
      [id: string]: any;
2✔
668
    },
2✔
669
    forceUpdate = false
2✔
670
  ) {
2✔
671
    for (const id in resources) {
2✔
672
      this.layerManager!.resourceManager.add({resourceId: id, data: resources[id], forceUpdate});
2✔
673
    }
2✔
674
  }
2✔
675

1✔
676
  /** Experimental
1✔
677
   * Remove a global resource
1✔
678
   */
1✔
679
  _removeResources(resourceIds: string[]) {
1✔
680
    for (const id of resourceIds) {
1✔
681
      this.layerManager!.resourceManager.remove(id);
1✔
682
    }
1✔
683
  }
1✔
684

1✔
685
  /** Experimental
1✔
686
   * Register a default effect. Effects will be sorted by order, those with a low order will be rendered first
1✔
687
   */
1✔
688
  _addDefaultEffect(effect: Effect) {
1✔
689
    this.effectManager!.addDefaultEffect(effect);
12✔
690
  }
12✔
691

1✔
692
  _addDefaultShaderModule(module: ShaderModule<Record<string, unknown>>) {
1✔
693
    this.layerManager!.addDefaultShaderModule(module);
2✔
694
  }
2✔
695

1✔
696
  _removeDefaultShaderModule(module: ShaderModule<Record<string, unknown>>) {
1✔
697
    this.layerManager?.removeDefaultShaderModule(module);
2✔
698
  }
2✔
699

1✔
700
  // Private Methods
1✔
701

1✔
702
  private _pick(
1✔
703
    method: 'pickObject',
1✔
704
    statKey: string,
1✔
705
    opts: PickByPointOptions & {layerIds?: string[]}
1✔
706
  ): {
1✔
707
    result: PickingInfo[];
1✔
708
    emptyInfo: PickingInfo;
1✔
709
  };
1✔
710
  private _pick(
1✔
711
    method: 'pickObjects',
1✔
712
    statKey: string,
1✔
713
    opts: PickByRectOptions & {layerIds?: string[]}
1✔
714
  ): PickingInfo[];
1✔
715

1✔
716
  private _pick(
1✔
717
    method: 'pickObject' | 'pickObjects',
65✔
718
    statKey: string,
65✔
719
    opts: (PickByPointOptions | PickByRectOptions) & {layerIds?: string[]}
65✔
720
  ) {
65✔
721
    assert(this.deckPicker);
65✔
722

65✔
723
    const {stats} = this;
65✔
724

65✔
725
    stats.get('Pick Count').incrementCount();
65✔
726
    stats.get(statKey).timeStart();
65✔
727

65✔
728
    const infos = this.deckPicker[method]({
65✔
729
      // layerManager, viewManager and effectManager are always defined if deckPicker is
65✔
730
      layers: this.layerManager!.getLayers(opts),
65✔
731
      views: this.viewManager!.getViews(),
65✔
732
      viewports: this.getViewports(opts),
65✔
733
      onViewportActive: this.layerManager!.activateViewport,
65✔
734
      effects: this.effectManager!.getEffects(),
65✔
735
      ...opts
65✔
736
    });
65✔
737

65✔
738
    stats.get(statKey).timeEnd();
65✔
739

65✔
740
    return infos;
65✔
741
  }
65✔
742

1✔
743
  /** Resolve props.canvas to element */
1✔
744
  private _createCanvas(props: DeckProps<ViewsT>): HTMLCanvasElement {
1✔
745
    let canvas = props.canvas;
20✔
746

20✔
747
    // TODO EventManager should accept element id
20✔
748
    if (typeof canvas === 'string') {
20!
749
      canvas = document.getElementById(canvas) as HTMLCanvasElement;
×
750
      assert(canvas);
×
751
    }
×
752

20✔
753
    if (!canvas) {
20✔
754
      canvas = document.createElement('canvas');
16✔
755
      canvas.id = props.id || 'deckgl-overlay';
16✔
756

16✔
757
      // TODO this is a hack, investigate why these are not set for the picking
16✔
758
      // tests
16✔
759
      if (props.width && typeof props.width === 'number') {
16✔
760
        canvas.width = props.width;
5✔
761
      }
5✔
762
      if (props.height && typeof props.height === 'number') {
16✔
763
        canvas.height = props.height;
5✔
764
      }
5✔
765
      const parent = props.parent || document.body;
16✔
766
      parent.appendChild(canvas);
16✔
767
    }
16✔
768

20✔
769
    Object.assign(canvas.style, props.style);
20✔
770

20✔
771
    return canvas;
20✔
772
  }
20✔
773

1✔
774
  /** Updates canvas width and/or height, if provided as props */
1✔
775
  private _setCanvasSize(props: Required<DeckProps<ViewsT>>): void {
1✔
776
    if (!this.canvas) {
266✔
777
      return;
66✔
778
    }
66✔
779

200✔
780
    const {width, height} = props;
200✔
781
    // Set size ONLY if props are being provided, otherwise let canvas be layouted freely
200✔
782
    if (width || width === 0) {
266✔
783
      const cssWidth = Number.isFinite(width) ? `${width}px` : (width as string);
194✔
784
      this.canvas.style.width = cssWidth;
194✔
785
    }
194✔
786
    if (height || height === 0) {
266✔
787
      const cssHeight = Number.isFinite(height) ? `${height}px` : (height as string);
194✔
788
      // Note: position==='absolute' required for height 100% to work
194✔
789
      this.canvas.style.position = props.style?.position || 'absolute';
194✔
790
      this.canvas.style.height = cssHeight;
194✔
791
    }
194✔
792
  }
266✔
793

1✔
794
  /** If canvas size has changed, reads out the new size and update */
1✔
795
  private _updateCanvasSize(): void {
1✔
796
    const {canvas} = this;
5,688✔
797
    if (!canvas) {
5,688!
798
      return;
×
799
    }
×
800
    // Fallback to width/height when clientWidth/clientHeight are undefined (OffscreenCanvas).
5,688✔
801
    const newWidth = canvas.clientWidth ?? canvas.width;
5,688!
802
    const newHeight = canvas.clientHeight ?? canvas.height;
5,688!
803
    if (newWidth !== this.width || newHeight !== this.height) {
5,688✔
804
      // @ts-expect-error private assign to read-only property
26✔
805
      this.width = newWidth;
26✔
806
      // @ts-expect-error private assign to read-only property
26✔
807
      this.height = newHeight;
26✔
808
      this.viewManager?.setProps({width: newWidth, height: newHeight});
26✔
809
      // Make sure that any new layer gets initialized with the current viewport
26✔
810
      this.layerManager?.activateViewport(this.getViewports()[0]);
26✔
811
      this.props.onResize({width: newWidth, height: newHeight});
26✔
812
    }
26✔
813
  }
5,688✔
814

1✔
815
  private _createAnimationLoop(
1✔
816
    deviceOrPromise: Device | Promise<Device>,
40✔
817
    props: DeckProps<ViewsT>
40✔
818
  ): AnimationLoop {
40✔
819
    const {
40✔
820
      // width,
40✔
821
      // height,
40✔
822
      gl,
40✔
823
      // debug,
40✔
824
      onError
40✔
825
      // onBeforeRender,
40✔
826
      // onAfterRender,
40✔
827
    } = props;
40✔
828

40✔
829
    return new AnimationLoop({
40✔
830
      device: deviceOrPromise,
40✔
831
      // TODO v9
40✔
832
      autoResizeDrawingBuffer: !gl, // do not auto resize external context
40✔
833
      autoResizeViewport: false,
40✔
834
      // @ts-expect-error luma.gl needs to accept Promise<void> return value
40✔
835
      onInitialize: context => this._setDevice(context.device),
40✔
836
      onRender: this._onRenderFrame.bind(this),
40✔
837
      // @ts-expect-error typing mismatch: AnimationLoop does not accept onError:null
40✔
838
      onError
40✔
839

40✔
840
      // onBeforeRender,
40✔
841
      // onAfterRender,
40✔
842
    });
40✔
843
  }
40✔
844

1✔
845
  // Get the most relevant view state: props.viewState, if supplied, shadows internal viewState
1✔
846
  // TODO: For backwards compatibility ensure numeric width and height is added to the viewState
1✔
847
  private _getViewState(): ViewStateObject<ViewsT> | null {
1✔
848
    return this.props.viewState || this.viewState;
295✔
849
  }
295✔
850

1✔
851
  // Get the view descriptor list
1✔
852
  private _getViews(): View[] {
1✔
853
    const {views} = this.props;
295✔
854
    const normalizedViews: View[] = Array.isArray(views)
295✔
855
      ? views
159✔
856
      : // If null, default to a full screen map view port
136✔
857
        views
136✔
858
        ? [views]
71✔
859
        : [new MapView({id: 'default-view'})];
65✔
860
    if (normalizedViews.length && this.props.controller) {
295✔
861
      // Backward compatibility: support controller prop
9✔
862
      normalizedViews[0].props.controller = this.props.controller;
9✔
863
    }
9✔
864
    return normalizedViews;
295✔
865
  }
295✔
866

1✔
867
  private _onContextLost() {
1✔
868
    const {onError} = this.props;
×
869
    if (this.animationLoop && onError) {
×
870
      onError(new Error('WebGL context is lost'));
×
871
    }
×
872
  }
×
873

1✔
874
  // The `pointermove` event may fire multiple times in between two animation frames,
1✔
875
  // it's a waste of time to run picking without rerender. Instead we save the last pick
1✔
876
  // request and only do it once on the next animation frame.
1✔
877
  /** Internal use only: event handler for pointerdown */
1✔
878
  _onPointerMove = (event: MjolnirPointerEvent) => {
1✔
879
    const {_pickRequest} = this;
13✔
880
    if (event.type === 'pointerleave') {
13!
881
      _pickRequest.x = -1;
×
882
      _pickRequest.y = -1;
×
883
      _pickRequest.radius = 0;
×
884
    } else if (event.leftButton || event.rightButton) {
13✔
885
      // Do not trigger onHover callbacks if mouse button is down.
6✔
886
      return;
6✔
887
    } else {
13✔
888
      const pos = event.offsetCenter;
7✔
889
      // Do not trigger callbacks when click/hover position is invalid. Doing so will cause a
7✔
890
      // assertion error when attempting to unproject the position.
7✔
891
      if (!pos) {
7!
892
        return;
×
893
      }
×
894
      _pickRequest.x = pos.x;
7✔
895
      _pickRequest.y = pos.y;
7✔
896
      _pickRequest.radius = this.props.pickingRadius;
7✔
897
    }
7✔
898

7✔
899
    if (this.layerManager) {
7✔
900
      this.layerManager.context.mousePosition = {x: _pickRequest.x, y: _pickRequest.y};
7✔
901
    }
7✔
902

7✔
903
    _pickRequest.event = event;
7✔
904
  };
13✔
905

1✔
906
  /** Actually run picking */
1✔
907
  private _pickAndCallback() {
1✔
908
    if (this.device?.type === 'webgpu') {
5,659!
909
      return;
×
910
    }
×
911

5,659✔
912
    const {_pickRequest} = this;
5,659✔
913

5,659✔
914
    if (_pickRequest.event) {
5,659✔
915
      // Perform picking
7✔
916
      const {result, emptyInfo} = this._pick('pickObject', 'pickObject Time', _pickRequest);
7✔
917
      this.cursorState.isHovering = result.length > 0;
7✔
918

7✔
919
      // There are 4 possible scenarios:
7✔
920
      // result is [outInfo, pickedInfo] (moved from one pickable layer to another)
7✔
921
      // result is [outInfo] (moved outside of a pickable layer)
7✔
922
      // result is [pickedInfo] (moved into or over a pickable layer)
7✔
923
      // result is [] (nothing is or was picked)
7✔
924
      //
7✔
925
      // `layer.props.onHover` should be called on all affected layers (out/over)
7✔
926
      // `deck.props.onHover` should be called with the picked info if any, or empty info otherwise
7✔
927
      // `deck.props.getTooltip` should be called with the picked info if any, or empty info otherwise
7✔
928

7✔
929
      // Execute callbacks
7✔
930
      let pickedInfo = emptyInfo;
7✔
931
      let handled = false;
7✔
932
      for (const info of result) {
7✔
933
        pickedInfo = info;
1✔
934
        handled = info.layer?.onHover(info, _pickRequest.event) || handled;
1✔
935
      }
1✔
936
      if (!handled) {
7✔
937
        this.props.onHover?.(pickedInfo, _pickRequest.event);
7✔
938
        this.widgetManager!.onHover(pickedInfo, _pickRequest.event);
7✔
939
      }
7✔
940

7✔
941
      // Clear pending pickRequest
7✔
942
      _pickRequest.event = null;
7✔
943
    }
7✔
944
  }
5,659✔
945

1✔
946
  private _updateCursor(): void {
1✔
947
    const container = this.props.parent || this.canvas;
5,659✔
948
    if (container) {
5,659✔
949
      container.style.cursor = this.props.getCursor(this.cursorState);
5,659✔
950
    }
5,659✔
951
  }
5,659✔
952

1✔
953
  private _setDevice(device: Device) {
1✔
954
    this.device = device;
40✔
955

40✔
956
    if (!this.animationLoop) {
40✔
957
      // finalize() has been called
11✔
958
      return;
11✔
959
    }
11✔
960

29✔
961
    // if external context...
29✔
962
    if (!this.canvas) {
29✔
963
      this.canvas = this.device.canvasContext?.canvas as HTMLCanvasElement;
29✔
964
      // TODO v9
29✔
965
      // ts-expect-error - Currently luma.gl v9 does not expose these options
29✔
966
      // All WebGLDevice contexts are instrumented, but it seems the device
29✔
967
      // should have a method to start state tracking even if not enabled?
29✔
968
      // instrumentGLContext(this.device.gl, {enable: true, copyState: true});
29✔
969
    }
29✔
970

29✔
971
    if (this.device.type === 'webgl') {
29✔
972
      this.device.setParametersWebGL({
29✔
973
        blend: true,
29✔
974
        blendFunc: [GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA, GL.ONE, GL.ONE_MINUS_SRC_ALPHA],
29✔
975
        polygonOffsetFill: true,
29✔
976
        depthTest: true,
29✔
977
        depthFunc: GL.LEQUAL
29✔
978
      });
29✔
979
    }
29✔
980

29✔
981
    this.props.onDeviceInitialized(this.device);
29✔
982
    if (this.device.type === 'webgl') {
29✔
983
      // Legacy callback - warn?
29✔
984
      // @ts-expect-error gl is not visible on Device base class
29✔
985
      this.props.onWebGLInitialized(this.device.gl);
29✔
986
    }
29✔
987

29✔
988
    // timeline for transitions
29✔
989
    const timeline = new Timeline();
29✔
990
    timeline.play();
29✔
991
    this.animationLoop.attachTimeline(timeline);
29✔
992

29✔
993
    this.eventManager = new EventManager(this.props.parent || this.canvas, {
40✔
994
      touchAction: this.props.touchAction,
40✔
995
      recognizers: Object.keys(RECOGNIZERS).map((eventName: string) => {
40✔
996
        // Resolve recognizer settings
145✔
997
        const [RecognizerConstructor, defaultOptions, recognizeWith, requestFailure] =
145✔
998
          RECOGNIZERS[eventName];
145✔
999
        const optionsOverride = this.props.eventRecognizerOptions?.[eventName];
145✔
1000
        const options = {...defaultOptions, ...optionsOverride, event: eventName};
145✔
1001
        return {
145✔
1002
          recognizer: new RecognizerConstructor(options),
145✔
1003
          recognizeWith,
145✔
1004
          requestFailure
145✔
1005
        };
145✔
1006
      }),
40✔
1007
      events: {
40✔
1008
        pointerdown: this._onPointerDown,
40✔
1009
        pointermove: this._onPointerMove,
40✔
1010
        pointerleave: this._onPointerMove
40✔
1011
      }
40✔
1012
    });
40✔
1013
    for (const eventType in EVENT_HANDLERS) {
40✔
1014
      this.eventManager.on(eventType, this._onEvent);
145✔
1015
    }
145✔
1016

29✔
1017
    this.viewManager = new ViewManager({
29✔
1018
      timeline,
29✔
1019
      eventManager: this.eventManager,
29✔
1020
      onViewStateChange: this._onViewStateChange.bind(this),
29✔
1021
      onInteractionStateChange: this._onInteractionStateChange.bind(this),
29✔
1022
      views: this._getViews(),
29✔
1023
      viewState: this._getViewState(),
29✔
1024
      width: this.width,
29✔
1025
      height: this.height
29✔
1026
    });
29✔
1027

29✔
1028
    // viewManager must be initialized before layerManager
29✔
1029
    // layerManager depends on viewport created by viewManager.
29✔
1030
    const viewport = this.viewManager.getViewports()[0];
29✔
1031

29✔
1032
    // Note: avoid React setState due GL animation loop / setState timing issue
29✔
1033
    this.layerManager = new LayerManager(this.device, {
29✔
1034
      deck: this,
29✔
1035
      stats: this.stats,
29✔
1036
      viewport,
29✔
1037
      timeline
29✔
1038
    });
29✔
1039

29✔
1040
    this.effectManager = new EffectManager({
29✔
1041
      deck: this,
29✔
1042
      device: this.device
29✔
1043
    });
29✔
1044

29✔
1045
    this.deckRenderer = new DeckRenderer(this.device);
29✔
1046

29✔
1047
    this.deckPicker = new DeckPicker(this.device);
29✔
1048

29✔
1049
    this.widgetManager = new WidgetManager({
29✔
1050
      deck: this,
29✔
1051
      parentElement: this.canvas?.parentElement
29✔
1052
    });
40✔
1053
    this.widgetManager.addDefault(new TooltipWidget());
40✔
1054

40✔
1055
    this.setProps(this.props);
40✔
1056

40✔
1057
    this._updateCanvasSize();
40✔
1058
    this.props.onLoad();
40✔
1059
  }
40✔
1060

1✔
1061
  /** Internal only: default render function (redraw all layers and views) */
1✔
1062
  _drawLayers(
1✔
1063
    redrawReason: string,
475✔
1064
    renderOptions?: {
475✔
1065
      target?: Framebuffer;
475✔
1066
      layerFilter?: (context: FilterContext) => boolean;
475✔
1067
      layers?: Layer[];
475✔
1068
      viewports?: Viewport[];
475✔
1069
      views?: {[viewId: string]: View};
475✔
1070
      pass?: string;
475✔
1071
      effects?: Effect[];
475✔
1072
      clearStack?: boolean;
475✔
1073
      clearCanvas?: boolean;
475✔
1074
    }
475✔
1075
  ) {
475✔
1076
    const {device, gl} = this.layerManager!.context;
475✔
1077

475✔
1078
    this.props.onBeforeRender({device, gl});
475✔
1079

475✔
1080
    const opts = {
475✔
1081
      target: this.props._framebuffer,
475✔
1082
      layers: this.layerManager!.getLayers(),
475✔
1083
      viewports: this.viewManager!.getViewports(),
475✔
1084
      onViewportActive: this.layerManager!.activateViewport,
475✔
1085
      views: this.viewManager!.getViews(),
475✔
1086
      pass: 'screen',
475✔
1087
      effects: this.effectManager!.getEffects(),
475✔
1088
      ...renderOptions
475✔
1089
    };
475✔
1090
    this.deckRenderer?.renderLayers(opts);
475✔
1091

475✔
1092
    if (opts.pass === 'screen') {
475✔
1093
      // This method could be called when drawing to picking buffer, texture etc.
475✔
1094
      // Only when drawing to screen, update all widgets (UI components)
475✔
1095
      this.widgetManager!.onRedraw({
475✔
1096
        viewports: opts.viewports,
475✔
1097
        layers: opts.layers
475✔
1098
      });
475✔
1099
    }
475✔
1100

475✔
1101
    this.props.onAfterRender({device, gl});
475✔
1102
  }
475✔
1103

1✔
1104
  // Callbacks
1✔
1105

1✔
1106
  private _onRenderFrame() {
1✔
1107
    this._getFrameStats();
5,659✔
1108

5,659✔
1109
    // Log perf stats every second
5,659✔
1110
    if (this._metricsCounter++ % 60 === 0) {
5,659✔
1111
      this._getMetrics();
113✔
1112
      this.stats.reset();
113✔
1113
      log.table(4, this.metrics)();
113✔
1114

113✔
1115
      // Experimental: report metrics
113✔
1116
      if (this.props._onMetrics) {
113!
1117
        this.props._onMetrics(this.metrics);
×
1118
      }
×
1119
    }
113✔
1120

5,659✔
1121
    this._updateCanvasSize();
5,659✔
1122

5,659✔
1123
    this._updateCursor();
5,659✔
1124

5,659✔
1125
    // Update layers if needed (e.g. some async prop has loaded)
5,659✔
1126
    // Note: This can trigger a redraw
5,659✔
1127
    this.layerManager!.updateLayers();
5,659✔
1128

5,659✔
1129
    // Perform picking request if any
5,659✔
1130
    // TODO(ibgreen): Picking not yet supported on WebGPU
5,659✔
1131
    if (this.device?.type !== 'webgpu') {
5,659✔
1132
      this._pickAndCallback();
5,659✔
1133
    }
5,659✔
1134

5,659✔
1135
    // Redraw if necessary
5,659✔
1136
    this.redraw();
5,659✔
1137

5,659✔
1138
    // Update viewport transition if needed
5,659✔
1139
    // Note: this can trigger `onViewStateChange`, and affect layers
5,659✔
1140
    // We want to defer these changes to the next frame
5,659✔
1141
    if (this.viewManager) {
5,659✔
1142
      this.viewManager.updateViewStates();
5,651✔
1143
    }
5,651✔
1144
  }
5,659✔
1145

1✔
1146
  // Callbacks
1✔
1147

1✔
1148
  private _onViewStateChange(params: ViewStateChangeParameters & {viewId: string}) {
1✔
1149
    // Let app know that view state is changing, and give it a chance to change it
158✔
1150
    const viewState = this.props.onViewStateChange(params) || params.viewState;
158✔
1151

158✔
1152
    // If initialViewState was set on creation, auto track position
158✔
1153
    if (this.viewState) {
158✔
1154
      this.viewState = {...this.viewState, [params.viewId]: viewState};
158✔
1155
      if (!this.props.viewState) {
158✔
1156
        // Apply internal view state
157✔
1157
        if (this.viewManager) {
157✔
1158
          this.viewManager.setProps({viewState: this.viewState});
157✔
1159
        }
157✔
1160
      }
157✔
1161
    }
158✔
1162
  }
158✔
1163

1✔
1164
  private _onInteractionStateChange(interactionState: InteractionState) {
1✔
1165
    this.cursorState.isDragging = interactionState.isDragging || false;
33✔
1166
    this.props.onInteractionStateChange(interactionState);
33✔
1167
  }
33✔
1168

1✔
1169
  /** Internal use only: event handler for click & drag */
1✔
1170
  _onEvent = (event: MjolnirGestureEvent) => {
1✔
1171
    const eventHandlerProp = EVENT_HANDLERS[event.type];
12✔
1172
    const pos = event.offsetCenter;
12✔
1173

12✔
1174
    if (!eventHandlerProp || !pos || !this.layerManager) {
12!
1175
      return;
×
1176
    }
×
1177

12✔
1178
    // Reuse last picked object
12✔
1179
    const layers = this.layerManager.getLayers();
12✔
1180
    const info = this.deckPicker!.getLastPickedObject(
12✔
1181
      {
12✔
1182
        x: pos.x,
12✔
1183
        y: pos.y,
12✔
1184
        layers,
12✔
1185
        viewports: this.getViewports(pos)
12✔
1186
      },
12✔
1187
      this._lastPointerDownInfo
12✔
1188
    ) as PickingInfo;
12✔
1189

12✔
1190
    const {layer} = info;
12✔
1191
    const layerHandler = layer && (layer[eventHandlerProp] || layer.props[eventHandlerProp]);
12!
1192
    const rootHandler = this.props[eventHandlerProp];
12✔
1193
    let handled = false;
12✔
1194

12✔
1195
    if (layerHandler) {
12!
UNCOV
1196
      handled = layerHandler.call(layer, info, event);
×
UNCOV
1197
    }
×
1198
    if (!handled) {
12✔
1199
      rootHandler?.(info, event);
12!
1200
      this.widgetManager!.onEvent(info, event);
12✔
1201
    }
12✔
1202
  };
12✔
1203

1✔
1204
  /** Internal use only: evnet handler for pointerdown */
1✔
1205
  _onPointerDown = (event: MjolnirPointerEvent) => {
1✔
1206
    // TODO(ibgreen) Picking not yet supported on WebGPU
6✔
1207
    if (this.device?.type === 'webgpu') {
6!
1208
      return;
×
1209
    }
×
1210
    const pos = event.offsetCenter;
6✔
1211
    const pickedInfo = this._pick('pickObject', 'pickObject Time', {
6✔
1212
      x: pos.x,
6✔
1213
      y: pos.y,
6✔
1214
      radius: this.props.pickingRadius
6✔
1215
    });
6✔
1216
    this._lastPointerDownInfo = pickedInfo.result[0] || pickedInfo.emptyInfo;
6✔
1217
  };
6✔
1218

1✔
1219
  private _getFrameStats(): void {
1✔
1220
    const {stats} = this;
5,659✔
1221
    stats.get('frameRate').timeEnd();
5,659✔
1222
    stats.get('frameRate').timeStart();
5,659✔
1223

5,659✔
1224
    // Get individual stats from luma.gl so reset works
5,659✔
1225
    const animationLoopStats = this.animationLoop!.stats;
5,659✔
1226
    stats.get('GPU Time').addTime(animationLoopStats.get('GPU Time').lastTiming);
5,659✔
1227
    stats.get('CPU Time').addTime(animationLoopStats.get('CPU Time').lastTiming);
5,659✔
1228
  }
5,659✔
1229

1✔
1230
  private _getMetrics(): void {
1✔
1231
    const {metrics, stats} = this;
113✔
1232
    metrics.fps = stats.get('frameRate').getHz();
113✔
1233
    metrics.setPropsTime = stats.get('setProps Time').time;
113✔
1234
    metrics.updateAttributesTime = stats.get('Update Attributes').time;
113✔
1235
    metrics.framesRedrawn = stats.get('Redraw Count').count;
113✔
1236
    metrics.pickTime =
113✔
1237
      stats.get('pickObject Time').time +
113✔
1238
      stats.get('pickMultipleObjects Time').time +
113✔
1239
      stats.get('pickObjects Time').time;
113✔
1240
    metrics.pickCount = stats.get('Pick Count').count;
113✔
1241

113✔
1242
    // Luma stats
113✔
1243
    metrics.gpuTime = stats.get('GPU Time').time;
113✔
1244
    metrics.cpuTime = stats.get('CPU Time').time;
113✔
1245
    metrics.gpuTimePerFrame = stats.get('GPU Time').getAverageTime();
113✔
1246
    metrics.cpuTimePerFrame = stats.get('CPU Time').getAverageTime();
113✔
1247

113✔
1248
    const memoryStats = luma.stats.get('Memory Usage');
113✔
1249
    metrics.bufferMemory = memoryStats.get('Buffer Memory').count;
113✔
1250
    metrics.textureMemory = memoryStats.get('Texture Memory').count;
113✔
1251
    metrics.renderbufferMemory = memoryStats.get('Renderbuffer Memory').count;
113✔
1252
    metrics.gpuMemory = memoryStats.get('GPU Memory').count;
113✔
1253
  }
113✔
1254
}
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