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

visgl / deck.gl / 13652271063

04 Mar 2025 11:05AM UTC coverage: 91.538% (-0.01%) from 91.548%
13652271063

Pull #9488

github

web-flow
Merge 97caf9bd4 into 1ab778f14
Pull Request #9488: feat(widgets): Simple GeolocateWidget

6695 of 7337 branches covered (91.25%)

Branch coverage included in aggregate %.

10 of 10 new or added lines in 1 file covered. (100.0%)

24 existing lines in 3 files now uncovered.

54205 of 59193 relevant lines covered (91.57%)

14828.9 hits per line

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

95.46
/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 {WidgetManager, Widget} from './widget-manager';
1✔
12
import Tooltip from './tooltip';
1✔
13
import log from '../utils/log';
1✔
14
import {deepEqual} from '../utils/deep-equal';
1✔
15
import typedArrayManager from '../utils/typed-array-manager';
1✔
16
import {VERSION} from './init';
1✔
17

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

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

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

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

1✔
47
/* global document */
1✔
48

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

40✔
374
    // Create a new device
40✔
375
    if (!deviceOrPromise) {
40✔
376
      // Create the "best" device supported from the registered adapters
20✔
377
      deviceOrPromise = luma.createDevice({
20✔
378
        type: 'best-available',
20✔
379
        // luma by default throws if a device is already attached
20✔
380
        // asynchronous device creation could happen after finalize() is called
20✔
381
        // TODO - createDevice should support AbortController?
20✔
382
        _reuseDevices: true,
20✔
383
        adapters: [webgl2Adapter],
20✔
384
        ...props.deviceProps,
20✔
385
        createCanvasContext: {
20✔
386
          canvas: this._createCanvas(props),
20✔
387
          useDevicePixels: this.props.useDevicePixels,
20✔
388
          // TODO v9.2 - replace AnimationLoop's `autoResizeDrawingBuffer` with CanvasContext's `autoResize`
20✔
389
          autoResize: false
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
8✔
436
      this.canvas.parentElement?.removeChild(this.canvas);
8✔
437
      this.canvas = null;
8✔
438
    }
8✔
439
  }
38✔
440

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

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

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

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

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

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

265✔
483
    // If initialized, update sub manager props
265✔
484
    if (this.layerManager) {
265✔
485
      this.viewManager!.setProps(resolvedProps);
201✔
486
      // Make sure that any new layer gets initialized with the current viewport
201✔
487
      this.layerManager.activateViewport(this.getViewports()[0]);
201✔
488
      this.layerManager.setProps(resolvedProps);
201✔
489
      this.effectManager!.setProps(resolvedProps);
201✔
490
      this.deckRenderer!.setProps(resolvedProps);
201✔
491
      this.deckPicker!.setProps(resolvedProps);
201✔
492
      this.widgetManager!.setProps(resolvedProps);
201✔
493
    }
201✔
494

265✔
495
    this.stats.get('setProps Time').timeEnd();
265✔
496
  }
265✔
497

1✔
498
  // Public API
1✔
499

1✔
500
  /**
1✔
501
   * Check if a redraw is needed
1✔
502
   * @returns `false` or a string summarizing the redraw reason
1✔
503
   */
1✔
504
  needsRedraw(
1✔
505
    opts: {
5,580✔
506
      /** Reset the redraw flag afterwards. Default `true` */
5,580✔
507
      clearRedrawFlags: boolean;
5,580✔
508
    } = {clearRedrawFlags: false}
5,580✔
509
  ): false | string {
5,580✔
510
    if (!this.layerManager) {
5,580!
511
      // Not initialized or already finalized
×
512
      return false;
×
513
    }
×
514
    if (this.props._animate) {
5,580!
515
      return 'Deck._animate';
×
516
    }
×
517

5,580✔
518
    let redraw: false | string = this._needsRedraw;
5,580✔
519

5,580✔
520
    if (opts.clearRedrawFlags) {
5,580✔
521
      this._needsRedraw = false;
5,580✔
522
    }
5,580✔
523

5,580✔
524
    const viewManagerNeedsRedraw = this.viewManager!.needsRedraw(opts);
5,580✔
525
    const layerManagerNeedsRedraw = this.layerManager.needsRedraw(opts);
5,580✔
526
    const effectManagerNeedsRedraw = this.effectManager!.needsRedraw(opts);
5,580✔
527
    const deckRendererNeedsRedraw = this.deckRenderer!.needsRedraw(opts);
5,580✔
528

5,580✔
529
    redraw =
5,580✔
530
      redraw ||
5,580✔
531
      viewManagerNeedsRedraw ||
5,553✔
532
      layerManagerNeedsRedraw ||
5,318✔
533
      effectManagerNeedsRedraw ||
5,116✔
534
      deckRendererNeedsRedraw;
5,116✔
535
    return redraw;
5,580✔
536
  }
5,580✔
537

1✔
538
  /**
1✔
539
   * Redraw the GL context
1✔
540
   * @param reason If not provided, only redraw if deemed necessary. Otherwise redraw regardless of internal states.
1✔
541
   * @returns
1✔
542
   */
1✔
543
  redraw(reason?: string): void {
1✔
544
    if (!this.layerManager) {
5,581✔
545
      // Not yet initialized
1✔
546
      return;
1✔
547
    }
1✔
548
    // Check if we need to redraw
5,580✔
549
    let redrawReason = this.needsRedraw({clearRedrawFlags: true});
5,580✔
550
    // User-supplied should take precedent, however the redraw flags get cleared regardless
5,580✔
551
    redrawReason = reason || redrawReason;
5,580✔
552

5,581✔
553
    if (!redrawReason) {
5,581✔
554
      return;
5,116✔
555
    }
5,116✔
556

464✔
557
    this.stats.get('Redraw Count').incrementCount();
464✔
558
    if (this.props._customRender) {
5,581✔
559
      this.props._customRender(redrawReason);
10✔
560
    } else {
5,581✔
561
      this._drawLayers(redrawReason);
454✔
562
    }
454✔
563
  }
5,581✔
564

1✔
565
  /** Flag indicating that the Deck instance has initialized its resources and it's safe to call public methods. */
1✔
566
  get isInitialized(): boolean {
1✔
567
    return this.viewManager !== null;
31✔
568
  }
31✔
569

1✔
570
  /** Get a list of views that are currently rendered */
1✔
571
  getViews(): View[] {
1✔
572
    assert(this.viewManager);
×
573
    return this.viewManager.views;
×
574
  }
×
575

1✔
576
  /** Get a list of viewports that are currently rendered.
1✔
577
   * @param rect If provided, only returns viewports within the given bounding box.
1✔
578
   */
1✔
579
  getViewports(rect?: {x: number; y: number; width?: number; height?: number}): Viewport[] {
1✔
580
    assert(this.viewManager);
350✔
581
    return this.viewManager.getViewports(rect);
350✔
582
  }
350✔
583

1✔
584
  /** Get the current canvas element. */
1✔
585
  getCanvas(): HTMLCanvasElement | null {
1✔
586
    return this.canvas;
149✔
587
  }
149✔
588

1✔
589
  /** Query the object rendered on top at a given point */
1✔
590
  pickObject(opts: {
1✔
591
    /** x position in pixels */
18✔
592
    x: number;
18✔
593
    /** y position in pixels */
18✔
594
    y: number;
18✔
595
    /** Radius of tolerance in pixels. Default `0`. */
18✔
596
    radius?: number;
18✔
597
    /** A list of layer ids to query from. If not specified, then all pickable and visible layers are queried. */
18✔
598
    layerIds?: string[];
18✔
599
    /** If `true`, `info.coordinate` will be a 3D point by unprojecting the `x, y` screen coordinates onto the picked geometry. Default `false`. */
18✔
600
    unproject3D?: boolean;
18✔
601
  }): PickingInfo | null {
18✔
602
    const infos = this._pick('pickObject', 'pickObject Time', opts).result;
18✔
603
    return infos.length ? infos[0] : null;
18✔
604
  }
18✔
605

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

1✔
625
  /* Query all objects rendered on top within a bounding box */
1✔
626
  pickObjects(opts: {
1✔
627
    /** Left of the bounding box in pixels */
18✔
628
    x: number;
18✔
629
    /** Top of the bounding box in pixels */
18✔
630
    y: number;
18✔
631
    /** Width of the bounding box in pixels. Default `1` */
18✔
632
    width?: number;
18✔
633
    /** Height of the bounding box in pixels. Default `1` */
18✔
634
    height?: number;
18✔
635
    /** A list of layer ids to query from. If not specified, then all pickable and visible layers are queried. */
18✔
636
    layerIds?: string[];
18✔
637
    /** If specified, limits the number of objects that can be returned. */
18✔
638
    maxObjects?: number | null;
18✔
639
  }): PickingInfo[] {
18✔
640
    return this._pick('pickObjects', 'pickObjects Time', opts);
18✔
641
  }
18✔
642

1✔
643
  /** Experimental
1✔
644
   * Add a global resource for sharing among layers
1✔
645
   */
1✔
646
  _addResources(
1✔
647
    resources: {
2✔
648
      [id: string]: any;
2✔
649
    },
2✔
650
    forceUpdate = false
2✔
651
  ) {
2✔
652
    for (const id in resources) {
2✔
653
      this.layerManager!.resourceManager.add({resourceId: id, data: resources[id], forceUpdate});
2✔
654
    }
2✔
655
  }
2✔
656

1✔
657
  /** Experimental
1✔
658
   * Remove a global resource
1✔
659
   */
1✔
660
  _removeResources(resourceIds: string[]) {
1✔
661
    for (const id of resourceIds) {
1✔
662
      this.layerManager!.resourceManager.remove(id);
1✔
663
    }
1✔
664
  }
1✔
665

1✔
666
  /** Experimental
1✔
667
   * Register a default effect. Effects will be sorted by order, those with a low order will be rendered first
1✔
668
   */
1✔
669
  _addDefaultEffect(effect: Effect) {
1✔
670
    this.effectManager!.addDefaultEffect(effect);
12✔
671
  }
12✔
672

1✔
673
  _addDefaultShaderModule(module: ShaderModule<Record<string, unknown>>) {
1✔
674
    this.layerManager!.addDefaultShaderModule(module);
2✔
675
  }
2✔
676

1✔
677
  _removeDefaultShaderModule(module: ShaderModule<Record<string, unknown>>) {
1✔
678
    this.layerManager?.removeDefaultShaderModule(module);
2✔
679
  }
2✔
680

1✔
681
  // Private Methods
1✔
682

1✔
683
  private _pick(
1✔
684
    method: 'pickObject',
1✔
685
    statKey: string,
1✔
686
    opts: PickByPointOptions & {layerIds?: string[]}
1✔
687
  ): {
1✔
688
    result: PickingInfo[];
1✔
689
    emptyInfo: PickingInfo;
1✔
690
  };
1✔
691
  private _pick(
1✔
692
    method: 'pickObjects',
1✔
693
    statKey: string,
1✔
694
    opts: PickByRectOptions & {layerIds?: string[]}
1✔
695
  ): PickingInfo[];
1✔
696

1✔
697
  private _pick(
1✔
698
    method: 'pickObject' | 'pickObjects',
67✔
699
    statKey: string,
67✔
700
    opts: (PickByPointOptions | PickByRectOptions) & {layerIds?: string[]}
67✔
701
  ) {
67✔
702
    assert(this.deckPicker);
67✔
703

67✔
704
    const {stats} = this;
67✔
705

67✔
706
    stats.get('Pick Count').incrementCount();
67✔
707
    stats.get(statKey).timeStart();
67✔
708

67✔
709
    const infos = this.deckPicker[method]({
67✔
710
      // layerManager, viewManager and effectManager are always defined if deckPicker is
67✔
711
      layers: this.layerManager!.getLayers(opts),
67✔
712
      views: this.viewManager!.getViews(),
67✔
713
      viewports: this.getViewports(opts),
67✔
714
      onViewportActive: this.layerManager!.activateViewport,
67✔
715
      effects: this.effectManager!.getEffects(),
67✔
716
      ...opts
67✔
717
    });
67✔
718

67✔
719
    stats.get(statKey).timeEnd();
67✔
720

67✔
721
    return infos;
67✔
722
  }
67✔
723

1✔
724
  /** Resolve props.canvas to element */
1✔
725
  private _createCanvas(props: DeckProps<ViewsT>): HTMLCanvasElement {
1✔
726
    let canvas = props.canvas;
20✔
727

20✔
728
    // TODO EventManager should accept element id
20✔
729
    if (typeof canvas === 'string') {
20!
730
      canvas = document.getElementById(canvas) as HTMLCanvasElement;
×
731
      assert(canvas);
×
732
    }
×
733

20✔
734
    if (!canvas) {
20✔
735
      canvas = document.createElement('canvas');
16✔
736
      canvas.id = props.id || 'deckgl-overlay';
16✔
737
      const parent = props.parent || document.body;
16✔
738
      parent.appendChild(canvas);
16✔
739
    }
16✔
740

20✔
741
    Object.assign(canvas.style, props.style);
20✔
742

20✔
743
    return canvas;
20✔
744
  }
20✔
745

1✔
746
  /** Updates canvas width and/or height, if provided as props */
1✔
747
  private _setCanvasSize(props: Required<DeckProps<ViewsT>>): void {
1✔
748
    if (!this.canvas) {
265✔
749
      return;
64✔
750
    }
64✔
751

201✔
752
    const {width, height} = props;
201✔
753
    // Set size ONLY if props are being provided, otherwise let canvas be layouted freely
201✔
754
    if (width || width === 0) {
265✔
755
      const cssWidth = Number.isFinite(width) ? `${width}px` : (width as string);
195✔
756
      this.canvas.style.width = cssWidth;
195✔
757
    }
195✔
758
    if (height || height === 0) {
265✔
759
      const cssHeight = Number.isFinite(height) ? `${height}px` : (height as string);
195✔
760
      // Note: position==='absolute' required for height 100% to work
195✔
761
      this.canvas.style.position = props.style?.position || 'absolute';
195✔
762
      this.canvas.style.height = cssHeight;
195✔
763
    }
195✔
764
  }
265✔
765

1✔
766
  /** If canvas size has changed, reads out the new size and update */
1✔
767
  private _updateCanvasSize(): void {
1✔
768
    const {canvas} = this;
5,609✔
769
    if (!canvas) {
5,609!
770
      return;
×
771
    }
×
772
    // Fallback to width/height when clientWidth/clientHeight are undefined (OffscreenCanvas).
5,609✔
773
    const newWidth = canvas.clientWidth ?? canvas.width;
5,609!
774
    const newHeight = canvas.clientHeight ?? canvas.height;
5,609!
775
    if (newWidth !== this.width || newHeight !== this.height) {
5,609✔
776
      // @ts-expect-error private assign to read-only property
26✔
777
      this.width = newWidth;
26✔
778
      // @ts-expect-error private assign to read-only property
26✔
779
      this.height = newHeight;
26✔
780
      this.viewManager?.setProps({width: newWidth, height: newHeight});
26✔
781
      // Make sure that any new layer gets initialized with the current viewport
26✔
782
      this.layerManager?.activateViewport(this.getViewports()[0]);
26✔
783
      this.props.onResize({width: newWidth, height: newHeight});
26✔
784
    }
26✔
785
  }
5,609✔
786

1✔
787
  private _createAnimationLoop(
1✔
788
    deviceOrPromise: Device | Promise<Device>,
40✔
789
    props: DeckProps<ViewsT>
40✔
790
  ): AnimationLoop {
40✔
791
    const {
40✔
792
      // width,
40✔
793
      // height,
40✔
794
      gl,
40✔
795
      // debug,
40✔
796
      onError,
40✔
797
      // onBeforeRender,
40✔
798
      // onAfterRender,
40✔
799
      useDevicePixels
40✔
800
    } = props;
40✔
801

40✔
802
    return new AnimationLoop({
40✔
803
      device: deviceOrPromise,
40✔
804
      useDevicePixels,
40✔
805
      // TODO v9
40✔
806
      autoResizeDrawingBuffer: !gl, // do not auto resize external context
40✔
807
      autoResizeViewport: false,
40✔
808
      // @ts-expect-error luma.gl needs to accept Promise<void> return value
40✔
809
      onInitialize: context => this._setDevice(context.device),
40✔
810

40✔
811
      onRender: this._onRenderFrame.bind(this),
40✔
812
      // @ts-expect-error typing mismatch: AnimationLoop does not accept onError:null
40✔
813
      onError
40✔
814

40✔
815
      // onBeforeRender,
40✔
816
      // onAfterRender,
40✔
817
    });
40✔
818
  }
40✔
819

1✔
820
  // Get the most relevant view state: props.viewState, if supplied, shadows internal viewState
1✔
821
  // TODO: For backwards compatibility ensure numeric width and height is added to the viewState
1✔
822
  private _getViewState(): ViewStateObject<ViewsT> | null {
1✔
823
    return this.props.viewState || this.viewState;
296✔
824
  }
296✔
825

1✔
826
  // Get the view descriptor list
1✔
827
  private _getViews(): View[] {
1✔
828
    const {views} = this.props;
296✔
829
    const normalizedViews: View[] = Array.isArray(views)
296✔
830
      ? views
156✔
831
      : // If null, default to a full screen map view port
140✔
832
        views
140✔
833
        ? [views]
75✔
834
        : [new MapView({id: 'default-view'})];
65✔
835
    if (normalizedViews.length && this.props.controller) {
296✔
836
      // Backward compatibility: support controller prop
9✔
837
      normalizedViews[0].props.controller = this.props.controller;
9✔
838
    }
9✔
839
    return normalizedViews;
296✔
840
  }
296✔
841

1✔
842
  private _onContextLost() {
1✔
843
    const {onError} = this.props;
×
844
    if (this.animationLoop && onError) {
×
845
      onError(new Error('WebGL context is lost'));
×
846
    }
×
847
  }
×
848

1✔
849
  // The `pointermove` event may fire multiple times in between two animation frames,
1✔
850
  // it's a waste of time to run picking without rerender. Instead we save the last pick
1✔
851
  // request and only do it once on the next animation frame.
1✔
852
  /** Internal use only: event handler for pointerdown */
1✔
853
  _onPointerMove = (event: MjolnirPointerEvent) => {
1✔
854
    const {_pickRequest} = this;
14✔
855
    if (event.type === 'pointerleave') {
14!
856
      _pickRequest.x = -1;
×
857
      _pickRequest.y = -1;
×
858
      _pickRequest.radius = 0;
×
859
    } else if (event.leftButton || event.rightButton) {
14✔
860
      // Do not trigger onHover callbacks if mouse button is down.
6✔
861
      return;
6✔
862
    } else {
14✔
863
      const pos = event.offsetCenter;
8✔
864
      // Do not trigger callbacks when click/hover position is invalid. Doing so will cause a
8✔
865
      // assertion error when attempting to unproject the position.
8✔
866
      if (!pos) {
8!
867
        return;
×
868
      }
×
869
      _pickRequest.x = pos.x;
8✔
870
      _pickRequest.y = pos.y;
8✔
871
      _pickRequest.radius = this.props.pickingRadius;
8✔
872
    }
8✔
873

8✔
874
    if (this.layerManager) {
8✔
875
      this.layerManager.context.mousePosition = {x: _pickRequest.x, y: _pickRequest.y};
8✔
876
    }
8✔
877

8✔
878
    _pickRequest.event = event;
8✔
879
  };
14✔
880

1✔
881
  /** Actually run picking */
1✔
882
  private _pickAndCallback() {
1✔
883
    if (this.device?.type === 'webgpu') {
5,578!
UNCOV
884
      return;
×
UNCOV
885
    }
×
886

5,578✔
887
    const {_pickRequest} = this;
5,578✔
888

5,578✔
889
    if (_pickRequest.event) {
5,578✔
890
      // Perform picking
8✔
891
      const {result, emptyInfo} = this._pick('pickObject', 'pickObject Time', _pickRequest);
8✔
892
      this.cursorState.isHovering = result.length > 0;
8✔
893

8✔
894
      // There are 4 possible scenarios:
8✔
895
      // result is [outInfo, pickedInfo] (moved from one pickable layer to another)
8✔
896
      // result is [outInfo] (moved outside of a pickable layer)
8✔
897
      // result is [pickedInfo] (moved into or over a pickable layer)
8✔
898
      // result is [] (nothing is or was picked)
8✔
899
      //
8✔
900
      // `layer.props.onHover` should be called on all affected layers (out/over)
8✔
901
      // `deck.props.onHover` should be called with the picked info if any, or empty info otherwise
8✔
902
      // `deck.props.getTooltip` should be called with the picked info if any, or empty info otherwise
8✔
903

8✔
904
      // Execute callbacks
8✔
905
      let pickedInfo = emptyInfo;
8✔
906
      let handled = false;
8✔
907
      for (const info of result) {
8✔
908
        pickedInfo = info;
2✔
909
        handled = info.layer?.onHover(info, _pickRequest.event) || handled;
2✔
910
      }
2✔
911
      if (!handled) {
8✔
912
        this.props.onHover?.(pickedInfo, _pickRequest.event);
8✔
913
        this.widgetManager!.onHover(pickedInfo, _pickRequest.event);
8✔
914
      }
8✔
915

8✔
916
      // Clear pending pickRequest
8✔
917
      _pickRequest.event = null;
8✔
918
    }
8✔
919
  }
5,578✔
920

1✔
921
  private _updateCursor(): void {
1✔
922
    const container = this.props.parent || this.canvas;
5,578✔
923
    if (container) {
5,578✔
924
      container.style.cursor = this.props.getCursor(this.cursorState);
5,578✔
925
    }
5,578✔
926
  }
5,578✔
927

1✔
928
  private _setDevice(device: Device) {
1✔
929
    this.device = device;
40✔
930

40✔
931
    if (!this.animationLoop) {
40✔
932
      // finalize() has been called
9✔
933
      return;
9✔
934
    }
9✔
935

31✔
936
    // if external context...
31✔
937
    if (!this.canvas) {
31✔
938
      this.canvas = this.device.canvasContext?.canvas as HTMLCanvasElement;
31✔
939
      // TODO v9
31✔
940
      // ts-expect-error - Currently luma.gl v9 does not expose these options
31✔
941
      // All WebGLDevice contexts are instrumented, but it seems the device
31✔
942
      // should have a method to start state tracking even if not enabled?
31✔
943
      // instrumentGLContext(this.device.gl, {enable: true, copyState: true});
31✔
944
    }
31✔
945

31✔
946
    if (this.device.type === 'webgl') {
31✔
947
      this.device.setParametersWebGL({
31✔
948
        blend: true,
31✔
949
        blendFunc: [GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA, GL.ONE, GL.ONE_MINUS_SRC_ALPHA],
31✔
950
        polygonOffsetFill: true,
31✔
951
        depthTest: true,
31✔
952
        depthFunc: GL.LEQUAL
31✔
953
      });
31✔
954
    }
31✔
955

31✔
956
    this.props.onDeviceInitialized(this.device);
31✔
957
    if (this.device.type === 'webgl') {
31✔
958
      // Legacy callback - warn?
31✔
959
      // @ts-expect-error gl is not visible on Device base class
31✔
960
      this.props.onWebGLInitialized(this.device.gl);
31✔
961
    }
31✔
962

31✔
963
    // timeline for transitions
31✔
964
    const timeline = new Timeline();
31✔
965
    timeline.play();
31✔
966
    this.animationLoop.attachTimeline(timeline);
31✔
967

31✔
968
    this.eventManager = new EventManager(this.props.parent || this.canvas, {
40✔
969
      touchAction: this.props.touchAction,
40✔
970
      recognizers: Object.keys(RECOGNIZERS).map((eventName: string) => {
40✔
971
        // Resolve recognizer settings
155✔
972
        const [RecognizerConstructor, defaultOptions, recognizeWith, requestFailure] =
155✔
973
          RECOGNIZERS[eventName];
155✔
974
        const optionsOverride = this.props.eventRecognizerOptions?.[eventName];
155✔
975
        const options = {...defaultOptions, ...optionsOverride, event: eventName};
155✔
976
        return {
155✔
977
          recognizer: new RecognizerConstructor(options),
155✔
978
          recognizeWith,
155✔
979
          requestFailure
155✔
980
        };
155✔
981
      }),
40✔
982
      events: {
40✔
983
        pointerdown: this._onPointerDown,
40✔
984
        pointermove: this._onPointerMove,
40✔
985
        pointerleave: this._onPointerMove
40✔
986
      }
40✔
987
    });
40✔
988
    for (const eventType in EVENT_HANDLERS) {
40✔
989
      this.eventManager.on(eventType, this._onEvent);
124✔
990
    }
124✔
991

31✔
992
    this.viewManager = new ViewManager({
31✔
993
      timeline,
31✔
994
      eventManager: this.eventManager,
31✔
995
      onViewStateChange: this._onViewStateChange.bind(this),
31✔
996
      onInteractionStateChange: this._onInteractionStateChange.bind(this),
31✔
997
      views: this._getViews(),
31✔
998
      viewState: this._getViewState(),
31✔
999
      width: this.width,
31✔
1000
      height: this.height
31✔
1001
    });
31✔
1002

31✔
1003
    // viewManager must be initialized before layerManager
31✔
1004
    // layerManager depends on viewport created by viewManager.
31✔
1005
    const viewport = this.viewManager.getViewports()[0];
31✔
1006

31✔
1007
    // Note: avoid React setState due GL animation loop / setState timing issue
31✔
1008
    this.layerManager = new LayerManager(this.device, {
31✔
1009
      deck: this,
31✔
1010
      stats: this.stats,
31✔
1011
      viewport,
31✔
1012
      timeline
31✔
1013
    });
31✔
1014

31✔
1015
    this.effectManager = new EffectManager({
31✔
1016
      deck: this,
31✔
1017
      device: this.device
31✔
1018
    });
31✔
1019

31✔
1020
    this.deckRenderer = new DeckRenderer(this.device);
31✔
1021

31✔
1022
    this.deckPicker = new DeckPicker(this.device);
31✔
1023

31✔
1024
    this.widgetManager = new WidgetManager({
31✔
1025
      deck: this,
31✔
1026
      parentElement: this.canvas?.parentElement
31✔
1027
    });
40✔
1028
    this.widgetManager.addDefault(new Tooltip());
40✔
1029

40✔
1030
    this.setProps(this.props);
40✔
1031

40✔
1032
    this._updateCanvasSize();
40✔
1033
    this.props.onLoad();
40✔
1034
  }
40✔
1035

1✔
1036
  /** Internal only: default render function (redraw all layers and views) */
1✔
1037
  _drawLayers(
1✔
1038
    redrawReason: string,
464✔
1039
    renderOptions?: {
464✔
1040
      target?: Framebuffer;
464✔
1041
      layerFilter?: (context: FilterContext) => boolean;
464✔
1042
      layers?: Layer[];
464✔
1043
      viewports?: Viewport[];
464✔
1044
      views?: {[viewId: string]: View};
464✔
1045
      pass?: string;
464✔
1046
      effects?: Effect[];
464✔
1047
      clearStack?: boolean;
464✔
1048
      clearCanvas?: boolean;
464✔
1049
    }
464✔
1050
  ) {
464✔
1051
    const {device, gl} = this.layerManager!.context;
464✔
1052

464✔
1053
    this.props.onBeforeRender({device, gl});
464✔
1054

464✔
1055
    const opts = {
464✔
1056
      target: this.props._framebuffer,
464✔
1057
      layers: this.layerManager!.getLayers(),
464✔
1058
      viewports: this.viewManager!.getViewports(),
464✔
1059
      onViewportActive: this.layerManager!.activateViewport,
464✔
1060
      views: this.viewManager!.getViews(),
464✔
1061
      pass: 'screen',
464✔
1062
      effects: this.effectManager!.getEffects(),
464✔
1063
      ...renderOptions
464✔
1064
    };
464✔
1065
    this.deckRenderer?.renderLayers(opts);
464✔
1066

464✔
1067
    if (opts.pass === 'screen') {
464✔
1068
      // This method could be called when drawing to picking buffer, texture etc.
464✔
1069
      // Only when drawing to screen, update all widgets (UI components)
464✔
1070
      this.widgetManager!.onRedraw({
464✔
1071
        viewports: opts.viewports,
464✔
1072
        layers: opts.layers
464✔
1073
      });
464✔
1074
    }
464✔
1075

464✔
1076
    this.props.onAfterRender({device, gl});
464✔
1077
  }
464✔
1078

1✔
1079
  // Callbacks
1✔
1080

1✔
1081
  private _onRenderFrame() {
1✔
1082
    this._getFrameStats();
5,578✔
1083

5,578✔
1084
    // Log perf stats every second
5,578✔
1085
    if (this._metricsCounter++ % 60 === 0) {
5,578✔
1086
      this._getMetrics();
116✔
1087
      this.stats.reset();
116✔
1088
      log.table(4, this.metrics)();
116✔
1089

116✔
1090
      // Experimental: report metrics
116✔
1091
      if (this.props._onMetrics) {
116!
UNCOV
1092
        this.props._onMetrics(this.metrics);
×
UNCOV
1093
      }
×
1094
    }
116✔
1095

5,578✔
1096
    this._updateCanvasSize();
5,578✔
1097

5,578✔
1098
    this._updateCursor();
5,578✔
1099

5,578✔
1100
    // Update layers if needed (e.g. some async prop has loaded)
5,578✔
1101
    // Note: This can trigger a redraw
5,578✔
1102
    this.layerManager!.updateLayers();
5,578✔
1103

5,578✔
1104
    // Perform picking request if any
5,578✔
1105
    // TODO(ibgreen): Picking not yet supported on WebGPU
5,578✔
1106
    if (this.device?.type !== 'webgpu') {
5,578✔
1107
      this._pickAndCallback();
5,578✔
1108
    }
5,578✔
1109

5,578✔
1110
    // Redraw if necessary
5,578✔
1111
    this.redraw();
5,578✔
1112

5,578✔
1113
    // Update viewport transition if needed
5,578✔
1114
    // Note: this can trigger `onViewStateChange`, and affect layers
5,578✔
1115
    // We want to defer these changes to the next frame
5,578✔
1116
    if (this.viewManager) {
5,578✔
1117
      this.viewManager.updateViewStates();
5,570✔
1118
    }
5,570✔
1119
  }
5,578✔
1120

1✔
1121
  // Callbacks
1✔
1122

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

159✔
1127
    // If initialViewState was set on creation, auto track position
159✔
1128
    if (this.viewState) {
159✔
1129
      this.viewState = {...this.viewState, [params.viewId]: viewState};
159✔
1130
      if (!this.props.viewState) {
159✔
1131
        // Apply internal view state
158✔
1132
        if (this.viewManager) {
158✔
1133
          this.viewManager.setProps({viewState: this.viewState});
158✔
1134
        }
158✔
1135
      }
158✔
1136
    }
159✔
1137
  }
