isard 1.16.0 → 2.0.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 (103) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +4 -4
  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/workerpool.d.ts +1 -1
  8. package/dist/basemap/workerpool.js.map +1 -1
  9. package/dist/decoder.worker.js +4 -5
  10. package/dist/decoder.worker.js.map +6 -6
  11. package/dist/decoding/decoders.d.ts +1 -1
  12. package/dist/decoding/decoders.js.map +1 -1
  13. package/dist/decoding/loader.d.ts +6 -3
  14. package/dist/decoding/loader.js +7 -5
  15. package/dist/decoding/loader.js.map +1 -1
  16. package/dist/decoding/protocol.d.ts +3 -2
  17. package/dist/decoding/protocol.js.map +1 -1
  18. package/dist/engine-loader.d.ts +20 -18
  19. package/dist/engine-loader.js +45 -42
  20. package/dist/engine-loader.js.map +1 -1
  21. package/dist/index.d.ts +3 -3
  22. package/dist/index.js.map +1 -1
  23. package/dist/isard_grid_bg.wasm +0 -0
  24. package/dist/isard_tess_bg.wasm +0 -0
  25. package/dist/isard_tiles3d_bg.wasm +0 -0
  26. package/dist/isard_webgl2_bg.wasm +0 -0
  27. package/dist/isard_webgpu_bg.wasm +0 -0
  28. package/dist/layers/custom.d.ts +35 -29
  29. package/dist/layers/custom.js +60 -46
  30. package/dist/layers/custom.js.map +1 -1
  31. package/dist/layers/extrusions.js.map +1 -1
  32. package/dist/layers/features.js.map +1 -1
  33. package/dist/layers/flow.js.map +1 -1
  34. package/dist/layers/glyphs.d.ts +1 -1
  35. package/dist/layers/glyphs.js.map +1 -1
  36. package/dist/layers/ground.d.ts +20 -13
  37. package/dist/layers/ground.js +29 -8
  38. package/dist/layers/ground.js.map +1 -1
  39. package/dist/layers/icons.d.ts +1 -1
  40. package/dist/layers/icons.js.map +1 -1
  41. package/dist/layers/layers.d.ts +1 -2
  42. package/dist/layers/layers.js +2 -11
  43. package/dist/layers/layers.js.map +1 -1
  44. package/dist/layers/models.js +0 -4
  45. package/dist/layers/models.js.map +1 -1
  46. package/dist/layers/network.d.ts +1 -1
  47. package/dist/layers/network.js.map +1 -1
  48. package/dist/layers/panoramas.d.ts +1 -0
  49. package/dist/layers/panoramas.js +3 -2
  50. package/dist/layers/panoramas.js.map +1 -1
  51. package/dist/layers/pointclouds.d.ts +1 -1
  52. package/dist/layers/pointclouds.js +0 -4
  53. package/dist/layers/pointclouds.js.map +1 -1
  54. package/dist/layers/raster.js.map +1 -1
  55. package/dist/layers/symbols.js.map +1 -1
  56. package/dist/layers/tiles3d.d.ts +1 -1
  57. package/dist/layers/tiles3d.js +0 -4
  58. package/dist/layers/tiles3d.js.map +1 -1
  59. package/dist/layers/vectors.d.ts +4 -5
  60. package/dist/layers/vectors.js +7 -15
  61. package/dist/layers/vectors.js.map +1 -1
  62. package/dist/map/engine-settings.d.ts +1 -1
  63. package/dist/map/engine-settings.js.map +1 -1
  64. package/dist/map/engines.d.ts +11 -26
  65. package/dist/map/engines.js +10 -48
  66. package/dist/map/engines.js.map +1 -1
  67. package/dist/map/frame-loop.d.ts +1 -3
  68. package/dist/map/frame-loop.js +3 -3
  69. package/dist/map/frame-loop.js.map +1 -1
  70. package/dist/map/hover-pick.d.ts +9 -3
  71. package/dist/map/hover-pick.js +29 -6
  72. package/dist/map/hover-pick.js.map +1 -1
  73. package/dist/map/layer-stack.d.ts +2 -2
  74. package/dist/map/layer-stack.js +1 -3
  75. package/dist/map/layer-stack.js.map +1 -1
  76. package/dist/map/symbol-atlases.d.ts +5 -4
  77. package/dist/map/symbol-atlases.js +8 -13
  78. package/dist/map/symbol-atlases.js.map +1 -1
  79. package/dist/map/viewshed.d.ts +1 -1
  80. package/dist/map/viewshed.js.map +1 -1
  81. package/dist/map.d.ts +26 -20
  82. package/dist/map.js +93 -78
  83. package/dist/map.js.map +1 -1
  84. package/dist/react/IsardMap.d.ts +1 -1
  85. package/dist/react/IsardMap.js.map +1 -1
  86. package/dist/react/index.d.ts +1 -1
  87. package/dist/react/index.js.map +1 -1
  88. package/dist/types.d.ts +29 -18
  89. package/dist/types.js.map +1 -1
  90. package/dist/ui/interaction.d.ts +1 -1
  91. package/dist/ui/interaction.js.map +1 -1
  92. package/dist/util/animation.d.ts +1 -1
  93. package/dist/util/animation.js.map +1 -1
  94. package/dist/wasm/isard_tiles3d.d.ts +181 -0
  95. package/dist/wasm/isard_tiles3d.js +387 -0
  96. package/dist/wasm/{isard_core.d.ts → isard_webgl2.d.ts} +373 -182
  97. package/dist/wasm/{isard_core.js → isard_webgl2.js} +2155 -413
  98. package/dist/wasm/{isard.d.ts → isard_webgpu.d.ts} +176 -353
  99. package/dist/wasm/{isard.js → isard_webgpu.js} +1361 -1104
  100. package/package.json +4 -3
  101. package/vite.js +5 -4
  102. package/dist/isard_bg.wasm +0 -0
  103. 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,9 +67,19 @@ 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
