@loaders.gl/tiles 5.0.0-alpha.6 → 5.0.0-alpha.8

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 (114) hide show
  1. package/README.md +26 -0
  2. package/dist/dist.dev.js +10015 -8172
  3. package/dist/dist.min.js +3 -4
  4. package/dist/index.cjs +1676 -477
  5. package/dist/index.cjs.map +3 -3
  6. package/dist/index.d.ts +10 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +4 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/spatial/format-spatial-reference.d.ts +1 -0
  11. package/dist/spatial/format-spatial-reference.d.ts.map +1 -1
  12. package/dist/spatial/format-spatial-reference.js +104 -46
  13. package/dist/spatial/format-spatial-reference.js.map +1 -1
  14. package/dist/spatial/get-spatial-coordinate-frame.d.ts +12 -1
  15. package/dist/spatial/get-spatial-coordinate-frame.d.ts.map +1 -1
  16. package/dist/spatial/get-spatial-coordinate-frame.js +105 -24
  17. package/dist/spatial/get-spatial-coordinate-frame.js.map +1 -1
  18. package/dist/spatial/normalize-crs-identifier.d.ts +15 -0
  19. package/dist/spatial/normalize-crs-identifier.d.ts.map +1 -0
  20. package/dist/spatial/normalize-crs-identifier.js +22 -0
  21. package/dist/spatial/normalize-crs-identifier.js.map +1 -0
  22. package/dist/spatial/spatial-coordinate-transformer.d.ts +4 -0
  23. package/dist/spatial/spatial-coordinate-transformer.d.ts.map +1 -1
  24. package/dist/spatial/spatial-coordinate-transformer.js +102 -18
  25. package/dist/spatial/spatial-coordinate-transformer.js.map +1 -1
  26. package/dist/spatial/spatial-resource-registry.js +3 -3
  27. package/dist/spatial/spatial-resource-registry.js.map +1 -1
  28. package/dist/spatial/tiles-3d-spatial-transformer.d.ts +32 -0
  29. package/dist/spatial/tiles-3d-spatial-transformer.d.ts.map +1 -0
  30. package/dist/spatial/tiles-3d-spatial-transformer.js +175 -0
  31. package/dist/spatial/tiles-3d-spatial-transformer.js.map +1 -0
  32. package/dist/tileset-3d/common/tile-3d-contracts.d.ts +63 -0
  33. package/dist/tileset-3d/common/tile-3d-contracts.d.ts.map +1 -0
  34. package/dist/tileset-3d/common/tile-3d-contracts.js +51 -0
  35. package/dist/tileset-3d/common/tile-3d-contracts.js.map +1 -0
  36. package/dist/tileset-3d/common/tile-3d.d.ts +33 -5
  37. package/dist/tileset-3d/common/tile-3d.d.ts.map +1 -1
  38. package/dist/tileset-3d/common/tile-3d.js +179 -29
  39. package/dist/tileset-3d/common/tile-3d.js.map +1 -1
  40. package/dist/tileset-3d/common/tileset-3d.d.ts +15 -25
  41. package/dist/tileset-3d/common/tileset-3d.d.ts.map +1 -1
  42. package/dist/tileset-3d/common/tileset-3d.js +35 -42
  43. package/dist/tileset-3d/common/tileset-3d.js.map +1 -1
  44. package/dist/tileset-3d/common/tileset-content-traversal.d.ts +37 -0
  45. package/dist/tileset-3d/common/tileset-content-traversal.d.ts.map +1 -0
  46. package/dist/tileset-3d/common/tileset-content-traversal.js +80 -0
  47. package/dist/tileset-3d/common/tileset-content-traversal.js.map +1 -0
  48. package/dist/tileset-3d/common/tileset-observability.d.ts +60 -0
  49. package/dist/tileset-3d/common/tileset-observability.d.ts.map +1 -0
  50. package/dist/tileset-3d/common/tileset-observability.js +39 -0
  51. package/dist/tileset-3d/common/tileset-observability.js.map +1 -0
  52. package/dist/tileset-3d/common/tileset-source.d.ts +11 -1
  53. package/dist/tileset-3d/common/tileset-source.d.ts.map +1 -1
  54. package/dist/tileset-3d/common/tileset-source.js.map +1 -1
  55. package/dist/tileset-3d/common/tileset-traverser.d.ts.map +1 -1
  56. package/dist/tileset-3d/common/tileset-traverser.js +4 -3
  57. package/dist/tileset-3d/common/tileset-traverser.js.map +1 -1
  58. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts +47 -1
  59. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts.map +1 -1
  60. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js +327 -30
  61. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js.map +1 -1
  62. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts +34 -0
  63. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts.map +1 -1
  64. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js +278 -14
  65. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js.map +1 -1
  66. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts +5 -3
  67. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts.map +1 -1
  68. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js +7 -4
  69. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js.map +1 -1
  70. package/dist/tileset-3d/format-i3s/i3s-source.d.ts +4 -2
  71. package/dist/tileset-3d/format-i3s/i3s-source.d.ts.map +1 -1
  72. package/dist/tileset-3d/format-i3s/i3s-source.js +60 -9
  73. package/dist/tileset-3d/format-i3s/i3s-source.js.map +1 -1
  74. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.d.ts.map +1 -1
  75. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js +1 -1
  76. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js.map +1 -1
  77. package/dist/tileset-3d/helpers/bounding-volume.d.ts +7 -3
  78. package/dist/tileset-3d/helpers/bounding-volume.d.ts.map +1 -1
  79. package/dist/tileset-3d/helpers/bounding-volume.js +19 -61
  80. package/dist/tileset-3d/helpers/bounding-volume.js.map +1 -1
  81. package/dist/tileset-3d/helpers/frame-state.d.ts +5 -0
  82. package/dist/tileset-3d/helpers/frame-state.d.ts.map +1 -1
  83. package/dist/tileset-3d/helpers/frame-state.js +11 -5
  84. package/dist/tileset-3d/helpers/frame-state.js.map +1 -1
  85. package/dist/tileset-3d/helpers/s2-bounding-volume.d.ts +17 -0
  86. package/dist/tileset-3d/helpers/s2-bounding-volume.d.ts.map +1 -0
  87. package/dist/tileset-3d/helpers/s2-bounding-volume.js +20 -0
  88. package/dist/tileset-3d/helpers/s2-bounding-volume.js.map +1 -0
  89. package/dist/types.d.ts +2 -0
  90. package/dist/types.d.ts.map +1 -1
  91. package/package.json +11 -12
  92. package/src/index.ts +18 -1
  93. package/src/spatial/format-spatial-reference.ts +127 -53
  94. package/src/spatial/get-spatial-coordinate-frame.ts +126 -31
  95. package/src/spatial/normalize-crs-identifier.ts +26 -0
  96. package/src/spatial/spatial-coordinate-transformer.ts +124 -24
  97. package/src/spatial/spatial-resource-registry.ts +3 -3
  98. package/src/spatial/tiles-3d-spatial-transformer.ts +206 -0
  99. package/src/tileset-3d/common/tile-3d-contracts.ts +115 -0
  100. package/src/tileset-3d/common/tile-3d.ts +208 -32
  101. package/src/tileset-3d/common/tileset-3d.ts +37 -61
  102. package/src/tileset-3d/common/tileset-content-traversal.ts +114 -0
  103. package/src/tileset-3d/common/tileset-observability.ts +100 -0
  104. package/src/tileset-3d/common/tileset-source.ts +19 -1
  105. package/src/tileset-3d/common/tileset-traverser.ts +4 -3
  106. package/src/tileset-3d/format-3d-tiles/implicit-tiling.ts +481 -34
  107. package/src/tileset-3d/format-3d-tiles/tiles-3d-source.ts +405 -14
  108. package/src/tileset-3d/format-3d-tiles/tileset-3d-traverser.ts +7 -4
  109. package/src/tileset-3d/format-i3s/i3s-source.ts +84 -13
  110. package/src/tileset-3d/format-i3s/i3s-tileset-traverser.ts +2 -1
  111. package/src/tileset-3d/helpers/bounding-volume.ts +29 -62
  112. package/src/tileset-3d/helpers/frame-state.ts +15 -7
  113. package/src/tileset-3d/helpers/s2-bounding-volume.ts +35 -0
  114. package/src/types.ts +2 -0