159✔
1138

1✔
1139
  private _onInteractionStateChange(interactionState: InteractionState) {
1✔
1140
    this.cursorState.isDragging = interactionState.isDragging || false;
33✔
1141
    this.props.onInteractionStateChange(interactionState);
33✔
1142
  }
33✔
1143

1✔
1144
  /** Internal use only: event handler for click & drag */
1✔
1145
  _onEvent = (event: MjolnirGestureEvent) => {
1✔
1146
    const eventHandlerProp = EVENT_HANDLERS[event.type];
11✔
1147
    const pos = event.offsetCenter;
11✔
1148

11✔
1149
    if (!eventHandlerProp || !pos || !this.layerManager) {
11!
UNCOV
1150
      return;
×
UNCOV
1151
    }
×
1152

11✔
1153
    // Reuse last picked object
11✔
1154
    const layers = this.layerManager.getLayers();
11✔
1155
    const info = this.deckPicker!.getLastPickedObject(
11✔
1156
      {
11✔
1157
        x: pos.x,
11✔
1158
        y: pos.y,
11✔
1159
        layers,
11✔
1160
        viewports: this.getViewports(pos)
11✔
1161
      },
11✔
1162
      this._lastPointerDownInfo
11✔
1163
    ) as PickingInfo;
11✔
1164

11✔
1165
    const {layer} = info;
11✔
1166
    const layerHandler = layer && (layer[eventHandlerProp] || layer.props[eventHandlerProp]);
11!
1167
    const rootHandler = this.props[eventHandlerProp];
11✔
1168
    let handled = false;
11✔
1169

11✔
1170
    if (layerHandler) {
11✔
1171
      handled = layerHandler.call(layer, info, event);
1✔
1172
    }
1✔
1173
    if (!handled) {
11✔
1174
      rootHandler?.(info, event);
11✔
1175
      this.widgetManager!.onEvent(info, event);
11✔
1176
    }
11✔
1177
  };