85
  /** The engine class (core, or with 3D Tiles) and the engine with 3D Tiles loaded when needed */
@@ -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,7 +182,8 @@ 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
189
  // The browser may take the GPU away (driver reset, background tab on mobile): the map waits, then rebuilds
@@ -224,34 +240,6 @@ export class GeoMap {
224
240
  get onTop() {
225
241
  return this.stack.onTop;
226
242
  }
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
- });
242
- }
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
- }
254
- }
255
243
  /**
256
244
  * The context is lost: its engine freed at once, the map's calls going to one that does nothing until `restore`
257
245
  * builds the next. The old engine's GPU objects went with the context: deleted now, it is silent (on the context
@@ -266,19 +254,34 @@ export class GeoMap {
266
254
  // Glyph ranges and icons arriving meanwhile upload to it, not to the freed engine
267
255
  this.atlases.attach(this.#engine);
268
256
  }
269
- /** After a context loss: a new engine with the same camera, settings and layers (same handles, same order) */
257
+ /** After a context loss: a new engine with the same camera, settings and layers (same handles, same order); the
258
+ * engine made asynchronously, adopted once there */
270
259
  restore() {
271
260
  if (this.gone)
272
261
  return;
262
+ this.engines
263
+ .create(this.canvas, () => this.requestRender(), this.settings)
264
+ .then((engine) => this.adopt(engine))
265
+ .catch(report);
266
+ }
267
+ /** A new engine in place of the map's (a placeholder, or a lost one): the same camera, settings and layers (same
268
+ * handles, same order) */
269
+ adopt(engine) {
270
+ if (this.gone) {
271
+ engine.free();
272
+ return;
273
+ }
273
274
  this.hoverPick.cancel();
274
275
  const camera = this.getCamera();
275
276
  this.#engine.free();
276
- this.#engine = this.createEngine();
277
+ this.#engine = engine;
277
278
  this.lost = false;
279
+ // Still timed (profiling)
280
+ if (this.gpuTiming)
281
+ engineGpuTiming(engine, true);
278
282
  this.setCamera(camera);
279
283
  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);
284
+ this.stack.reattach(this.#engine);
282
285
  this.applySwipe();
283
286
  // A street-level eye stays one in the new engine
284
287
  if (this.#firstPerson)
@@ -293,6 +296,7 @@ export class GeoMap {
293
296
  void set.show(set.current.index).catch(report);
294
297
  });
295
298
  this.resize();
299
+ this.requestRender();
296
300
  }
297
301
  /** The street-level eye, while there is one (`setFirstPerson`) */
298
302
  #firstPerson;
@@ -354,20 +358,14 @@ export class GeoMap {
354
358
  return !this.gone && this.stack.has(layer);
355
359
  }
356
360
  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
- }
361
+ try {
362
+ layer.attach(this.engine);
363
+ }
364
+ catch (error) {
365
+ // A context lost before the browser says so (its event comes later): nothing could be made on it, the layer is
366
+ // made with the others on the next engine (`restore`). Any other failure is the caller's
367
+ if (!this.contextLost())
368
+ throw error;
371
369
  }
372
370
  // Under the layers kept on top (the basemap's labels)
373
371
  this.stack.add(layer);
@@ -376,6 +374,7 @@ export class GeoMap {
376
374
  }
377
375
  /** Whether the canvas's WebGL context is lost, its event come or not */
378
376
  contextLost() {
377
+ // On a WebGPU canvas, no WebGL context: null, nothing taken
379
378
  return this.lost || this.canvas.getContext('webgl2')?.isContextLost() === true;
380
379
  }
381
380
  /** @internal Keeps a layer above the layers added after it (they go under it), as the labels of a basemap */
