isard 1.16.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +5 -5
  3. package/dist/basemap/vectortiles/placed.js +7 -4
  4. package/dist/basemap/vectortiles/placed.js.map +1 -1
  5. package/dist/basemap/vectortiles/tiles.d.ts +1 -1
  6. package/dist/basemap/vectortiles/tiles.js.map +1 -1
  7. package/dist/basemap/vectortiles/vectors.d.ts +3 -1
  8. package/dist/basemap/vectortiles/vectors.js +2 -0
  9. package/dist/basemap/vectortiles/vectors.js.map +1 -1
  10. package/dist/basemap/vectortiles.d.ts +1 -3
  11. package/dist/basemap/vectortiles.js +1 -3
  12. package/dist/basemap/vectortiles.js.map +1 -1
  13. package/dist/basemap/workerpool.d.ts +1 -1
  14. package/dist/basemap/workerpool.js.map +1 -1
  15. package/dist/bind/layers.js +2 -2
  16. package/dist/bind/layers.js.map +1 -1
  17. package/dist/decoder.worker.js +4 -5
  18. package/dist/decoder.worker.js.map +6 -6
  19. package/dist/decoding/decoders.d.ts +1 -1
  20. package/dist/decoding/decoders.js.map +1 -1
  21. package/dist/decoding/loader.d.ts +6 -3
  22. package/dist/decoding/loader.js +7 -5
  23. package/dist/decoding/loader.js.map +1 -1
  24. package/dist/decoding/protocol.d.ts +3 -2
  25. package/dist/decoding/protocol.js.map +1 -1
  26. package/dist/engine-loader.d.ts +20 -18
  27. package/dist/engine-loader.js +45 -42
  28. package/dist/engine-loader.js.map +1 -1
  29. package/dist/index.d.ts +3 -3
  30. package/dist/index.js.map +1 -1
  31. package/dist/isard_grid_bg.wasm +0 -0
  32. package/dist/isard_tess_bg.wasm +0 -0
  33. package/dist/isard_tiles3d_bg.wasm +0 -0
  34. package/dist/isard_webgl2_bg.wasm +0 -0
  35. package/dist/isard_webgpu_bg.wasm +0 -0
  36. package/dist/layers/custom.d.ts +35 -29
  37. package/dist/layers/custom.js +60 -46
  38. package/dist/layers/custom.js.map +1 -1
  39. package/dist/layers/extrusions.d.ts +6 -1
  40. package/dist/layers/extrusions.js +15 -4
  41. package/dist/layers/extrusions.js.map +1 -1
  42. package/dist/layers/features.js.map +1 -1
  43. package/dist/layers/flow.js.map +1 -1
  44. package/dist/layers/glyphs.d.ts +1 -1
  45. package/dist/layers/glyphs.js.map +1 -1
  46. package/dist/layers/ground.d.ts +21 -14
  47. package/dist/layers/ground.js +30 -9
  48. package/dist/layers/ground.js.map +1 -1
  49. package/dist/layers/icons.d.ts +1 -1
  50. package/dist/layers/icons.js.map +1 -1
  51. package/dist/layers/layers.d.ts +1 -2
  52. package/dist/layers/layers.js +3 -12
  53. package/dist/layers/layers.js.map +1 -1
  54. package/dist/layers/models.d.ts +7 -1
  55. package/dist/layers/models.js +19 -5
  56. package/dist/layers/models.js.map +1 -1
  57. package/dist/layers/network.d.ts +1 -1
  58. package/dist/layers/network.js.map +1 -1
  59. package/dist/layers/panoramas.d.ts +1 -0
  60. package/dist/layers/panoramas.js +3 -2
  61. package/dist/layers/panoramas.js.map +1 -1
  62. package/dist/layers/pointclouds.d.ts +3 -3
  63. package/dist/layers/pointclouds.js +3 -7
  64. package/dist/layers/pointclouds.js.map +1 -1
  65. package/dist/layers/raster.js +5 -0
  66. package/dist/layers/raster.js.map +1 -1
  67. package/dist/layers/symbols.js.map +1 -1
  68. package/dist/layers/tiles3d.d.ts +5 -7
  69. package/dist/layers/tiles3d.js +9 -18
  70. package/dist/layers/tiles3d.js.map +1 -1
  71. package/dist/layers/vectors.d.ts +10 -5
  72. package/dist/layers/vectors.js +25 -16
  73. package/dist/layers/vectors.js.map +1 -1
  74. package/dist/map/engine-settings.d.ts +5 -2
  75. package/dist/map/engine-settings.js +13 -2
  76. package/dist/map/engine-settings.js.map +1 -1
  77. package/dist/map/engines.d.ts +11 -26
  78. package/dist/map/engines.js +10 -48
  79. package/dist/map/engines.js.map +1 -1
  80. package/dist/map/frame-loop.d.ts +1 -3
  81. package/dist/map/frame-loop.js +3 -3
  82. package/dist/map/frame-loop.js.map +1 -1
  83. package/dist/map/hover-pick.d.ts +9 -3
  84. package/dist/map/hover-pick.js +29 -6
  85. package/dist/map/hover-pick.js.map +1 -1
  86. package/dist/map/layer-stack.d.ts +7 -8
  87. package/dist/map/layer-stack.js +6 -9
  88. package/dist/map/layer-stack.js.map +1 -1
  89. package/dist/map/symbol-atlases.d.ts +5 -4
  90. package/dist/map/symbol-atlases.js +8 -13
  91. package/dist/map/symbol-atlases.js.map +1 -1
  92. package/dist/map/viewshed.d.ts +1 -1
  93. package/dist/map/viewshed.js.map +1 -1
  94. package/dist/map.d.ts +45 -26
  95. package/dist/map.js +141 -90
  96. package/dist/map.js.map +1 -1
  97. package/dist/react/IsardMap.d.ts +1 -1
  98. package/dist/react/IsardMap.js.map +1 -1
  99. package/dist/react/VectorGeometry.d.ts +4 -1
  100. package/dist/react/VectorGeometry.js +5 -1
  101. package/dist/react/VectorGeometry.js.map +1 -1
  102. package/dist/react/index.d.ts +1 -1
  103. package/dist/react/index.js.map +1 -1
  104. package/dist/settings.d.ts +2 -0
  105. package/dist/settings.js +1 -1
  106. package/dist/settings.js.map +1 -1
  107. package/dist/types.d.ts +52 -22
  108. package/dist/types.js.map +1 -1
  109. package/dist/ui/interaction.d.ts +1 -1
  110. package/dist/ui/interaction.js.map +1 -1
  111. package/dist/util/animation.d.ts +1 -1
  112. package/dist/util/animation.js.map +1 -1
  113. package/dist/wasm/isard_tiles3d.d.ts +181 -0
  114. package/dist/wasm/isard_tiles3d.js +387 -0
  115. package/dist/wasm/{isard_core.d.ts → isard_webgl2.d.ts} +387 -182
  116. package/dist/wasm/{isard_core.js → isard_webgl2.js} +2087 -324
  117. package/dist/wasm/{isard.d.ts → isard_webgpu.d.ts} +195 -358
  118. package/dist/wasm/{isard.js → isard_webgpu.js} +1320 -1042
  119. package/package.json +4 -3
  120. package/vite.js +5 -4
  121. package/dist/isard_bg.wasm +0 -0
  122. package/dist/isard_core_bg.wasm +0 -0