11✔
1178

1✔
1179
  /** Internal use only: evnet handler for pointerdown */
1✔
1180
  _onPointerDown = (event: MjolnirPointerEvent) => {
1✔
1181
    // TODO(ibgreen) Picking not yet supported on WebGPU
7✔
1182
    if (this.device?.type === 'webgpu') {
7!
UNCOV
1183
      return;
×
UNCOV
1184
    }
×
1185
    const pos = event.offsetCenter;
7✔
1186
    const pickedInfo = this._pick('pickObject', 'pickObject Time', {
7✔
1187
      x: pos.x,
7✔
1188
      y: pos.y,
7✔
1189
      radius: this.props.pickingRadius
7✔
1190
    });
7✔
1191
    this._lastPointerDownInfo = pickedInfo.result[0] || pickedInfo.emptyInfo;
7✔
1192
  };
7✔
1193

1✔
1194
  private _getFrameStats(): void {
1✔
1195
    const {stats} = this;
5,578✔
1196
    stats.get('frameRate').timeEnd();
5,578✔
1197
    stats.get('frameRate').timeStart();
5,578✔
1198

5,578✔
1199
    // Get individual stats from luma.gl so reset works
5,578✔
1200
    const animationLoopStats = this.animationLoop!.stats;
5,578✔
1201
    stats.get('GPU Time').addTime(animationLoopStats.get('GPU Time').lastTiming);
5,578✔
1202
    stats.get('CPU Time').addTime(animationLoopStats.get('CPU Time').lastTiming);
5,578✔
1203
  }
