@loaders.gl/tiles 5.0.0-alpha.1 → 5.0.0-alpha.3

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 (161) hide show
  1. package/LICENSE +14 -3
  2. package/dist/dist.dev.js +17482 -3685
  3. package/dist/dist.min.js +4 -1
  4. package/dist/index.cjs +4594 -422
  5. package/dist/index.cjs.map +3 -4
  6. package/dist/index.d.ts +16 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +7 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/point-cloud/point-cloud-tile.d.ts.map +1 -1
  11. package/dist/point-cloud/point-cloud-tile.js +3 -0
  12. package/dist/point-cloud/point-cloud-tile.js.map +1 -1
  13. package/dist/point-cloud/point-cloud-tileset.d.ts +4 -0
  14. package/dist/point-cloud/point-cloud-tileset.d.ts.map +1 -1
  15. package/dist/point-cloud/point-cloud-tileset.js +54 -12
  16. package/dist/point-cloud/point-cloud-tileset.js.map +1 -1
  17. package/dist/point-cloud/types.d.ts +17 -0
  18. package/dist/point-cloud/types.d.ts.map +1 -1
  19. package/dist/point-cloud/types.js +3 -0
  20. package/dist/point-cloud/types.js.map +1 -1
  21. package/dist/raster-set.d.ts +37 -29
  22. package/dist/raster-set.d.ts.map +1 -1
  23. package/dist/raster-set.js +25 -2
  24. package/dist/raster-set.js.map +1 -1
  25. package/dist/spatial/format-spatial-reference.d.ts +67 -0
  26. package/dist/spatial/format-spatial-reference.d.ts.map +1 -0
  27. package/dist/spatial/format-spatial-reference.js +233 -0
  28. package/dist/spatial/format-spatial-reference.js.map +1 -0
  29. package/dist/spatial/get-spatial-coordinate-frame.d.ts +9 -0
  30. package/dist/spatial/get-spatial-coordinate-frame.d.ts.map +1 -0
  31. package/dist/spatial/get-spatial-coordinate-frame.js +58 -0
  32. package/dist/spatial/get-spatial-coordinate-frame.js.map +1 -0
  33. package/dist/spatial/i3s-elevation.d.ts +20 -0
  34. package/dist/spatial/i3s-elevation.d.ts.map +1 -0
  35. package/dist/spatial/i3s-elevation.js +90 -0
  36. package/dist/spatial/i3s-elevation.js.map +1 -0
  37. package/dist/spatial/i3s-spatial-transformer.d.ts +189 -0
  38. package/dist/spatial/i3s-spatial-transformer.d.ts.map +1 -0
  39. package/dist/spatial/i3s-spatial-transformer.js +686 -0
  40. package/dist/spatial/i3s-spatial-transformer.js.map +1 -0
  41. package/dist/spatial/spatial-coordinate-transformer.d.ts +48 -0
  42. package/dist/spatial/spatial-coordinate-transformer.d.ts.map +1 -0
  43. package/dist/spatial/spatial-coordinate-transformer.js +211 -0
  44. package/dist/spatial/spatial-coordinate-transformer.js.map +1 -0
  45. package/dist/spatial/spatial-resource-registry.d.ts +44 -0
  46. package/dist/spatial/spatial-resource-registry.d.ts.map +1 -0
  47. package/dist/spatial/spatial-resource-registry.js +67 -0
  48. package/dist/spatial/spatial-resource-registry.js.map +1 -0
  49. package/dist/spatial/spatial-types.d.ts +190 -0
  50. package/dist/spatial/spatial-types.d.ts.map +1 -0
  51. package/dist/spatial/spatial-types.js +192 -0
  52. package/dist/spatial/spatial-types.js.map +1 -0
  53. package/dist/tileset-3d/common/tile-3d.d.ts +147 -4
  54. package/dist/tileset-3d/common/tile-3d.d.ts.map +1 -1
  55. package/dist/tileset-3d/common/tile-3d.js +358 -76
  56. package/dist/tileset-3d/common/tile-3d.js.map +1 -1
  57. package/dist/tileset-3d/common/tileset-3d.d.ts +168 -3
  58. package/dist/tileset-3d/common/tileset-3d.d.ts.map +1 -1
  59. package/dist/tileset-3d/common/tileset-3d.js +306 -23
  60. package/dist/tileset-3d/common/tileset-3d.js.map +1 -1
  61. package/dist/tileset-3d/common/tileset-cache.d.ts +11 -1
  62. package/dist/tileset-3d/common/tileset-cache.d.ts.map +1 -1
  63. package/dist/tileset-3d/common/tileset-cache.js +11 -3
  64. package/dist/tileset-3d/common/tileset-cache.js.map +1 -1
  65. package/dist/tileset-3d/common/tileset-source.d.ts +32 -0
  66. package/dist/tileset-3d/common/tileset-source.d.ts.map +1 -1
  67. package/dist/tileset-3d/common/tileset-source.js.map +1 -1
  68. package/dist/tileset-3d/common/tileset-traverser.d.ts +19 -1
  69. package/dist/tileset-3d/common/tileset-traverser.d.ts.map +1 -1
  70. package/dist/tileset-3d/common/tileset-traverser.js +39 -7
  71. package/dist/tileset-3d/common/tileset-traverser.js.map +1 -1
  72. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts +136 -0
  73. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts.map +1 -0
  74. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js +384 -0
  75. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js.map +1 -0
  76. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts +86 -3
  77. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts.map +1 -1
  78. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js +196 -21
  79. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js.map +1 -1
  80. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts +36 -0
  81. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts.map +1 -1
  82. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js +66 -0
  83. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js.map +1 -1
  84. package/dist/tileset-3d/format-i3s/i3s-pending-tiles-register.d.ts.map +1 -1
  85. package/dist/tileset-3d/format-i3s/i3s-pending-tiles-register.js +3 -0
  86. package/dist/tileset-3d/format-i3s/i3s-pending-tiles-register.js.map +1 -1
  87. package/dist/tileset-3d/format-i3s/i3s-source.d.ts +14 -1
  88. package/dist/tileset-3d/format-i3s/i3s-source.d.ts.map +1 -1
  89. package/dist/tileset-3d/format-i3s/i3s-source.js +107 -17
  90. package/dist/tileset-3d/format-i3s/i3s-source.js.map +1 -1
  91. package/dist/tileset-3d/format-i3s/i3s-tile-manager.d.ts.map +1 -1
  92. package/dist/tileset-3d/format-i3s/i3s-tile-manager.js +3 -0
  93. package/dist/tileset-3d/format-i3s/i3s-tile-manager.js.map +1 -1
  94. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.d.ts.map +1 -1
  95. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js +3 -0
  96. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js.map +1 -1
  97. package/dist/tileset-3d/helpers/3d-tiles-options.d.ts.map +1 -1
  98. package/dist/tileset-3d/helpers/3d-tiles-options.js +3 -0
  99. package/dist/tileset-3d/helpers/3d-tiles-options.js.map +1 -1
  100. package/dist/tileset-3d/helpers/bounding-volume.d.ts.map +1 -1
  101. package/dist/tileset-3d/helpers/bounding-volume.js +4 -33
  102. package/dist/tileset-3d/helpers/bounding-volume.js.map +1 -1
  103. package/dist/tileset-3d/helpers/frame-state.d.ts +62 -2
  104. package/dist/tileset-3d/helpers/frame-state.d.ts.map +1 -1
  105. package/dist/tileset-3d/helpers/frame-state.js +85 -9
  106. package/dist/tileset-3d/helpers/frame-state.js.map +1 -1
  107. package/dist/tileset-3d/helpers/i3s-lod.d.ts.map +1 -1
  108. package/dist/tileset-3d/helpers/i3s-lod.js +20 -8
  109. package/dist/tileset-3d/helpers/i3s-lod.js.map +1 -1
  110. package/dist/tileset-3d/helpers/tiles-3d-lod.d.ts +61 -9
  111. package/dist/tileset-3d/helpers/tiles-3d-lod.d.ts.map +1 -1
  112. package/dist/tileset-3d/helpers/tiles-3d-lod.js +198 -93
  113. package/dist/tileset-3d/helpers/tiles-3d-lod.js.map +1 -1
  114. package/dist/tileset-3d/helpers/tiles-3d-request-priority.d.ts +134 -0
  115. package/dist/tileset-3d/helpers/tiles-3d-request-priority.d.ts.map +1 -0
  116. package/dist/tileset-3d/helpers/tiles-3d-request-priority.js +179 -0
  117. package/dist/tileset-3d/helpers/tiles-3d-request-priority.js.map +1 -0
  118. package/dist/types.d.ts +5 -0
  119. package/dist/types.d.ts.map +1 -1
  120. package/dist/types.js +3 -0
  121. package/dist/types.js.map +1 -1
  122. package/dist/utils/doubly-linked-list.d.ts.map +1 -1
  123. package/dist/utils/doubly-linked-list.js +1 -0
  124. package/dist/utils/doubly-linked-list.js.map +1 -1
  125. package/dist/utils/managed-array.d.ts.map +1 -1
  126. package/dist/utils/managed-array.js +1 -0
  127. package/dist/utils/managed-array.js.map +1 -1
  128. package/package.json +12 -9
  129. package/src/index.ts +61 -1
  130. package/src/point-cloud/point-cloud-tile.ts +4 -0
  131. package/src/point-cloud/point-cloud-tileset.ts +70 -12
  132. package/src/point-cloud/types.ts +21 -0
  133. package/src/raster-set.ts +118 -45
  134. package/src/spatial/format-spatial-reference.ts +311 -0
  135. package/src/spatial/get-spatial-coordinate-frame.ts +74 -0
  136. package/src/spatial/i3s-elevation.ts +96 -0
  137. package/src/spatial/i3s-spatial-transformer.ts +957 -0
  138. package/src/spatial/spatial-coordinate-transformer.ts +276 -0
  139. package/src/spatial/spatial-resource-registry.ts +79 -0
  140. package/src/spatial/spatial-types.ts +422 -0
  141. package/src/tileset-3d/common/tile-3d.ts +435 -84
  142. package/src/tileset-3d/common/tileset-3d.ts +445 -23
  143. package/src/tileset-3d/common/tileset-cache.ts +15 -7
  144. package/src/tileset-3d/common/tileset-source.ts +36 -0
  145. package/src/tileset-3d/common/tileset-traverser.ts +40 -7
  146. package/src/tileset-3d/format-3d-tiles/implicit-tiling.ts +612 -0
  147. package/src/tileset-3d/format-3d-tiles/tiles-3d-source.ts +254 -21
  148. package/src/tileset-3d/format-3d-tiles/tileset-3d-traverser.ts +77 -0
  149. package/src/tileset-3d/format-i3s/i3s-pending-tiles-register.ts +4 -0
  150. package/src/tileset-3d/format-i3s/i3s-source.ts +119 -18
  151. package/src/tileset-3d/format-i3s/i3s-tile-manager.ts +4 -0
  152. package/src/tileset-3d/format-i3s/i3s-tileset-traverser.ts +4 -0
  153. package/src/tileset-3d/helpers/3d-tiles-options.ts +4 -0
  154. package/src/tileset-3d/helpers/bounding-volume.ts +5 -55
  155. package/src/tileset-3d/helpers/frame-state.ts +142 -11
  156. package/src/tileset-3d/helpers/i3s-lod.ts +22 -8
  157. package/src/tileset-3d/helpers/tiles-3d-lod.ts +291 -112
  158. package/src/tileset-3d/helpers/tiles-3d-request-priority.ts +301 -0
  159. package/src/types.ts +9 -0
  160. package/src/utils/doubly-linked-list.ts +2 -0
  161. package/src/utils/managed-array.ts +2 -0