package/dist/map.js CHANGED
@@ -9,7 +9,7 @@ import { SEA } from "./basemap/basemaps.js";
9
9
  import { pmtilesLayer } from "./basemap/pmtileslayer.js";
10
10
  import { VectorTiles } from "./basemap/vectortiles.js";
11
11
  import { defaultWorkerUrl } from "./basemap/workerpool.js";
12
- import { destroyedEngine, engineUrl, gridUrl, loadEngine, tessUrl } from "./engine-loader.js";
12
+ import { destroyedEngine, engineUrl, gridUrl, loadEngine, rendererKind, tessUrl, } from "./engine-loader.js";
13
13
  import { IGN_PRESETS } from "./geoplateforme.js";
14
14
  import { CustomLayer } from "./layers/custom.js";
15
15
  import { ExtrusionLayer, extrusionStyle } from "./layers/extrusions.js";
@@ -34,7 +34,7 @@ import { mapSettings, sendHeaders } from "./map/engine-settings.js";
34
34
  import { MapEngines } from "./map/engines.js";
35
35
  import { MapListeners } from "./map/events.js";
36
36
  import { FrameLoop } from "./map/frame-loop.js";
37
- import { HoverPick } from "./map/hover-pick.js";
37
+ import { HoverPick, pickOnce } from "./map/hover-pick.js";
38
38
  import { addSpec } from "./map/layer-spec.js";