@@ -8,10 +8,12 @@ import {Vector3} from '@math.gl/core';
8
8
  import type {CoreAPI, Loader, LoaderOptions} from '@loaders.gl/loader-utils';
9
9
  import type {Tile3D} from '../common/tile-3d';
10
10
  import {Tile3D as Tile3DNode} from '../common/tile-3d';
11
+ import type {Tile3DContent} from '../common/tile-3d-contracts';
11
12
  import {I3STilesetTraverser} from './i3s-tileset-traverser';
12
13
  import type {Tileset3D} from '../common/tileset-3d';
13
14
  import type {FrameState} from '../helpers/frame-state';
14
15
  import type {
16
+ TileChildrenLoadResult,
15
17
  TileContentLoadResult,
16
18
  TilesetContentFormats,
17
19
  TilesetJSON,
@@ -219,7 +221,17 @@ export class I3SSource implements Tileset3DSource {
219
221
  };
220
222
 
221
223
  tile.content = await this.loadResourceData(contentUrl, options, this.contentLoader);
222
- return {loaded: true};
224
+ const contentEntry: Tile3DContent = {
225
+ index: 0,
226
+ uri: tile.contentUrl,
227
+ type: tile.header.layerType,
228
+ payload: tile.content,
229
+ metadata: tile.header.metadata || null,
230
+ boundingVolume: tile.boundingVolume,
231
+ featureIds: tile.featureIdSets,
232
+ renderable: Boolean(tile.content)
233
+ };
234
+ return {loaded: true, contents: [tile.content], contentEntries: [contentEntry]};
223
235
  }