@@ -577,22 +576,44 @@ export class GeoMap {
577
576
  stats() {
578
577
  return this.engine.stats();
579
578
  }
579
+ /** Times the parts of each frame on the GPU, for profiling (`gpuTimings`): timer queries, read a few frames later.
580
+ * An engine built to profile only (`PROFILE=1 bun run build`); false without one, or if the browser cannot
581
+ * (`EXT_disjoint_timer_query_webgl2`) */
582
+ setGpuTiming(on) {
583
+ this.gpuTiming = engineGpuTiming(this.engine, on);
584
+ return this.gpuTiming;
585
+ }
586
+ /** The GPU time of the last frame measured (`setGpuTiming`), in all and by part: the backdrop, the ground (the raster
587
+ * layers over the relief), each layer, the shadows, the panorama, the viewshed (ms) */
588
+ gpuTimings() {
589
+ // The frame's number, then each part's code, layer id (-1 for none) and milliseconds
590
+ const read = Reflect.get(this.engine, 'gpuTimings');
591
+ const flat = typeof read === 'function' ? Reflect.apply(read, this.engine, []) : undefined;
592
+ if (!(flat instanceof Float64Array) || flat.length === 0)
593
+ return undefined;
594
+ const parts = Array.from({ length: (flat.length - 1) / 3 }, (_, k) => {
595
+ const [code = 0, id = -1, ms = 0] = flat.subarray(1 + k * 3, 4 + k * 3);
596
+ const layer = id >= 0 ? this.layers.find((l) => l.id === id) : undefined;
597
+ return { part: GPU_PARTS[code] ?? 'layer', ms, ...(layer && { layer }) };
598
+ });
599
+ return { frame: flat[0] ?? 0, ms: parts.reduce((sum, p) => sum + p.ms, 0), parts };
600
+ }
580
601
  /**
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.
602
+ * The feature under a screen point (CSS pixels), undefined if none: its layer id and index. Reads the pixels around
603
+ * the point of an offscreen picking pass, whatever the number of features, once the GPU is done (a frame or two
604
+ * later: the GPU is not waited for). On hover, listen to 'hoverfeature' instead.
584
605
  */
585
- pick(point, { radius = 0, layers } = {}) {
606
+ async pick(point, { radius = 0, layers } = {}) {
586
607
  const only = layers && Uint32Array.from(layers.map((l) => l.id));
587
- return this.engine.pick(point[0], point[1], radius, only);
608
+ return (await pickOnce(() => this.engine, point, radius, only))?.hit;
588
609
  }
589
610
  /**
590
611
  * The feature under a screen point (CSS pixels), or the nearest one within `radius` pixels: its layer handle, its
591
612
  * index there and, for line, circle and fill layers, the feature itself; undefined if none. `layers` restricts the
592
613
  * search to some layers (the others hide nothing).
593
614
  */
594
- pickFeature(point, options = {}) {
595
- const hit = this.pick(point, options);
615
+ async pickFeature(point, options = {}) {
616
+ const hit = await this.pick(point, options);
596
617
  return hit && this.featureHit(hit);
597
618
  }
598
619
  /** A hit with its layer handle and feature; undefined if the layer is gone */
@@ -617,13 +638,6 @@ export class GeoMap {
617
638
  * and calibrations loaded, none shown until `show`, which projects one onto the scene around it
618
639
  */
619
640
  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
641
  const host = panoramaHost(this, (lon, lat) => this.engines.geoidHeight(lon, lat));
628
642
  const panoramas = await loadPanoramas(host, options);
629
643
  this.panoramaSets.add(panoramas);
@@ -664,15 +678,16 @@ export class GeoMap {
664
678
  this.interaction.rotateAroundPointer = enabled;
665
679
  }
666
680
  /** 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 */
681
+ * position and its height above the relief, in metres; undefined where nothing was drawn. Read from the last copy of
682
+ * the scene's depth (a frame or two old, a pixel every 4 CSS pixels): at once, without waiting for the GPU */
669
683
  pickSurface(point) {
670
684
  const hit = this.engine.surface(point[0], point[1]);
671
685
  return hit && { lonLat: [hit[0], hit[1]], height: hit[2] };
672
686
  }
673
687
  /** 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 */
688
+ * ground; undefined where nothing of the last frame is there, or until measured: the scene is drawn looking down at
689
+ * it after a frame and read back a frame or two later, the answer kept and measured again every 250 ms at most (ask
690
+ * again on the next `render` event) */
676
691
  surfaceAt(lonLat) {
677
692
  return this.engine.surfaceAt(lonLat[0], lonLat[1]);
678
693
  }
@@ -917,7 +932,7 @@ export class GeoMap {
917
932
  * WebGL, three.js… with the matrices of the frame (`frame.matrixAt`); under `before` if given
918
933
  */
919
934
  addCustomLayer(layer, options = {}) {
920
- const custom = new CustomLayer(this, layer, this.canvas, this, (lon, lat, altitude) => this.engines.customMatrix(lon, lat, altitude), () => this.getCamera());
935
+ const custom = new CustomLayer(this, layer, this, (lon, lat, altitude) => this.engines.customMatrix(lon, lat, altitude), () => this.getCamera());
921
936
  return this.place(custom, options.before);
922
937
  }
923
938
  // ── Frames ──