39
39
  import { LayerStack } from "./map/layer-stack.js";
40
40
  import { OverlayHost } from "./map/overlay-host.js";
@@ -67,12 +67,22 @@ const NETWORK_HALO_OPACITY = 0.11;
67
67
  /** A layer whose picked features come back with their properties (lines, circles, fills, extrusions, 3D Tiles…) */
68
68
  const hasFeatures = (layer) => 'featureAt' in layer && typeof layer.featureAt === 'function';
69
69
  /** A map (see `GeoMap.create`); layers take it as their host */
70
+ /** The parts of a frame by their code in the engine's GPU timings (`Engine.gpuTimings`) */
71
+ const GPU_PARTS = ['backdrop', 'ground', 'layer', 'shadows', 'panorama', 'viewshed'];
72
+ /** Turns the GPU timing of an engine on or off: whether it times, false for an engine built without it (only a build to
73
+ * profile has `setGpuTiming`) */
74
+ const engineGpuTiming = (engine, on) => {
75
+ const set = Reflect.get(engine, 'setGpuTiming');
76
+ return typeof set === 'function' && Reflect.apply(set, engine, [on]) === true;
77
+ };
70
78
  export class GeoMap {
71
79
  canvas;
72
80
  #engine;
81
+ /** The frames' parts timed on the GPU (`setGpuTiming`), again on a new engine */
82
+ gpuTiming = false;
73
83
  /** The work the map waits on before it is idle (`track`) */
74
84
  #working = 0;
75
- /** The engine class (core, or with 3D Tiles) and the engine with 3D Tiles loaded when needed */
85
+ /** The engine, made asynchronously, and the workers decoding its 3D Tiles */
76
86
  engines;
77
87
  /** The layers in drawing order and those kept on top */
78
88
  stack = new LayerStack(() => this.#engine);
@@ -90,7 +100,7 @@ export class GeoMap {
90
100
  tessUrl;
91
101
  /** @internal Where the workers load the grid module from (`isard_grid_bg.wasm`) */
92
102
  gridUrl;
93
- /** The WebGL context is lost: nothing is drawn until it is restored */
103
+ /** The GPU is lost (the WebGL context, the WebGPU device): nothing is drawn until a new engine is there */
94
104
  lost = false;
95
105
  gone = false;
96
106
  listeners = new MapListeners();
@@ -124,20 +134,25 @@ export class GeoMap {
124
134
  static async create(container, given = {}) {
125
135
  // A preset under the options given
126
136
  const options = given.preset ? { ...IGN_PRESETS[given.preset], ...given } : given;
127
- // With 3D Tiles from the start when asked, or when only that engine's URL is given (as before the core engine)
128
- const full = options.tiles3d === true || (options.wasmUrl !== undefined && options.coreWasmUrl === undefined);
129
137
  // Before anything is created
130
138
  if (options.basemap !== undefined)
131
139
  checkBasemap(options.basemap);
132
- const kind = full ? 'full' : 'core';
140
+ // The engine on wgpu (#146): WebGPU, else wgpu's WebGL2 fallback
141
+ const kind = await rendererKind(options.renderer);
133
142
  const bindings = await loadEngine(kind, engineUrl(kind, options));
134
- const map = new GeoMap(container, options, bindings.Engine, full ? bindings.module : undefined);
135
- if (options.tiles3d === 'preload' && !full)
136
- map.preloadTiles3d();
143
+ const map = new GeoMap(container, options, bindings.Engine);
144
+ // Its device asked for: the map started on a placeholder meanwhile, its layers created on the engine once it is
145
+ // there (as after a context loss)
146
+ map.adopt(await map.engines.create(map.canvas, () => map.requestRender(), map.settings));
147
+ // A profiling script's page (`window.__isardProfile` set before isard loads): its maps, to time
148
+ if (Reflect.get(globalThis, '__isardProfile')) {
149
+ const maps = Reflect.get(globalThis, '__isardMaps');
150
+ Reflect.set(globalThis, '__isardMaps', [...(Array.isArray(maps) ? maps : []), map]);
151
+ }
137
152
  return map;
138
153
  }
139
- constructor(container, options, engineClass, tiles3dModule) {
140
- this.engines = new MapEngines(engineClass, tiles3dModule, options, () => this.gone);
154
+ constructor(container, options, engineClass) {
155
+ this.engines = new MapEngines(engineClass, options);
141
156
  const workerUrl = options.basemapWorkerUrl ?? defaultWorkerUrl(options.assetsUrl);
142
157
  this.tessUrl = tessUrl(options);
143
158
  this.gridUrl = gridUrl(options);
@@ -145,7 +160,8 @@ export class GeoMap {
145
160
  this.basemaps = new BasemapLayers(this, this.stack, workerUrl, options.basemapTileSize);
146
161
  this.container = container;
147
162
  this.canvas = createCanvas();
148
- this.atlases = new SymbolAtlases(this.canvas, options.glyphs, () => this.#engine);
163
+ // The engine takes the canvas for itself: it says how large a texture its device takes
164
+ this.atlases = new SymbolAtlases(() => this.#engine.maxTextureSize(), options.glyphs, () => this.#engine);
149
165
  this.overlays = new OverlayHost(container, this.canvas, this);
150
166
  this.earth = (options.body ?? 'earth') === 'earth';
151
167
  this.body = options.body;
@@ -156,7 +172,6 @@ export class GeoMap {
156
172
  lost: () => this.lost,
157
173
  interacting: () => this.interaction?.active === true,
158
174
  customLayers: () => this.layers.some((layer) => layer instanceof CustomLayer),
159
- upgrading: () => this.engines.upgrading !== undefined,
160
175
  working: () => this.#working > 0,
161
176
  drawn: (hovered) => {
162
177
  this.qualityWatch?.frame(performance.now(), this.movingNow);
@@ -167,15 +182,13 @@ export class GeoMap {
167
182
  this.overlays.positionAll();
168
183
  },
169
184
  }, this.listeners);
170
- this.#engine = this.createEngine();
185
+ // The engine is made asynchronously (`GeoMap.create` adopts it): a placeholder meanwhile
186
+ this.#engine = destroyedEngine(EARTH_START);
171
187
  // Once it has an engine: a map whose creation failed before is not left following them
172
188
  this.unwatchHeaders = onHeaderRules((rules) => sendHeaders(this.#engine, rules));
173
- // The browser may take the GPU away (driver reset, background tab on mobile): the map waits, then rebuilds
174
- this.unwatchContext = watchContext(this.canvas, () => {
175
- this.lost = true;
176
- this.frames.cancel();
177
- this.dropLostEngine();
178
- }, () => this.restore());
189
+ // The browser may take the GPU away (driver reset, background tab on mobile): the map waits, then rebuilds. A
190
+ // WebGPU device lost is watched with each engine (`adopt`)
191
+ this.unwatchContext = watchContext(this.canvas, () => this.gpuLost(), () => this.restore());
179
192
  const start = this.earth ? EARTH_START : BODY_START;
180
193
  this.setCamera({ ...start, ...options });
181
194
  this.setBasemap(options.basemap ?? (this.earth ? 'osm-positron' : 'none'), options.hillshade ?? 0);
@@ -224,33 +237,26 @@ export class GeoMap {
224
237
  get onTop() {
225
238
  return this.stack.onTop;
226
239
  }
227
- createEngine() {
228
- return this.engines.create(this.canvas, () => this.requestRender(), this.settings);
229
- }
230
- /** A 3D Tiles layer or a model on the core engine: loads the engine with 3D Tiles, then rebuilds the map on it
231
- * (camera, settings and layers kept, as after a context loss); the layer is created then */
232
- upgrade() {
233
- this.engines.upgrade(() => {
234
- // A lost context: the engine is rebuilt on it once restored
235
- if (!this.lost)
236
- this.restore();
237
- }, (error) => {
238
- // The 3D Tiles layers stay empty; the next one added tries again
239
- report(error);
240
- this.requestRender();
241
- });
240
+ /** The GPU is lost: nothing drawn, the engine dropped (`dropLostEngine`) */
241
+ gpuLost() {
242
+ this.lost = true;
243
+ this.frames.cancel();
244
+ this.dropLostEngine();
242
245
  }
243
- /** Downloads and compiles the engine with 3D Tiles once the map is idle (`tiles3d: 'preload'`), without running
244
- * it: the first 3D Tiles layer then only instantiates it */
245
- preloadTiles3d() {
246
- if (this.frames.settled)
247
- this.engines.preload();
248
- else {
249
- const off = this.on('idle', () => {
250
- off();
251
- this.engines.preload();
252
- });
253
- }
246
+ /**
247
+ * Follows the loss of `engine`'s WebGPU device (a phone taking its memory back, a driver reset, the GPU process
248
+ * gone): while it is the map's, the map drops it and makes a new one at once, WebGPU having no event for the GPU
249
+ * given back. The device of an engine the map freed itself (replaced, the map destroyed) is lost too: not the map's
250
+ * any more, left be
251
+ */
252
+ watchDevice(engine) {
253
+ void engine.gpuDevice()?.lost.then((info) => {
254
+ if (this.gone || this.#engine !== engine)
255
+ return;
256
+ console.warn(`isard: the GPU device was lost (${info.reason}: ${info.message}), the map made again`);
257
+ this.gpuLost();
258
+ this.restore();
259
+ });
254
260
  }
255
261
  /**
256
262
  * The context is lost: its engine freed at once, the map's calls going to one that does nothing until `restore`
@@ -266,19 +272,35 @@ export class GeoMap {
266
272
  // Glyph ranges and icons arriving meanwhile upload to it, not to the freed engine
267
273
  this.atlases.attach(this.#engine);
268
274
  }
269
- /** After a context loss: a new engine with the same camera, settings and layers (same handles, same order) */
275
+ /** After the GPU lost: a new engine with the same camera, settings and layers (same handles, same order); the engine
276
+ * made asynchronously, adopted once there */
270
277
  restore() {
271
278
  if (this.gone)
272
279
  return;
280
+ this.engines
281
+ .create(this.canvas, () => this.requestRender(), this.settings)
282
+ .then((engine) => this.adopt(engine))
283
+ .catch(report);
284
+ }
285
+ /** A new engine in place of the map's (a placeholder, or a lost one): the same camera, settings and layers (same
286
+ * handles, same order) */
287
+ adopt(engine) {
288
+ if (this.gone) {
289
+ engine.free();
290
+ return;
291
+ }
273
292
  this.hoverPick.cancel();
274
293
  const camera = this.getCamera();
275
294
  this.#engine.free();
276
- this.#engine = this.createEngine();
295
+ this.#engine = engine;
277
296
  this.lost = false;
297
+ this.watchDevice(engine);
298
+ // Still timed (profiling)
299
+ if (this.gpuTiming)
300
+ engineGpuTiming(engine, true);
278
301
  this.setCamera(camera);
279
302
  this.atlases.attach(this.#engine);
280
- // Those still waiting for the engine with 3D Tiles (loading, or failed) are created with it
281
- this.stack.reattach(this.#engine, (layer) => layer.needsFullEngine && !this.engines.tiles3d);
303
+ this.stack.reattach(this.#engine);
282
304
  this.applySwipe();
283
305
  // A street-level eye stays one in the new engine
284
306
  if (this.#firstPerson)
@@ -293,6 +315,7 @@ export class GeoMap {
293
315
  void set.show(set.current.index).catch(report);
294
316
  });
295
317
  this.resize();
318
+ this.requestRender();
296
319
  }
297
320
  /** The street-level eye, while there is one (`setFirstPerson`) */
298
321
  #firstPerson;
@@ -354,20 +377,14 @@ export class GeoMap {
354
377
  return !this.gone && this.stack.has(layer);
355
378
  }
356
379
  addLayer(layer) {
357
- // On the core engine, a 3D Tiles layer or a model waits for the engine with 3D Tiles: it is created with the
358
- // others on it
359
- if (layer.needsFullEngine && !this.engines.tiles3d)
360
- this.upgrade();
361
- else {
362
- try {
363
- layer.attach(this.engine);
364
- }
365
- catch (error) {
366
- // A context lost before the browser says so (its event comes later): nothing could be made on it, the layer is
367
- // made with the others on the next engine (`restore`). Any other failure is the caller's
368
- if (!this.contextLost())
369
- throw error;
370
- }
380
+ try {
381
+ layer.attach(this.engine);
382
+ }
383
+ catch (error) {
384
+ // A context lost before the browser says so (its event comes later): nothing could be made on it, the layer is
385
+ // made with the others on the next engine (`restore`). Any other failure is the caller's
386
+ if (!this.contextLost())
387
+ throw error;
371
388
  }
372
389
  // Under the layers kept on top (the basemap's labels)
373
390
  this.stack.add(layer);
@@ -376,6 +393,7 @@ export class GeoMap {
376
393
  }
377
394
  /** Whether the canvas's WebGL context is lost, its event come or not */
378
395
  contextLost() {
396
+ // On a WebGPU canvas, no WebGL context: null, nothing taken
379
397
  return this.lost || this.canvas.getContext('webgl2')?.isContextLost() === true;
380
398
  }
381
399
  /** @internal Keeps a layer above the layers added after it (they go under it), as the labels of a basemap */
@@ -573,26 +591,64 @@ export class GeoMap {
573
591
  this.engine.setBackground(...this.settings.background);
574
592
  this.requestRender();
575
593
  }
594
+ /** The GPU memory its layers hold (bytes): the tiles of the 3D Tiles and point clouds, kept within the budget
595
+ * (`GeoMapOptions.memory`), the rasters' pools */
596
+ memory() {
597
+ // Two numbers a layer, its id and its bytes, the relief's (id 0) last
598
+ const pairs = this.engine.memory();
599
+ const byId = new Map(Array.from({ length: pairs.length / 2 }, (_, i) => [pairs[2 * i], pairs[2 * i + 1] ?? 0]));
600
+ const layers = this.layers.flatMap((layer) => {
601
+ const bytes = layer.id >= 0 ? byId.get(layer.id) : undefined;
602
+ return bytes === undefined ? [] : [{ layer, bytes }];
603
+ });
604
+ return {
605
+ budget: this.settings.memory,
606
+ held: [...byId.values()].reduce((sum, bytes) => sum + bytes, 0),
607
+ layers,
608
+ };
609
+ }
576
610
  /** Tile counters of the raster layers, for diagnostics */
577
611
  stats() {
578
612
  return this.engine.stats();
579
613
  }
614
+ /** Times the parts of each frame on the GPU, for profiling (`gpuTimings`): timer queries, read a few frames later.
615
+ * An engine built to profile only (`PROFILE=1 bun run build`); false without one, or if the browser cannot
616
+ * (`EXT_disjoint_timer_query_webgl2`) */
617
+ setGpuTiming(on) {
618
+ this.gpuTiming = engineGpuTiming(this.engine, on);
619
+ return this.gpuTiming;
620
+ }
621
+ /** The GPU time of the last frame measured (`setGpuTiming`), in all and by part: the backdrop, the ground (the raster
622
+ * layers over the relief), each layer, the shadows, the panorama, the viewshed (ms) */
623
+ gpuTimings() {
624
+ // The frame's number, then each part's code, layer id (-1 for none) and milliseconds
625
+ const read = Reflect.get(this.engine, 'gpuTimings');
626
+ const flat = typeof read === 'function' ? Reflect.apply(read, this.engine, []) : undefined;
627
+ if (!(flat instanceof Float64Array) || flat.length === 0)
628
+ return undefined;
629
+ const parts = Array.from({ length: (flat.length - 1) / 3 }, (_, k) => {
630
+ const [code = 0, id = -1, ms = 0] = flat.subarray(1 + k * 3, 4 + k * 3);
631
+ const layer = id >= 0 ? this.layers.find((l) => l.id === id) : undefined;
632
+ return { part: GPU_PARTS[code] ?? 'layer', ms, ...(layer && { layer }) };
633
+ });
634
+ return { frame: flat[0] ?? 0, ms: parts.reduce((sum, p) => sum + p.ms, 0), parts };
635
+ }
580
636
  /**
581
- * The feature under a screen point (CSS pixels), undefined if none: its layer id and index. Reads one pixel of an
582
- * offscreen picking pass, whatever the number of features, but waits for the GPU to finish the frame (a stall): for
583
- * clicks and one-off queries. On hover, listen to 'hoverfeature' instead, which picks without waiting.
637
+ * The feature under a screen point (CSS pixels), undefined if none: its layer id and index. Reads the pixels around
638
+ * the point of an offscreen picking pass, whatever the number of features, once the GPU is done (a frame or two
639
+ * later: the GPU is not waited for). On hover, listen to 'hoverfeature' instead.
584
640
  */
585
- pick(point, { radius = 0, layers } = {}) {
641
+ async pick(point, { radius = 0, layers } = {}) {
586
642
  const only = layers && Uint32Array.from(layers.map((l) => l.id));
587
- return this.engine.pick(point[0], point[1], radius, only);
643
+ return (await pickOnce(() => this.engine, point, radius, only))?.hit;
588
644
  }
589
645
  /**
590
646
  * The feature under a screen point (CSS pixels), or the nearest one within `radius` pixels: its layer handle, its
591
647
  * index there and, for line, circle and fill layers, the feature itself; undefined if none. `layers` restricts the
592
648
  * search to some layers (the others hide nothing).
593
649
  */
594
- pickFeature(point, options = {}) {
595
- const hit = this.pick(point, options);
650
+ async pickFeature(point, options = {}) {
651
+ const hit = await this.pick(point, options);
596
652
  return hit && this.featureHit(hit);
597
653
  }
598
654
  /** A hit with its layer handle and feature; undefined if the layer is gone */
@@ -617,13 +673,6 @@ export class GeoMap {
617
673
  * and calibrations loaded, none shown until `show`, which projects one onto the scene around it
618
674
  */
619
675
  async addPanoramas(options) {
620
- // Drawn by the engine with 3D Tiles: loaded first if needed
621
- if (!this.engines.tiles3d) {
622
- this.upgrade();
623
- await this.engines.upgrading;
624
- if (!this.engines.tiles3d)
625
- throw new Error('isard: panoramas need the engine with 3D Tiles, which failed to load');
626
- }
627
676
  const host = panoramaHost(this, (lon, lat) => this.engines.geoidHeight(lon, lat));
628
677
  const panoramas = await loadPanoramas(host, options);
629
678
  this.panoramaSets.add(panoramas);
@@ -664,15 +713,16 @@ export class GeoMap {
664
713
  this.interaction.rotateAroundPointer = enabled;
665
714
  }
666
715
  /** What is drawn under a screen point (CSS pixels), on the relief or standing on it (a roof, a wall, a mesh): its
667
- * position and its height above the relief, in metres; undefined where nothing was drawn. The last frame's scene is
668
- * drawn again and read back, waiting for the GPU (a stall): for clicks and drags */
716
+ * position and its height above the relief, in metres; undefined where nothing was drawn. Read from the last copy of
717
+ * the scene's depth (a frame or two old, a pixel every 4 CSS pixels): at once, without waiting for the GPU */
669
718
  pickSurface(point) {
670
719
  const hit = this.engine.surface(point[0], point[1]);
671
720
  return hit && { lonLat: [hit[0], hit[1]], height: hit[2] };
672
721
  }
673
722
  /** The height above the relief, in metres, of the top of what stands at a position: a roof, a mesh, 0 on the bare
674
- * ground; undefined where nothing of the last frame is there. The scene is drawn from above into a single pixel and
675
- * read back: cheap, but it waits for the GPU */
723
+ * ground; undefined where nothing of the last frame is there, or until measured: the scene is drawn looking down at
724
+ * it after a frame and read back a frame or two later, the answer kept and measured again every 250 ms at most (ask
725
+ * again on the next `render` event) */
676
726
  surfaceAt(lonLat) {
677
727
  return this.engine.surfaceAt(lonLat[0], lonLat[1]);
678
728
  }
@@ -802,7 +852,7 @@ export class GeoMap {
802
852
  }
803
853
  /** Polygons raised into blocks with depth (buildings), styled per feature; tiles of them come with `setTile` */
804
854
  addExtrusions(options = {}) {
805
- return this.place(new ExtrusionLayer(this, options.data, extrusionStyle(options)), options.before);
855
+ return this.place(new ExtrusionLayer(this, options.data, extrusionStyle(options), options.drape), options.before);
806
856
  }
807
857
  /** Text labels from GeoJSON (points, the middle of lines, the center of polygons), placed without overlaps */
808
858
  addSymbols(options) {
@@ -879,7 +929,7 @@ export class GeoMap {
879
929
  }
880
930
  /**
881
931
  * A point cloud (see `PointCloudOptions`): a COPC file read by range requests (IGN's LiDAR HD…), or 3D Tiles
882
- * points; with depth, on the engine with 3D Tiles; under `before` if given
932
+ * points; with depth; under `before` if given
883
933
  */
884
934
  addPointCloud(options) {
885
935
  return this.place(new PointCloudLayer(this, options), options.before);
@@ -898,7 +948,7 @@ export class GeoMap {
898
948
  addFlow(options) {
899
949
  return this.place(new FlowLayer(this, options), options.before);
900
950
  }
901
- /** A glTF model drawn at positions (see `ModelOptions`), with depth; on the engine with 3D Tiles, as they are */
951
+ /** A glTF model drawn at positions (see `ModelOptions`), with depth */
902
952
  addModels(options) {
903
953
  return this.place(new ModelLayer(this, options), options.before);
904
954
  }
@@ -913,11 +963,12 @@ export class GeoMap {
913
963
  return addPhoto((o) => this.addModels(o), options);
914
964
  }
915
965
  /**
916
- * The page's own WebGL drawing at its place among the layers, in every frame (see `CustomLayerInterface`): raw
917
- * WebGL, three.js… with the matrices of the frame (`frame.matrixAt`); under `before` if given
966
+ * The page's own WebGPU drawing at its place among the layers, in every frame (see `CustomLayerInterface`): raw
967
+ * WebGPU, three.js's WebGPU renderer… with the map's device and the matrices of the frame (`frame.matrixAt`); under
968
+ * `before` if given. Not drawn on the WebGL2 fallback
918
969
  */
919
970
  addCustomLayer(layer, options = {}) {
920
- const custom = new CustomLayer(this, layer, this.canvas, this, (lon, lat, altitude) => this.engines.customMatrix(lon, lat, altitude), () => this.getCamera());
971
+ const custom = new CustomLayer(this, layer, this, (lon, lat, altitude) => this.engines.customMatrix(lon, lat, altitude), () => this.getCamera());
921
972
  return this.place(custom, options.before);
922
973
  }
923
974
  // ── Frames ──
@@ -975,7 +1026,7 @@ export class GeoMap {
975
1026
  this.resizeObserver.disconnect();
976
1027
  this.interaction?.destroy();
977
1028
  this.engines.destroy();
978
- // Custom layers free their WebGL objects, models their data, while the context is there
1029
+ // Custom layers free their GPU objects, models their data, while the device is there
979
1030
  this.stack.dispose();
980
1031
  this.engine.free();
981
1032
  // Work still under way (labels being shaped, tiles arriving, timers) may call the engine after this: it finds one