224
236
 
225
237
  /**
@@ -228,7 +240,7 @@ export class I3SSource implements Tileset3DSource {
228
240
  async loadChildTileHeader(
229
241
  _parentTile: Tile3D,
230
242
  childId: string,
231
- _frameState: FrameState
243
+ _frameState: FrameState | null
232
244
  ): Promise<any> {
233
245
  const metadata = this.getMetadata();
234
246
  if (metadata.tileset.nodePages || metadata.tileset.pointNodePages) {
@@ -253,6 +265,50 @@ export class I3SSource implements Tileset3DSource {
253
265
  return await this.transformTileHeader(header);
254
266
  }
255
267
 
268
+ /** Loads all declared I3S child headers without camera or visibility state. */
269
+ async loadTileChildrenForTraversal(
270
+ parentTile: Tile3D,
271
+ signal?: AbortSignal
272
+ ): Promise<TileChildrenLoadResult> {
273
+ const childHeaders = parentTile.header.children || [];
274
+ let loadedChildren = 0;
275
+ const orderedChildren: Tile3D[] = [];
276
+
277
+ for (const childHeader of childHeaders) {
278
+ throwIfTraversalAborted(signal);
279
+ const childId = childHeader.id;
280
+ if (typeof childId !== 'string' && typeof childId !== 'number') {
281
+ throw new Error('I3S child header is missing its node ID');
282
+ }
283
+ const existingChild = parentTile.children.find(
284
+ tile => tile.id === String(childId) || tile.header?.id === childId
285
+ );
286
+ if (existingChild) {
287
+ orderedChildren.push(existingChild);
288
+ continue;
289
+ }
290
+
291
+ const childTileHeader = await this.loadChildTileHeader(parentTile, String(childId), null);
292
+ throwIfTraversalAborted(signal);
293
+ const childTile = new Tile3DNode(
294
+ parentTile.tileset,
295
+ childTileHeader,
296
+ parentTile,
297
+ String(childId)
298
+ );
299
+ orderedChildren.push(childTile);
300
+ loadedChildren++;
301
+ }
302
+
303
+ const orderedChildSet = new Set(orderedChildren);
304
+ parentTile.children = [
305
+ ...orderedChildren,
306
+ ...parentTile.children.filter(childTile => !orderedChildSet.has(childTile))
307
+ ];
308
+
309
+ return {loaded: true, tileCount: loadedChildren, childSubtreeCount: 0};
310
+ }
311
+
256
312
  /**
257
313
  * Resolves an I3S request URL with source-managed query parameters.
258
314
  */
@@ -265,24 +321,28 @@ export class I3SSource implements Tileset3DSource {
265
321
  return tilePath;
266
322
  }
267
323
 