@@ -7,12 +7,13 @@
7
7
 
8
8
  import {Vector3, Matrix4} from '@math.gl/core';
9
9
  import {CullingVolume} from '@math.gl/culling';
10
+ import type {CullingResult} from '@math.gl/culling';
10
11
 
11
12
  // Note: circular dependency
12
13
  import type {Tileset3D} from './tileset-3d';
13
14
  import type {DoublyLinkedListNode} from '../../utils/doubly-linked-list-node';
14
15
  import {LOD_METRIC_TYPE, TILE_REFINEMENT, TILE_CONTENT_STATE, TILESET_TYPE} from '../../constants';
15
- import type {TileContentLoadResult} from './tileset-source';
16
+ import type {TileChildrenLoadResult, TileContentLoadResult} from './tileset-source';
16
17
 
17
18
  import {FrameState} from '../helpers/frame-state';
18
19
  import {
@@ -23,9 +24,67 @@ import {
23
24
  import {getTiles3DScreenSpaceError} from '../helpers/tiles-3d-lod';
24
25
  import {getProjectedRadius} from '../helpers/i3s-lod';
25
26
  import {TilesetTraverser} from './tileset-traverser';
27
+ import {
28
+ calculateFoveatedFactor,
29
+ calculateTileRequestPriority,
30
+ isFoveatedRequestDelayActive,
31
+ isFoveatedRequestDeferred,
32
+ isProgressiveResolutionPriority
33
+ } from '../helpers/tiles-3d-request-priority';
26
34
 
27
35
  const scratchVector = new Vector3();
28
36
 
37
+ /**
38
+ * Classifies render-content visibility across all content volumes owned by a tile.
39
+ *
40
+ * Content volumes form a union: one visible content entry keeps the tile renderable even when
41
+ * another entry is outside the frustum or clipping planes. The traversal volume is intentionally
42
+ * not used here except as a fallback when a tile has no content-specific volume. This separation
43
+ * keeps clipping render-only and prevents a tight content volume from pruning descendants.
44
+ *
45
+ * @param contentVolumes - Transformed content volumes in source order.
46
+ * @param fallbackVolume - Tile volume used when content does not declare its own volume.
47
+ * @param cullingVolume - Current view frustum.
48
+ * @param visibilityPlaneMask - Parent visibility mask optimization.
49
+ * @param clippingPlanes - Optional world-space planes applied to render content only.
50
+ * @returns Union visibility classification for the renderable content.
51
+ */
52
+ export function getContentVisibility(
53
+ contentVolumes: any[],
54
+ fallbackVolume: any,
55
+ cullingVolume: CullingVolume,
56
+ visibilityPlaneMask: number | undefined,
57
+ clippingPlanes: any[] = []
58
+ ): CullingResult {
59
+ const volumes = contentVolumes.length ? contentVolumes : [fallbackVolume];
60
+ let visibleVolume = false;
61
+ let intersectingVolume = false;
62
+ for (const contentVolume of volumes) {
63
+ let visibility: CullingResult = 'inside';
64
+ if (visibilityPlaneMask !== CullingVolume.MASK_INSIDE) {
65
+ visibility = cullingVolume.computeVisibility(contentVolume);
66
+ if (visibility === 'outside') {
67
+ continue;
68
+ }
69
+ }
70
+ let clipped = false;
71
+ for (const clippingPlane of clippingPlanes) {
72
+ if (contentVolume.intersectPlane(clippingPlane) === 'outside') {
73
+ clipped = true;
74
+ break;
75
+ }
76
+ }
77
+ if (!clipped) {
78
+ visibleVolume = true;
79
+ intersectingVolume = intersectingVolume || visibility === 'intersecting';
80
+ }
81
+ }
82
+ if (!visibleVolume) {
83
+ return 'outside';
84
+ }
85
+ return intersectingVolume ? 'intersecting' : 'inside';
86
+ }
87
+
29
88
  function defined(x) {
30
89
  return x !== undefined && x !== null;
31
90
  }
@@ -44,6 +103,9 @@ export type Tile3DProps = {
44
103
  extendedId: string;
45
104
  };
46
105
 
106
+ /** Lifecycle of a source-managed lazy child-header group. */
107
+ export type TileChildrenState = 'unloaded' | 'loading' | 'ready' | 'failed';
108
+
47
109
  /**
48
110
  * A Tile3DHeader represents a tile as Tileset3D. When a tile is first created, its content is not loaded;
49
111
  * the content is loaded on-demand when needed based on the view.
@@ -59,6 +121,8 @@ export class Tile3D {
59
121
  refine: TILE_REFINEMENT;
60
122
  type: string;
61
123
  contentUrl: string;
124
+ /** Resolved render-content URLs in source order; empty for contentless tiles. */
125
+ contentUrls: string[] = [];
62
126
  /** Different refinement algorithms used by I3S and 3D tiles */
63
127
  lodMetricType = 'geometricError';
64
128
  /**
@@ -81,11 +145,24 @@ export class Tile3D {
81
145
  * not the content's metadata in the tileset JSON file.
82
146
  */
83
147
  content: any = null;
148
+ /** Loaded payloads in the same order as {@link contentUrls}; `content` remains the primary payload. */
149
+ contents: any[] = [];
150
+ /**
151
+ * Metadata entity attached to this tile in the source tileset, if present.
152
+ *
153
+ * This is the raw 3D Tiles metadata reference. The generic tiles runtime preserves it without
154
+ * decoding schema classes or binary property tables.
155
+ */
156
+ metadata: Record<string, any> | null = null;
157
+ /** Raw metadata entities attached to tile contents, in content source order. */
158
+ contentMetadata: Array<Record<string, any> | null> = [];
84
159
  contentState: number = TILE_CONTENT_STATE.UNLOADED;
85
160
  gpuMemoryUsageInBytes: number = 0;
86
161
 
87
162
  /** The tile's children - an array of Tile3D objects. */
88
163
  children: Tile3D[] = [];
164
+ /** Current lifecycle state for a lazy implicit subtree rooted at this tile. */
165
+ childrenState: TileChildrenState = 'ready';
89
166
  depth: number = 0;
90
167
  viewportIds: any[] = [];
91
168
  transform = new Matrix4();
@@ -111,17 +188,46 @@ export class Tile3D {
111
188
  private _expireDate: any = null;
112
189
  private _expiredContent: any = null;
113
190
 
191
+ /** In-flight source request shared by repeated traversal frames. */
192
+ private _childrenPromise: Promise<TileChildrenLoadResult> | null = null;
193
+
114
194
  private _boundingBox?: CartographicBounds = undefined;
115
195
 
116
196
  /** updated every frame for tree traversal and rendering optimizations: */
117
197
  public _distanceToCamera: number = 0;
118
198
  _screenSpaceError: number = 0;
199
+ /** SSE calculated at the reduced viewport height used to prioritize coarse coverage. */
200
+ _screenSpaceErrorProgressiveResolution: number = 0;
201
+ /** Whether this tile should be promoted into the progressive-resolution request pass. */
202
+ _priorityProgressiveResolution: boolean = false;
203
+ /** Angular distance from the camera view axis used to prioritize center tiles. */
204
+ _foveatedFactor: number = 0;
205
+ /** Whether this peripheral request is currently inside the camera-motion deferral window. */
206
+ priorityDeferred: boolean = false;
119
207
  private _visibilityPlaneMask: any;
120
208
  private _visible: boolean | undefined = undefined;
121
209
 
122
210
  private _contentBoundingVolume: any;
211
+ private _contentBoundingVolumes: any[] = [];
212
+ /** Source content-volume headers retained after render content is unloaded. */
213
+ private _contentBoundingVolumeHeaders: any[] = [];
123
214
  private _viewerRequestVolume: any;
124
215
 
216
+ /** Bounding volume used to cull the tile's first renderable content entry. */
217
+ get contentBoundingVolume(): any {
218
+ return this._contentBoundingVolume;
219
+ }
220
+
221
+ /** All transformed render-content bounding volumes, including tile-volume fallbacks. */
222
+ get contentBoundingVolumes(): any[] {
223
+ return this._contentBoundingVolumes;
224
+ }
225
+
226
+ /** Bounding volume that limits when this tile may be requested, when declared. */
227
+ get viewerRequestVolume(): any {
228
+ return this._viewerRequestVolume;
229
+ }
230
+
125
231
  _initialTransform: Matrix4 = new Matrix4();
126
232
 
127
233
  // Used by traverser, cannot be marked private
@@ -176,6 +282,13 @@ export class Tile3D {
176
282
  this.refine = this._getRefine(header.refine);
177
283
  this.type = header.type;
178
284
  this.contentUrl = header.contentUrl;
285
+ this.contentUrls = header.contentUrls || (header.contentUrl ? [header.contentUrl] : []);
286
+ this.metadata = header.metadata || null;
287
+ const contentHeaders = Array.isArray(header.content) ? header.content : [header.content];
288
+ this.contentMetadata = contentHeaders
289
+ .filter(Boolean)
290
+ .map(contentHeader => contentHeader.metadata || null);
291
+ this.childrenState = header.implicitSubtree ? 'unloaded' : 'ready';
179
292
 
180
293
  this._initializeLodMetric(header);
181
294
  this._initializeTransforms(header);
@@ -214,7 +327,24 @@ export class Tile3D {
214
327
 
215
328
  /** Returns true if tile has children */
216
329
  get hasChildren() {
217
- return this.children.length > 0 || (this.header.children && this.header.children.length > 0);
330
+ return (
331
+ this.children.length > 0 ||
332
+ (this.header.children && this.header.children.length > 0) ||
333
+ Boolean(this.header.implicitSubtree && this.childrenState !== 'ready')
334
+ );
335
+ }
336
+
337
+ /** Returns whether traversal may request an unresolved lazy child-header group. */
338
+ get hasUnloadedChildren(): boolean {
339
+ return Boolean(
340
+ this.header.implicitSubtree &&
341
+ (this.childrenState === 'unloaded' || this.childrenState === 'failed')
342
+ );
343
+ }
344
+
345
+ /** Returns whether a lazy child-header request is currently in flight. */
346
+ get childrenLoading(): boolean {
347
+ return this.childrenState === 'loading';
218
348
  }
219
349
 
220
350
  /**
@@ -287,13 +417,28 @@ export class Tile3D {
287
417
  return this._boundingBox;
288
418
  }
289
419
 
290
- /** Get the tile's screen space error. */
291
- getScreenSpaceError(frameState, useParentLodMetric) {
420
+ /**
421
+ * Gets the tile's screen-space error for normal refinement or a reduced-height priority pass.
422
+ *
423
+ * I3S already supplies a screen-oriented LOD metric and intentionally ignores the optional 3D
424
+ * Tiles progressive-resolution height fraction.
425
+ *
426
+ * @param frameState - Current camera and viewport measurements.
427
+ * @param useParentLodMetric - Whether to substitute the parent LOD metric.
428
+ * @param viewportHeightFraction - Fraction of viewport height represented by the priority pass.
429
+ * @returns Screen-space LOD error for the current tileset format.
430
+ */
431
+ getScreenSpaceError(frameState, useParentLodMetric, viewportHeightFraction = 1) {
292
432
  switch (this.tileset.type) {
293
433
  case TILESET_TYPE.I3S:
294
434
  return getProjectedRadius(this, frameState);
295
435
  case TILESET_TYPE.TILES3D:
296
- return getTiles3DScreenSpaceError(this, frameState, useParentLodMetric);
436
+ return getTiles3DScreenSpaceError(
437
+ this,
438
+ frameState,
439
+ useParentLodMetric,
440
+ viewportHeightFraction
441
+ );
297
442
  default:
298
443
  // eslint-disable-next-line
299
444
  throw new Error('Unsupported tileset type');
@@ -312,15 +457,43 @@ export class Tile3D {
312
457
  * Memory usage of tile on GPU
313
458
  */
314
459
  _getGpuMemoryUsageInBytes(): number {
315
- return this.content.gpuMemoryUsageInBytes || this.content.byteLength || 0;
460
+ const contents = this.contents.length ? this.contents : [this.content];
461
+ return contents.reduce(
462
+ (total, content) => total + (content?.gpuMemoryUsageInBytes || content?.byteLength || 0),
463
+ 0
464
+ );
465
+ }
466
+
467
+ /**
468
+ * Returns scheduler priority for render-content requests.
469
+ *
470
+ * @returns Numeric priority, or a negative value when the queued request should be cancelled.
471
+ */
472
+ _getPriority(): number {
473
+ return this._calculateRequestPriority(false);
474
+ }
475
+
476
+ /**
477
+ * Returns scheduler priority for lazy child-header requests.
478
+ *
479
+ * Contentless implicit placeholders intentionally retain `UNLOADED` content state, so metadata
480
+ * scheduling skips only that render-content cancellation check and preserves every visibility,
481
+ * recency, progressive, foveated, and motion-deferral rule.
482
+ *
483
+ * @returns Numeric priority, or a negative value when the queued request should be cancelled.
484
+ */
485
+ private _getChildrenPriority(): number {
486
+ return this._calculateRequestPriority(true);
316
487
  }
317
488
 
318
- /*
319
- * If skipLevelOfDetail is off try to load child tiles as soon as possible so that their parent can refine sooner.
320
- * Tiles are prioritized by screen space error.
489
+ /**
490
+ * Calculates the common scheduler priority for content or hierarchy metadata.
491
+ *
492
+ * @param allowUnloadedContent - Whether an empty placeholder may request child metadata.
493
+ * @returns Numeric priority, or a negative value when the queued request should be cancelled.
321
494
  */
322
495
  // eslint-disable-next-line complexity
323
- _getPriority() {
496
+ private _calculateRequestPriority(allowUnloadedContent: boolean): number {
324
497
  const traverser = this.tileset._traverser;
325
498
  const {skipLevelOfDetail} = traverser.options;
326
499
 
@@ -340,7 +513,13 @@ export class Tile3D {
340
513
  if (this.tileset._frameNumber - this._touchedFrame >= 1) {
341
514
  return -1;
342
515
  }
343
- if (this.contentState === TILE_CONTENT_STATE.UNLOADED) {
516
+ // RequestScheduler re-evaluates this callback before a queued request receives a slot. A
517
+ // negative priority cancels work that became deferred after it was queued; the scheduled
518
+ // follow-up traversal submits it again after the motion delay expires.
519
+ if (this.priorityDeferred) {
520
+ return -1;
521
+ }
522
+ if (!allowUnloadedContent && this.contentState === TILE_CONTENT_STATE.UNLOADED) {
344
523
  return -1;
345
524
  }
346
525
 
@@ -354,8 +533,15 @@ export class Tile3D {
354
533
 
355
534
  const rootScreenSpaceError = traverser.root ? traverser.root._screenSpaceError : 0.0;
356
535
 
357
- // Map higher SSE to lower values (e.g. root tile is highest priority)
358
- return Math.max(rootScreenSpaceError - screenSpaceError, 0);
536
+ // Map higher SSE to lower values (e.g. root tile is highest priority), then combine the
537
+ // existing order with independent progressive and foveated priority bands.
538
+ const reverseScreenSpaceError = Math.max(rootScreenSpaceError - screenSpaceError, 0);
539
+ return calculateTileRequestPriority({
540
+ priorityProgressiveResolution: this._priorityProgressiveResolution,
541
+ foveatedFactor: this._foveatedFactor,
542
+ reverseScreenSpaceError,
543
+ rootScreenSpaceError
544
+ });
359
545
  }
360
546
 
361
547
  /**
@@ -394,6 +580,11 @@ export class Tile3D {
394
580
  try {
395
581
  const loadResult = await this.tileset.source.loadTileContent(this);
396
582
 
583
+ if (loadResult.contents) {
584
+ this.contents = loadResult.contents;
585
+ this.content = this.contents[0] || null;
586
+ }
587
+
397
588
  if (this.tileset.options.contentLoader) {
398
589
  await this.tileset.options.contentLoader(this);
399
590
  }
@@ -410,11 +601,107 @@ export class Tile3D {
410
601
  }
411
602
  }
412
603
 
604
+ /**
605
+ * Requests and installs this tile's lazy child-header group through its source.
606
+ *
607
+ * The same request scheduler and SSE/foveated priority value used for render content are reused
608
+ * for subtree metadata. Repeated traversal frames share one promise, and scheduler cancellation
609
+ * restores the unloaded state so a later frame can retry.
610
+ *
611
+ * @param frameState - View state that made the subtree eligible for refinement.
612
+ * @returns Source load result, or an unloaded result if scheduling was cancelled.
613
+ */
614
+ async loadChildren(frameState: FrameState): Promise<TileChildrenLoadResult> {
615
+ if (!this.header.implicitSubtree || this.childrenState === 'ready') {
616
+ return {loaded: false, tileCount: 0, childSubtreeCount: 0};
617
+ }
618
+ if (this._childrenPromise) {
619
+ return await this._childrenPromise;
620
+ }
621
+ if (!this.tileset.source.loadTileChildren) {
622
+ throw new Error('Tileset source does not support lazy tile children');
623
+ }
624
+
625
+ this.childrenState = 'loading';
626
+ this._childrenPromise = this._loadChildrenWithScheduler(frameState);
627
+ try {
628
+ return await this._childrenPromise;
629
+ } finally {
630
+ this._childrenPromise = null;
631
+ }
632
+ }
633
+
634
+ /**
635
+ * Runs one lazy child-header load after obtaining a scheduler token.
636
+ *
637
+ * @param frameState - View state forwarded to the source implementation.
638
+ * @returns Source load result.
639
+ */
640
+ private async _loadChildrenWithScheduler(
641
+ frameState: FrameState
642
+ ): Promise<TileChildrenLoadResult> {
643
+ const requestToken = await this.tileset._requestScheduler.scheduleRequest(
644
+ `${this.id}:implicit-subtree`,
645
+ this._getChildrenPriority.bind(this)
646
+ );
647
+ if (!requestToken) {
648
+ this.childrenState = 'unloaded';
649
+ return {loaded: false, tileCount: 0, childSubtreeCount: 0};
650
+ }
651
+
652
+ try {
653
+ const loadResult = await this.tileset.source.loadTileChildren!(this, frameState);
654
+ this.childrenState = loadResult.loaded ? 'ready' : 'unloaded';
655
+ return loadResult;
656
+ } catch (error) {
657
+ this.childrenState = 'failed';
658
+ throw error;
659
+ } finally {
660
+ requestToken.done();
661
+ }
662
+ }
663
+
664
+ /**
665
+ * Replaces a contentless implicit placeholder with its materialized subtree-root header.
666
+ *
667
+ * Transform state is intentionally preserved: the placeholder already owns the composed
668
+ * transform, and reapplying the root transform here would double-transform bounds and geometric
669
+ * error. Content and raw geometric error are reinitialized from the newly available header.
670
+ *
671
+ * @param materializedHeader - Available root header produced from one subtree resource.
672
+ */
673
+ applyImplicitSubtreeHeader(materializedHeader: Record<string, any>): void {
674
+ const existingTransform = this.header.transform;
675
+ const existingTransformMatrix = this.header.transformMatrix;
676
+ this.header = {
677
+ ...this.header,
678
+ ...materializedHeader,
679
+ transform: existingTransform,
680
+ transformMatrix: existingTransformMatrix,
681
+ implicitSubtree: undefined
682
+ };
683
+ this.refine = this._getRefine(materializedHeader.refine);
684
+ this.type = materializedHeader.type;
685
+ this.contentUrl = materializedHeader.contentUrl;
686
+ this.contentUrls =
687
+ materializedHeader.contentUrls ||
688
+ (materializedHeader.contentUrl ? [materializedHeader.contentUrl] : []);
689
+ this._initializeLodMetric(materializedHeader);
690
+ this._updateLodMetricScale();
691
+ this._initializeBoundingVolumes(this.header);
692
+ // Use the merged header so inherited viewer-request-volume metadata remains available when a
693
+ // materialized implicit tile has no render content of its own.
694
+ this._initializeContent(this.header);
695
+ }
696
+
413
697
  // Unloads the tile's content.
414
698
  unloadContent() {
415
- if (this.content && this.content.destroy) {
416
- this.content.destroy();
699
+ for (const content of this.contents.length ? this.contents : [this.content]) {
700
+ if (content && content.destroy) {
701
+ content.destroy();
702
+ }
417
703
  }
704
+ this.contents = [];
418
705
  this.content = null;
419
706
  if (this.header.content && this.header.content.destroy) {
420
707
  this.header.content.destroy();
@@ -450,6 +737,7 @@ export class Tile3D {
450
737
 
451
738
  this._distanceToCamera = this.distanceToTile(frameState);
452
739
  this._screenSpaceError = this.getScreenSpaceError(frameState, false);
740
+ this._updateRequestPriorityMetrics(frameState);
453
741
  this._visibilityPlaneMask = this.visibility(frameState, parentVisibilityPlaneMask); // Use parent's plane mask to speed up visibility test
454
742
  this._visible = this._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE;
455
743
  this._inRequestVolume = this.insideViewerRequestVolume(frameState);
@@ -458,6 +746,71 @@ export class Tile3D {
458
746
  this.viewportIds = viewportIds;
459
747
  }
460
748
 
749
+ /**
750
+ * Updates progressive-resolution and foveated request metrics for a 3D Tiles traversal frame.
751
+ *
752
+ * I3S retains its format-specific screen-threshold ordering and is explicitly isolated from
753
+ * geometric-error scheduling. Orthographic viewports still receive progressive coverage, but
754
+ * angular foveation is disabled because an orthographic frustum has no perspective field of view.
755
+ *
756
+ * @param frameState - Current camera and viewport measurements.
757
+ */
758
+ _updateRequestPriorityMetrics(frameState: FrameState): void {
759
+ if (this.tileset.type !== TILESET_TYPE.TILES3D) {
760
+ this._screenSpaceErrorProgressiveResolution = this._screenSpaceError;
761
+ this._priorityProgressiveResolution = false;
762
+ this._foveatedFactor = 0;
763
+ this.priorityDeferred = false;
764
+ return;
765
+ }
766
+
767
+ const options = this.tileset.options;
768
+ const progressiveResolutionHeightFraction = options.progressiveResolutionHeightFraction;
769
+ this._screenSpaceErrorProgressiveResolution = this.getScreenSpaceError(
770
+ frameState,
771
+ false,
772
+ progressiveResolutionHeightFraction
773
+ );
774
+ this._priorityProgressiveResolution = isProgressiveResolutionPriority(
775
+ this._screenSpaceErrorProgressiveResolution,
776
+ this.parent?._screenSpaceErrorProgressiveResolution,
777
+ this.tileset.memoryAdjustedScreenSpaceError,
778
+ progressiveResolutionHeightFraction
779
+ );
780
+
781
+ if (
782
+ !options.foveatedScreenSpaceError ||
783
+ frameState.viewport?.orthographic ||
784
+ !frameState.camera?.position ||
785
+ !frameState.camera?.direction
786
+ ) {
787
+ this._foveatedFactor = 0;
788
+ this.priorityDeferred = false;
789
+ return;
790
+ }
791
+
792
+ this._foveatedFactor = calculateFoveatedFactor(this.boundingVolume, frameState.camera);
793
+ const deferralEligible = isFoveatedRequestDeferred({
794
+ refinement: this.refine,
795
+ skipLevelOfDetail: this.tileset._traverser.options.skipLevelOfDetail,
796
+ foveatedScreenSpaceError: options.foveatedScreenSpaceError,
797
+ foveatedConeSize: options.foveatedConeSize,
798
+ minimumScreenSpaceErrorRelaxation: options.foveatedMinimumScreenSpaceErrorRelaxation,
799
+ interpolationCallback: options.foveatedInterpolationCallback,
800
+ foveatedFactor: this._foveatedFactor,
801
+ verticalFieldOfView: frameState.camera.verticalFieldOfView,
802
+ screenSpaceError: this._screenSpaceError,
803
+ parentScreenSpaceError: this.parent?._screenSpaceError,
804
+ maximumScreenSpaceError: this.tileset.memoryAdjustedScreenSpaceError,
805
+ priorityProgressiveResolution: this._priorityProgressiveResolution
806
+ });
807
+ this.priorityDeferred = isFoveatedRequestDelayActive(
808
+ deferralEligible,
809
+ frameState.camera.timeSinceMovement,
810
+ options.foveatedTimeDelay
811
+ );
812
+ }
813
+
461
814
  // Determines whether the tile's bounding volume intersects the culling volume.
462
815
  // @param {FrameState} frameState The frame state.
463
816
  // @param {Number} parentVisibilityPlaneMask The parent's plane mask to speed up the visibility check.
@@ -483,47 +836,22 @@ export class Tile3D {
483
836
  return cullingVolume.computeVisibilityWithPlaneMask(boundingVolume, parentVisibilityPlaneMask);
484
837
  }
485
838
 
486
- // Assuming the tile's bounding volume intersects the culling volume, determines
487
- // whether the tile's content's bounding volume intersects the culling volume.
488
- // @param {FrameState} frameState The frame state.
489
- // @returns {Intersect} The result of the intersection: the tile's content is completely outside, completely inside, or intersecting the culling volume.
490
- contentVisibility() {
491
- return true;
492
-
493
- // TODO restore
494
- /*
495
- // Assumes the tile's bounding volume intersects the culling volume already, so
496
- // just return Intersect.INSIDE if there is no content bounding volume.
497
- if (!defined(this.contentBoundingVolume)) {
498
- return Intersect.INSIDE;
499
- }
500
-
501
- if (this._visibilityPlaneMask === CullingVolume.MASK_INSIDE) {
502
- // The tile's bounding volume is completely inside the culling volume so
503
- // the content bounding volume must also be inside.
504
- return Intersect.INSIDE;
505
- }
506
-
507
- // PERFORMANCE_IDEA: is it possible to burn less CPU on this test since we know the
508
- // tile's (not the content's) bounding volume intersects the culling volume?
509
- const cullingVolume = frameState.cullingVolume;
510
- const boundingVolume = tile.contentBoundingVolume;
511
-
512
- const tileset = this.tileset;
513
- const clippingPlanes = tileset.clippingPlanes;
514
- if (defined(clippingPlanes) && clippingPlanes.enabled) {
515
- const intersection = clippingPlanes.computeIntersectionWithBoundingVolume(
516
- boundingVolume,
517
- tileset.clippingPlanesOriginMatrix
518
- );
519
- this._isClipped = intersection !== Intersect.INSIDE;
520
- if (intersection === Intersect.OUTSIDE) {
521
- return Intersect.OUTSIDE;
522
- }
523
- }
524
-
525
- return cullingVolume.computeVisibility(boundingVolume);
526
- */
839
+ /**
840
+ * Determines whether renderable content intersects the view and configured clipping planes.
841
+ *
842
+ * Clipping planes are render-only constraints; traversal continues to use the tile volume.
843
+ *
844
+ * @param frameState Current camera, culling, and optional clipping-plane state.
845
+ * @returns The content visibility classification.
846
+ */
847
+ contentVisibility(frameState: FrameState): CullingResult {
848
+ return getContentVisibility(
849
+ this._contentBoundingVolumes,
850
+ this.boundingVolume,
851
+ frameState.cullingVolume,
852
+ this._visibilityPlaneMask,
853
+ frameState.clippingPlanes
854
+ );
527
855
  }
528
856
 
529
857
  /**
@@ -649,6 +977,7 @@ export class Tile3D {
649
977
 
650
978
  _initializeBoundingVolumes(tileHeader) {
651
979
  this._contentBoundingVolume = null;
980
+ this._contentBoundingVolumeHeaders = [];
652
981
  this._viewerRequestVolume = null;
653
982
 
654
983
  this._updateBoundingVolume(tileHeader);
@@ -657,6 +986,7 @@ export class Tile3D {
657
986
  _initializeContent(tileHeader) {
658
987
  // Empty tile by default
659
988
  this.content = {_tileset: this.tileset, _tile: this};
989
+ this.contents = [];
660
990
  this.hasEmptyContent = true;
661
991
  this.contentState = TILE_CONTENT_STATE.UNLOADED;
662
992
 
@@ -664,7 +994,7 @@ export class Tile3D {
664
994
  // This is `false` until the tile's content is loaded.
665
995
  this.hasTilesetContent = false;
666
996
 
667
- if (tileHeader.contentUrl) {
997
+ if (tileHeader.contentUrl || tileHeader.contentUrls?.length) {
668
998
  this.content = null;
669
999
  this.hasEmptyContent = false;
670
1000
  }
@@ -679,6 +1009,10 @@ export class Tile3D {
679
1009
  this._distanceToCamera = 0;
680
1010
  this._centerZDepth = 0;
681
1011
  this._screenSpaceError = 0;
1012
+ this._screenSpaceErrorProgressiveResolution = 0;
1013
+ this._priorityProgressiveResolution = false;
1014
+ this._foveatedFactor = 0;
1015
+ this.priorityDeferred = false;
682
1016
  this._visibilityPlaneMask = CullingVolume.MASK_INDETERMINATE;
683
1017
  this._visible = undefined;
684
1018
  this._inRequestVolume = false;
@@ -700,19 +1034,31 @@ export class Tile3D {
700
1034
  return refine || (this.parent && this.parent.refine) || TILE_REFINEMENT.REPLACE;
701
1035
  }
702
1036
 
703
- _isTileset() {
704
- return this.contentUrl.indexOf('.json') !== -1;
1037
+ /**
1038
+ * Returns whether loaded content is an external tileset.
1039
+ *
1040
+ * Parsed shape is authoritative because valid external resources may be extensionless, signed,
1041
+ * or use a misleading filename. This method is called only after source loading has populated
1042
+ * `content`.
1043
+ */
1044
+ _isTileset(): boolean {
1045
+ return (this.contents.length ? this.contents : [this.content]).some(
1046
+ content => content?.shape === 'tileset3d'
1047
+ );
705
1048
  }
706
1049
 
707
1050
  _onContentLoaded() {
708
1051
  // Vector and Geometry tile rendering do not support the skip LOD optimization.
709
- switch (this.content && this.content.type) {
710
- case 'vctr':
711
- case 'geom':
712
- // @ts-ignore
713
- this.tileset._traverser.disableSkipLevelOfDetail = true;
714
- break;
715
- default:
1052
+ const contents = this.contents.length ? this.contents : [this.content];
1053
+ for (const content of contents) {
1054
+ switch (content && content.type) {
1055
+ case 'vctr':
1056
+ case 'geom':
1057
+ // @ts-ignore
1058
+ this.tileset._traverser.disableSkipLevelOfDetail = true;
1059
+ break;
1060
+ default:
1061
+ }
716
1062
  }
717
1063
 
718
1064
  // The content may be tileset json
@@ -731,24 +1077,9 @@ export class Tile3D {
731
1077
  this.boundingVolume
732
1078
  );
733
1079
 
734
- const content = header.content;
735
- if (!content) {
736
- return;
737
- }
738
-
739
- // TODO Cesium specific
740
- // Non-leaf tiles may have a content bounding-volume, which is a tight-fit bounding volume
741
- // around only the features in the tile. This box is useful for culling for rendering,
742
- // but not for culling for traversing the tree since it does not guarantee spatial coherence, i.e.,
743
- // since it only bounds features in the tile, not the entire tile, children may be
744
- // outside of this box.
745
- if (content.boundingVolume) {
746
- this._contentBoundingVolume = createBoundingVolume(
747
- content.boundingVolume,
748
- this.computedTransform,
749
- this._contentBoundingVolume
750
- );
751
- }
1080
+ // Viewer request volumes constrain traversal, not just render content. A contentless implicit
1081
+ // connector can still own an inherited request volume, so initialize it before the
1082
+ // content-specific early return below.
752
1083
  if (header.viewerRequestVolume) {
753
1084
  this._viewerRequestVolume = createBoundingVolume(
754
1085
  header.viewerRequestVolume,
@@ -756,6 +1087,26 @@ export class Tile3D {
756
1087
  this._viewerRequestVolume
757
1088
  );
758
1089
  }
1090
+
1091
+ const content = header.content;
1092
+ if (content) {
1093
+ this._contentBoundingVolumeHeaders = Array.isArray(content) ? content : [content];
1094
+ }
1095
+ const contentHeaders = content
1096
+ ? Array.isArray(content)
1097
+ ? content
1098
+ : [content]
1099
+ : this._contentBoundingVolumeHeaders;
1100
+ // Preserve source order: an unbounded content entry uses the tile volume at its
1101
+ // corresponding index rather than being appended after bounded sibling entries.
1102
+ this._contentBoundingVolumes = contentHeaders.length
1103
+ ? contentHeaders.map(headerEntry =>
1104
+ headerEntry?.boundingVolume
1105
+ ? createBoundingVolume(headerEntry.boundingVolume, this.computedTransform)
1106
+ : this.boundingVolume
1107
+ )
1108
+ : [this.boundingVolume];
1109
+ this._contentBoundingVolume = this._contentBoundingVolumes[0] || null;
759
1110
  }
760
1111
 
761
1112
  /**