5,578✔
1204

1✔
1205
  private _getMetrics(): void {
1✔
1206
    const {metrics, stats} = this;
116✔
1207
    metrics.fps = stats.get('frameRate').getHz();
116✔
1208
    metrics.setPropsTime = stats.get('setProps Time').time;
116✔
1209
    metrics.updateAttributesTime = stats.get('Update Attributes').time;
116✔
1210
    metrics.framesRedrawn = stats.get('Redraw Count').count;
116✔
1211
    metrics.pickTime =
116✔
1212
      stats.get('pickObject Time').time +
116✔
1213
      stats.get('pickMultipleObjects Time').time +
116✔
1214
      stats.get('pickObjects Time').time;
116✔
1215
    metrics.pickCount = stats.get('Pick Count').count;
116✔
1216

116✔
1217
    // Luma stats
116✔
1218
    metrics.gpuTime = stats.get('GPU Time').time;
116✔
1219
    metrics.cpuTime = stats.get('CPU Time').time;
116✔
1220
    metrics.gpuTimePerFrame = stats.get('GPU Time').getAverageTime();
116✔
1221
    metrics.cpuTimePerFrame = stats.get('CPU Time').getAverageTime();
116✔
1222

116✔
1223
    const memoryStats = luma.stats.get('Memory Usage');
116✔
1224
    metrics.bufferMemory = memoryStats.get('Buffer Memory').count;
116✔
1225
    metrics.textureMemory = memoryStats.get('Texture Memory').count;
116✔
1226
    metrics.renderbufferMemory = memoryStats.get('Renderbuffer Memory').count;
116✔
1227
    metrics.gpuMemory = memoryStats.get('GPU Memory').count;
116✔
1228
  }
116✔
1229
}
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