268
- const queryDelimiterIndex = tilePath.indexOf('?');
269
- if (queryDelimiterIndex === -1) {
270
- return `${tilePath}?${new URLSearchParams(this.queryParams).toString()}`;
271
- }
272
-
273
- const existingQuery = tilePath.slice(queryDelimiterIndex + 1);
274
- const existingQueryKeys = new Set(
275
- existingQuery.split('&').map(parameter => parameter.split('=', 1)[0])
276
- );
324
+ const hashIndex = tilePath.indexOf('#');
325
+ const hash = hashIndex >= 0 ? tilePath.slice(hashIndex) : '';
326
+ const tilePathWithoutHash = hashIndex >= 0 ? tilePath.slice(0, hashIndex) : tilePath;
327
+ const queryDelimiterIndex = tilePathWithoutHash.indexOf('?');
328
+ const path =
329
+ queryDelimiterIndex === -1
330
+ ? tilePathWithoutHash
331
+ : tilePathWithoutHash.slice(0, queryDelimiterIndex);
332
+ const existingQuery =
333
+ queryDelimiterIndex === -1 ? '' : tilePathWithoutHash.slice(queryDelimiterIndex + 1);
334
+ const existingQueryParams = new URLSearchParams(existingQuery);
277
335
  const queryParams = new URLSearchParams();
278
336
  for (const [key, value] of Object.entries(this.queryParams)) {
279
- if (!existingQueryKeys.has(key)) {
337
+ if (!existingQueryParams.has(key)) {
280
338
  queryParams.set(key, value);
281
339
  }
282
340
  }
283
341
 
284
342
  const queryString = queryParams.toString();
285
- return queryString ? `${tilePath}${existingQuery ? '&' : ''}${queryString}` : tilePath;
343
+ return `${path}${existingQuery ? `?${existingQuery}` : ''}${
344
+ queryString ? `${existingQuery ? '&' : '?'}${queryString}` : ''
345
+ }${hash}`;
286
346
  }
287
347
 
288
348
  /**
@@ -403,6 +463,10 @@ export class I3SSource implements Tileset3DSource {
403
463
  * @param loadOptions Loader options that may contain an ArcGIS token.
404
464
  */
405
465
  private initializeQueryParams(loadOptions: LoaderOptions): void {
466
+ for (const [key, value] of Object.entries(loadOptions.searchParams || {})) {
467
+ this.queryParams[key] = String(value);
468
+ }
469
+
406
470
  const i3sOptions = loadOptions.i3s;
407
471
  if (i3sOptions && typeof i3sOptions === 'object' && 'token' in i3sOptions) {
408
472
  const token = (i3sOptions as Record<string, unknown>).token;
@@ -449,6 +513,13 @@ export class I3SSource implements Tileset3DSource {
449
513
  }
450
514
  }
451
515
 
516
+ /** Throws a cancellation reason before or after source-managed traversal work. */
517
+ function throwIfTraversalAborted(signal?: AbortSignal): void {
518
+ if (signal?.aborted) {
519
+ throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');
520
+ }
521
+ }
522
+
452
523
  /** Returns the format-provided binary content loader, falling back for custom legacy loaders. */
453
524
  function getI3SContentLoader(loader: Loader): Loader {
454
525
  return (loader as Loader & {contentLoader?: Loader}).contentLoader || loader;
@@ -38,7 +38,8 @@ export class I3STilesetTraverser extends TilesetTraverser {
38
38
  for (const child of children) {
39
39
  const extendedId = `${child.id}-${frameState.viewport.id}`;
40
40
  // if child tile is not fetched
41
- const childTile = childTiles && childTiles.find(t => t.id === extendedId);
41
+ const childTile =
42
+ childTiles && childTiles.find(t => t.id === extendedId || t.header?.id === child.id);
42
43
  if (!childTile) {
43
44
  const request = () => this._loadTile(tile, child.id, frameState);
44
45
  const cachedRequest = this._tileManager.find(extendedId);
@@ -4,7 +4,7 @@
4
4
  // See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md
5
5
 
6
6
  /* eslint-disable */
7
- import {Quaternion, Vector3, Matrix3, degrees} from '@math.gl/core';
7
+ import {Quaternion, Vector3, Matrix3, Matrix4, degrees} from '@math.gl/core';
8
8
  import {BoundingSphere, OrientedBoundingBox} from '@math.gl/culling';
9
9
  import {Ellipsoid, makeOBBFromRegion} from '@math.gl/geospatial';
10
10
  import {assert} from '@loaders.gl/loader-utils';
@@ -18,6 +18,7 @@ function defined(x) {
18
18
  // const scratchMatrix = new Matrix3();
19
19
  const scratchPoint = new Vector3();
20
20
  const scratchScale = new Vector3();
21
+ const identityTransform = new Matrix4();
21
22
  // const scratchRectangle = new Rectangle();
22
23
  // const scratchOrientedBoundingBox = new OrientedBoundingBox();
23
24
  // const scratchTransform = new Matrix4();
@@ -25,20 +26,39 @@ const scratchScale = new Vector3();
25
26
  /**
26
27
  * Create a bounding volume from the tile's bounding volume header.
27
28
  * @param {Object} boundingVolumeHeader The tile's bounding volume header.
28
- * @param {Matrix4} transform The transform to apply to the bounding volume.
29
+ * @param {Matrix4} transform The current computed tile transform, including application transforms.
29
30
  * @param [result] The object onto which to store the result.
31
+ * @param initialTransform The accumulated JSON tile transform, excluded from region transforms.
32
+ * Defaults to transform so callers without application transforms keep geospatial regions fixed.
30
33
  * @returns The modified result parameter or a new TileBoundingVolume instance if none was provided.
31
34
  */
32
- export function createBoundingVolume(boundingVolumeHeader, transform, result?) {
35
+ export function createBoundingVolume(
36
+ boundingVolumeHeader,
37
+ transform: Matrix4,
38
+ result?,
39
+ initialTransform: Matrix4 = transform
40
+ ) {
33
41
  assert(boundingVolumeHeader, '3D Tile: boundingVolume must be defined');
34
42
 
35
43
  // boundingVolume schema:
36
44
  // https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/specification/schema/boundingVolume.schema.json
37
45
  if (boundingVolumeHeader.box) {
38
- return createBox(boundingVolumeHeader.box, transform, result);
46
+ // S2 cells and regions are geospatially anchored. Their conservative ECEF boxes are already
47
+ // world-space volumes and must not receive the tile/node transform a second time.
48
+ const boxTransform = boundingVolumeHeader.s2VolumeInfo ? identityTransform : transform;
49
+ return createBox(boundingVolumeHeader.box, boxTransform, result);
39
50
  }
40
51
  if (boundingVolumeHeader.region) {
41
- return makeOBBFromRegion(boundingVolumeHeader.region);
52
+ const boundingVolume = makeOBBFromRegion(boundingVolumeHeader.region);
53
+ // Regions are already in world coordinates. Apply only the change from the JSON transform.
54
+ const regionTransform = transform.equals(initialTransform)
55
+ ? identityTransform
56
+ : transform.clone().multiplyRight(initialTransform.clone().invert());
57
+ return createBox(
58
+ [...boundingVolume.center, ...boundingVolume.halfAxes],
59
+ regionTransform,
60
+ result
61
+ );
42
62
  }
43
63
 
44
64
  if (boundingVolumeHeader.sphere) {
@@ -55,15 +75,17 @@ export type CartographicBounds = [min: number[], max: number[]];
55
75
  * Calculate the cartographic bounding box the tile's bounding volume.
56
76
  * @param {Object} boundingVolumeHeader The tile's bounding volume header.
57
77
  * @param {BoundingVolume} boundingVolume The bounding volume.
78
+ * @param isRegionTransformed Whether application transforms moved the region from its JSON bounds.
58
79
  * @returns {CartographicBounds}
59
80
  */
60
81
  export function getCartographicBounds(
61
82
  boundingVolumeHeader,
62
- boundingVolume: OrientedBoundingBox | BoundingSphere
83
+ boundingVolume: OrientedBoundingBox | BoundingSphere,
84
+ isRegionTransformed = false
63
85
  ): CartographicBounds {
64
86
  // boundingVolume schema:
65
87
  // https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/specification/schema/boundingVolume.schema.json
66
- if (boundingVolumeHeader.box) {
88
+ if (boundingVolumeHeader.box || (boundingVolumeHeader.region && isRegionTransformed)) {
67
89
  return orientedBoundingBoxToCartographicBounds(boundingVolume as OrientedBoundingBox);
68
90
  }
69
91
  if (boundingVolumeHeader.region) {
@@ -142,61 +164,6 @@ function createBox(box, transform, result?) {
142
164
  return new OrientedBoundingBox(center, halfAxes);
143
165
  }
144
166
 
145
- /*
146
- function createBoxFromTransformedRegion(region, transform, initialTransform, result) {
147
- const rectangle = Rectangle.unpack(region, 0, scratchRectangle);
148
- const minimumHeight = region[4];
149
- const maximumHeight = region[5];
150
-
151
- const orientedBoundingBox = OrientedBoundingBox.fromRectangle(
152
- rectangle,
153
- minimumHeight,
154
- maximumHeight,
155
- Ellipsoid.WGS84,
156
- scratchOrientedBoundingBox
157
- );
158
- const center = orientedBoundingBox.center;
159
- const halfAxes = orientedBoundingBox.halfAxes;
160
-
161
- // A region bounding volume is not transformed by the transform in the tileset JSON,
162
- // but may be transformed by additional transforms applied in Cesium.
163
- // This is why the transform is calculated as the difference between the initial transform and the current transform.
164
- transform = Matrix4.multiplyTransformation(
165
- transform,
166
- Matrix4.inverseTransformation(initialTransform, scratchTransform),
167
- scratchTransform
168
- );
169
- center = Matrix4.multiplyByPoint(transform, center, center);
170
- const rotationScale = Matrix4.getRotation(transform, scratchMatrix);
171
- halfAxes = Matrix3.multiply(rotationScale, halfAxes, halfAxes);
172
-
173
- if (defined(result) && result instanceof TileOrientedBoundingBox) {
174
- result.update(center, halfAxes);
175
- return result;
176
- }
177
-
178
- return new TileOrientedBoundingBox(center, halfAxes);
179
- }
180
-
181
- function createRegion(region, transform, initialTransform, result) {
182
- if (!Matrix4.equalsEpsilon(transform, initialTransform, CesiumMath.EPSILON8)) {
183
- return createBoxFromTransformedRegion(region, transform, initialTransform, result);
184
- }
185
-
186
- if (defined(result)) {
187
- return result;
188
- }
189
-
190
- const rectangleRegion = Rectangle.unpack(region, 0, scratchRectangle);
191
-
192
- return new TileBoundingRegion({
193
- rectangle: rectangleRegion,
194
- minimumHeight: region[4],
195
- maximumHeight: region[5]
196
- });
197
- }
198
- */
199
-
200
167
  function createSphere(sphere, transform, result?) {
201
168
  // Find the transformed center
202
169
  const center = new Vector3(sphere[0], sphere[1], sphere[2]);
@@ -30,7 +30,8 @@ export type FrameState = {
30
30
  /** Optional world-space clipping planes applied to render-content culling. */
31
31
  clippingPlanes?: Plane[];
32
32
  frameNumber: number; // TODO: This can be the same between updates, what number is unique for between updates?
33
- sseDenominator: number; // Assumes fovy = 60 degrees
33
+ /** Perspective projection factor: twice the tangent of half the vertical field of view. */
34
+ sseDenominator: number;
34
35
  /** View-dependent density used by perspective dynamic screen-space error for this traversal. */
35
36
  dynamicScreenSpaceErrorDensity: number;
36
37
  };
@@ -75,6 +76,10 @@ const cullingVolume = new CullingVolume([
75
76
  /**
76
77
  * Extracts a frame state appropriate for tile culling from a structural deck.gl viewport.
77
78
  *
79
+ * The viewport must describe the render camera, including its elevation. Geographic heights
80
+ * returned by `unprojectPosition` are used directly; adding a terrain offset only to traversal
81
+ * would make visibility, camera distances, and screen-space error disagree with rendering.
82
+ *
78
83
  * `options.timeSinceCameraMovement` can be supplied by a caller that retains camera pose across
79
84
  * calls. It defaults to a stationary camera so standalone callers do not defer requests.
80
85
  *
@@ -134,7 +139,7 @@ export function getFrameState(
134
139
  commonSpacePlanesToWGS84(viewport);
135
140
 
136
141
  const ViewportClass = viewport.constructor;
137
- const {longitude, latitude, width, bearing, zoom} = viewport;
142
+ const {longitude, latitude, width, bearing, zoom, position} = viewport;
138
143
  // @ts-ignore
139
144
  const topDownViewport = new ViewportClass({
140
145
  longitude,
@@ -143,9 +148,15 @@ export function getFrameState(
143
148
  width,
144
149
  bearing,
145
150
  zoom,
151
+ position,
146
152
  pitch: 0
147
153
  });
148
154
 
155
+ const verticalFieldOfView =
156
+ Number.isFinite(viewport.fovy) && Number(viewport.fovy) > 0 && Number(viewport.fovy) < 180
157
+ ? (Number(viewport.fovy) * Math.PI) / 180
158
+ : Math.PI / 3;
159
+
149
160
  // TODO: make a file/class for frameState and document what needs to be attached to this so that traversal can function
150
161
  return {
151
162
  camera: {
@@ -153,10 +164,7 @@ export function getFrameState(
153
164
  direction: cameraDirectionCartesian,
154
165
  up: cameraUpCartesian,
155
166
  cartographicPosition: cameraPositionCartographic,
156
- verticalFieldOfView:
157
- Number.isFinite(viewport.fovy) && Number(viewport.fovy) > 0
158
- ? (Number(viewport.fovy) * Math.PI) / 180
159
- : Math.PI / 3,
167
+ verticalFieldOfView,
160
168
  timeSinceMovement: options.timeSinceCameraMovement ?? Number.POSITIVE_INFINITY
161
169
  },
162
170
  viewport,
@@ -165,7 +173,7 @@ export function getFrameState(
165
173
  cullingVolume,
166
174
  clippingPlanes: viewport.clippingPlanes,
167
175
  frameNumber, // TODO: This can be the same between updates, what number is unique for between updates?
168
- sseDenominator: 1.15, // Assumes fovy = 60 degrees
176
+ sseDenominator: 2 * Math.tan(verticalFieldOfView / 2),
169
177
  // Tileset3D fills this immediately before traversal because it depends on the root volume.
170
178
  dynamicScreenSpaceErrorDensity: 0
171
179
  };
@@ -0,0 +1,35 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright vis.gl contributors
4
+
5
+ // TypeScript's legacy `node` resolution does not inspect package `exports`, but this public
6
+ // subpath is resolved by the package at runtime and avoids loading unrelated DGGS decoders.
7
+ // @ts-expect-error Conditional package subpath exports require a modern module resolver.
8
+ import {getS2Bounds} from '@math.gl/dggs/s2';
9
+ import {Ellipsoid, makeOBBFromRegion} from '@math.gl/geospatial';
10
+
11
+ /** Serialized S2 cell metadata used by 3D Tiles bounding-volume extensions. */
12
+ export type S2VolumeInfo = {
13
+ /** S2 key or token. */
14
+ token: string;
15
+ /** Minimum height in meters. */
16
+ minimumHeight: number;
17
+ /** Maximum height in meters. */
18
+ maximumHeight: number;
19
+ };
20
+
21
+ /**
22
+ * Converts an S2 cell and its vertical range to the runtime oriented-box representation.
23
+ *
24
+ * @param s2VolumeInfo - S2 cell token and height range.
25
+ * @returns A twelve-component center/half-axis oriented bounding box.
26
+ */
27
+ export function convertS2BoundingVolumeToOBB(s2VolumeInfo: S2VolumeInfo): number[] {
28
+ const [[west, south], [east, north]] = getS2Bounds(s2VolumeInfo.token);
29
+ const orientedBoundingBox = makeOBBFromRegion(
30
+ [west, south, east, north, s2VolumeInfo.minimumHeight, s2VolumeInfo.maximumHeight],
31
+ Ellipsoid.WGS84,
32
+ {units: 'degrees'}
33
+ );
34
+ return [...orientedBoundingBox.center, ...orientedBoundingBox.halfAxes];
35
+ }
package/src/types.ts CHANGED
@@ -46,6 +46,8 @@ export type GeospatialViewport = Viewport & {
46
46
  cameraDirection: [number, number, number];
47
47
  /** @todo This field is not represented in Deck.gl viewports. Can be removed in the next version */
48
48
  cameraUp: [number, number, number];
49
+ /** Camera target offset in meters from longitude/latitude, including elevation. */
50
+ position?: number[];
49
51
  longitude: number;
50
52
  latitude: number;
51
53
  bearing: number;