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

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 (115) hide show
  1. package/LICENSE +14 -3
  2. package/dist/dist.dev.js +2396 -274
  3. package/dist/dist.min.js +1 -1
  4. package/dist/index.cjs +2763 -337
  5. package/dist/index.cjs.map +3 -4
  6. package/dist/index.d.ts +7 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -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.map +1 -1
  14. package/dist/point-cloud/point-cloud-tileset.js +3 -0
  15. package/dist/point-cloud/point-cloud-tileset.js.map +1 -1
  16. package/dist/point-cloud/types.d.ts.map +1 -1
  17. package/dist/point-cloud/types.js +3 -0
  18. package/dist/point-cloud/types.js.map +1 -1
  19. package/dist/raster-set.d.ts +35 -29
  20. package/dist/raster-set.d.ts.map +1 -1
  21. package/dist/raster-set.js +7 -1
  22. package/dist/raster-set.js.map +1 -1
  23. package/dist/tileset-3d/common/tile-3d.d.ts +98 -3
  24. package/dist/tileset-3d/common/tile-3d.d.ts.map +1 -1
  25. package/dist/tileset-3d/common/tile-3d.js +229 -15
  26. package/dist/tileset-3d/common/tile-3d.js.map +1 -1
  27. package/dist/tileset-3d/common/tileset-3d.d.ts +143 -3
  28. package/dist/tileset-3d/common/tileset-3d.d.ts.map +1 -1
  29. package/dist/tileset-3d/common/tileset-3d.js +257 -16
  30. package/dist/tileset-3d/common/tileset-3d.js.map +1 -1
  31. package/dist/tileset-3d/common/tileset-cache.d.ts +11 -1
  32. package/dist/tileset-3d/common/tileset-cache.d.ts.map +1 -1
  33. package/dist/tileset-3d/common/tileset-cache.js +11 -3
  34. package/dist/tileset-3d/common/tileset-cache.js.map +1 -1
  35. package/dist/tileset-3d/common/tileset-source.d.ts +18 -0
  36. package/dist/tileset-3d/common/tileset-source.d.ts.map +1 -1
  37. package/dist/tileset-3d/common/tileset-source.js.map +1 -1
  38. package/dist/tileset-3d/common/tileset-traverser.d.ts +13 -0
  39. package/dist/tileset-3d/common/tileset-traverser.d.ts.map +1 -1
  40. package/dist/tileset-3d/common/tileset-traverser.js +28 -1
  41. package/dist/tileset-3d/common/tileset-traverser.js.map +1 -1
  42. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts +111 -0
  43. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts.map +1 -0
  44. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js +340 -0
  45. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js.map +1 -0
  46. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts +92 -3
  47. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts.map +1 -1
  48. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js +211 -13
  49. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js.map +1 -1
  50. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts +36 -0
  51. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts.map +1 -1
  52. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js +66 -0
  53. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js.map +1 -1
  54. package/dist/tileset-3d/format-i3s/i3s-pending-tiles-register.d.ts.map +1 -1
  55. package/dist/tileset-3d/format-i3s/i3s-pending-tiles-register.js +3 -0
  56. package/dist/tileset-3d/format-i3s/i3s-pending-tiles-register.js.map +1 -1
  57. package/dist/tileset-3d/format-i3s/i3s-tile-manager.d.ts.map +1 -1
  58. package/dist/tileset-3d/format-i3s/i3s-tile-manager.js +3 -0
  59. package/dist/tileset-3d/format-i3s/i3s-tile-manager.js.map +1 -1
  60. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.d.ts.map +1 -1
  61. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js +3 -0
  62. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js.map +1 -1
  63. package/dist/tileset-3d/helpers/3d-tiles-options.d.ts.map +1 -1
  64. package/dist/tileset-3d/helpers/3d-tiles-options.js +3 -0
  65. package/dist/tileset-3d/helpers/3d-tiles-options.js.map +1 -1
  66. package/dist/tileset-3d/helpers/bounding-volume.d.ts.map +1 -1
  67. package/dist/tileset-3d/helpers/bounding-volume.js +1 -0
  68. package/dist/tileset-3d/helpers/bounding-volume.js.map +1 -1
  69. package/dist/tileset-3d/helpers/frame-state.d.ts +59 -1
  70. package/dist/tileset-3d/helpers/frame-state.d.ts.map +1 -1
  71. package/dist/tileset-3d/helpers/frame-state.js +84 -9
  72. package/dist/tileset-3d/helpers/frame-state.js.map +1 -1
  73. package/dist/tileset-3d/helpers/tiles-3d-lod.d.ts +61 -9
  74. package/dist/tileset-3d/helpers/tiles-3d-lod.d.ts.map +1 -1
  75. package/dist/tileset-3d/helpers/tiles-3d-lod.js +198 -93
  76. package/dist/tileset-3d/helpers/tiles-3d-lod.js.map +1 -1
  77. package/dist/tileset-3d/helpers/tiles-3d-request-priority.d.ts +134 -0
  78. package/dist/tileset-3d/helpers/tiles-3d-request-priority.d.ts.map +1 -0
  79. package/dist/tileset-3d/helpers/tiles-3d-request-priority.js +179 -0
  80. package/dist/tileset-3d/helpers/tiles-3d-request-priority.js.map +1 -0
  81. package/dist/types.d.ts +2 -0
  82. package/dist/types.d.ts.map +1 -1
  83. package/dist/types.js +3 -0
  84. package/dist/types.js.map +1 -1
  85. package/dist/utils/doubly-linked-list.d.ts.map +1 -1
  86. package/dist/utils/doubly-linked-list.js +1 -0
  87. package/dist/utils/doubly-linked-list.js.map +1 -1
  88. package/dist/utils/managed-array.d.ts.map +1 -1
  89. package/dist/utils/managed-array.js +1 -0
  90. package/dist/utils/managed-array.js.map +1 -1
  91. package/package.json +5 -5
  92. package/src/index.ts +19 -0
  93. package/src/point-cloud/point-cloud-tile.ts +4 -0
  94. package/src/point-cloud/point-cloud-tileset.ts +4 -0
  95. package/src/point-cloud/types.ts +4 -0
  96. package/src/raster-set.ts +81 -43
  97. package/src/tileset-3d/common/tile-3d.ts +285 -21
  98. package/src/tileset-3d/common/tileset-3d.ts +368 -17
  99. package/src/tileset-3d/common/tileset-cache.ts +15 -7
  100. package/src/tileset-3d/common/tileset-source.ts +21 -0
  101. package/src/tileset-3d/common/tileset-traverser.ts +29 -1
  102. package/src/tileset-3d/format-3d-tiles/implicit-tiling.ts +534 -0
  103. package/src/tileset-3d/format-3d-tiles/tiles-3d-source.ts +266 -13
  104. package/src/tileset-3d/format-3d-tiles/tileset-3d-traverser.ts +77 -0
  105. package/src/tileset-3d/format-i3s/i3s-pending-tiles-register.ts +4 -0
  106. package/src/tileset-3d/format-i3s/i3s-tile-manager.ts +4 -0
  107. package/src/tileset-3d/format-i3s/i3s-tileset-traverser.ts +4 -0
  108. package/src/tileset-3d/helpers/3d-tiles-options.ts +4 -0
  109. package/src/tileset-3d/helpers/bounding-volume.ts +2 -0
  110. package/src/tileset-3d/helpers/frame-state.ts +139 -11
  111. package/src/tileset-3d/helpers/tiles-3d-lod.ts +291 -112
  112. package/src/tileset-3d/helpers/tiles-3d-request-priority.ts +301 -0
  113. package/src/types.ts +6 -0
  114. package/src/utils/doubly-linked-list.ts +2 -0
  115. package/src/utils/managed-array.ts +2 -0
@@ -10,7 +10,14 @@ import {Stats} from '@probe.gl/stats';
10
10
  import {RequestScheduler, LoaderWithParser, LoaderOptions} from '@loaders.gl/loader-utils';
11
11
  import {TilesetCache} from './tileset-cache';
12
12
  import {calculateTransformProps} from '../helpers/transform-utils';
13
- import {FrameState, getFrameState, limitSelectedTiles} from '../helpers/frame-state';
13
+ import {getFrameState, limitSelectedTiles, updateCameraMotionState} from '../helpers/frame-state';
14
+ import type {CameraMotionState, FrameState} from '../helpers/frame-state';
15
+ import {
16
+ calculateDynamicScreenSpaceErrorDensity,
17
+ updateRootTransformForDynamicScreenSpaceError
18
+ } from '../helpers/tiles-3d-lod';
19
+ import {interpolateLinearly} from '../helpers/tiles-3d-request-priority';
20
+ import type {FoveatedInterpolationCallback} from '../helpers/tiles-3d-request-priority';
14
21
 
15
22
  import type {GeospatialViewport, Viewport} from '../../types';
16
23
  import {Tile3D} from './tile-3d';
@@ -34,7 +41,28 @@ export type Tileset3DProps = {
34
41
  loadOptions?: LoaderOptions;
35
42
  loadTiles?: boolean;
36
43
  basePath?: string;
44
+ /**
45
+ * Soft target, in bytes, for cached tile content that is not required by the current frame.
46
+ * The listed default applies to 3D Tiles; I3S retains its existing 32 MiB default.
47
+ * @default 536870912
48
+ */
49
+ cacheBytes?: number;
50
+ /**
51
+ * Additional bytes that the current-frame working set may use before memory-adjusted SSE rises.
52
+ * The listed default applies to 3D Tiles; I3S retains its existing 1 MiB default.
53
+ * @default 536870912
54
+ */
55
+ maximumCacheOverflowBytes?: number;
56
+ /**
57
+ * Soft cache target in mebibytes.
58
+ * @deprecated Use {@link Tileset3DProps.cacheBytes}; when both are supplied, `cacheBytes` wins.
59
+ */
37
60
  maximumMemoryUsage?: number;
61
+ /**
62
+ * Cache overflow headroom in mebibytes.
63
+ * @deprecated Use {@link Tileset3DProps.maximumCacheOverflowBytes}; when both are supplied, the
64
+ * byte-native option wins.
65
+ */
38
66
  memoryCacheOverflow?: number;
39
67
  maximumTilesSelected?: number;
40
68
  debounceTime?: number;
@@ -46,10 +74,34 @@ export type Tileset3DProps = {
46
74
  modelMatrix?: Matrix4;
47
75
 
48
76
  maximumScreenSpaceError?: number;
77
+ /** Enables perspective dynamic SSE to reduce distant, horizon-facing refinement. */
78
+ dynamicScreenSpaceError?: boolean;
79
+ /** Base dynamic SSE fog density in inverse meters. */
80
+ dynamicScreenSpaceErrorDensity?: number;
81
+ /** Maximum dynamic SSE reduction in logical/CSS pixels. */
82
+ dynamicScreenSpaceErrorFactor?: number;
83
+ /** Fraction of tileset height at which dynamic SSE begins to fade, clamped to `[0, 1]`. */
84
+ dynamicScreenSpaceErrorHeightFalloff?: number;
85
+ /**
86
+ * Enables adaptive LOD reduction when estimated tile memory exceeds the overflow ceiling.
87
+ * Defaults to `true` for 3D Tiles and retains the existing `false` default for I3S.
88
+ */
49
89
  memoryAdjustedScreenSpaceError?: boolean;
50
90
  viewportTraversersMap?: any;
51
91
  updateTransforms?: boolean;
52
92
  viewDistanceScale?: number;
93
+ /** Reduced viewport-height fraction used to prioritize coarse initial coverage; `0` disables it. */
94
+ progressiveResolutionHeightFraction?: number;
95
+ /** Whether requests nearer the center of a perspective viewport receive higher priority. */
96
+ foveatedScreenSpaceError?: boolean;
97
+ /** Fraction of the perspective field of view that receives no foveated SSE relaxation. */
98
+ foveatedConeSize?: number;
99
+ /** Minimum logical-pixel SSE relaxation immediately outside the foveated cone. */
100
+ foveatedMinimumScreenSpaceErrorRelaxation?: number;
101
+ /** Function that increases SSE relaxation from the foveated cone toward the viewport edge. */
102
+ foveatedInterpolationCallback?: FoveatedInterpolationCallback;
103
+ /** Seconds peripheral requests may wait after the camera moves. */
104
+ foveatedTimeDelay?: number;
53
105
 
54
106
  onTileLoad?: (tile: Tile3D) => any;
55
107
  onTileUnload?: (tile: Tile3D) => any;
@@ -69,7 +121,13 @@ type Props = {
69
121
  modelMatrix: Matrix4;
70
122
  throttleRequests: boolean;
71
123
  maxRequests: number;
124
+ /** Soft target, in bytes, for evictable cached tile content. */
125
+ cacheBytes: number;
126
+ /** Current-frame headroom, in bytes, before memory-adjusted SSE rises. */
127
+ maximumCacheOverflowBytes: number;
128
+ /** @deprecated Byte-native code should use `cacheBytes`. */
72
129
  maximumMemoryUsage: number;
130
+ /** @deprecated Byte-native code should use `maximumCacheOverflowBytes`. */
73
131
  memoryCacheOverflow: number;
74
132
  maximumTilesSelected: number;
75
133
  debounceTime: number;
@@ -81,6 +139,15 @@ type Props = {
81
139
  onTraversalComplete: (selectedTiles: Tile3D[]) => Tile3D[];
82
140
  onUpdate: () => void;
83
141
  maximumScreenSpaceError: number;
142
+ /** Whether perspective dynamic SSE is enabled. */
143
+ dynamicScreenSpaceError: boolean;
144
+ /** Base dynamic SSE fog density in inverse meters. */
145
+ dynamicScreenSpaceErrorDensity: number;
146
+ /** Maximum dynamic SSE reduction in logical/CSS pixels. */
147
+ dynamicScreenSpaceErrorFactor: number;
148
+ /** Fraction of tileset height at which dynamic SSE begins to fade. */
149
+ dynamicScreenSpaceErrorHeightFalloff: number;
150
+ /** Whether cache pressure may adapt the active SSE threshold. */
84
151
  memoryAdjustedScreenSpaceError: boolean;
85
152
  viewportTraversersMap: Record<string, any> | null;
86
153
  attributions: string[];
@@ -88,19 +155,97 @@ type Props = {
88
155
  loadOptions: LoaderOptions;
89
156
  updateTransforms: boolean;
90
157
  viewDistanceScale: number;
158
+ /** Reduced viewport-height fraction used to prioritize coarse initial coverage. */
159
+ progressiveResolutionHeightFraction: number;
160
+ /** Whether perspective requests are prioritized around the viewport center. */
161
+ foveatedScreenSpaceError: boolean;
162
+ /** Fraction of the perspective field of view with no foveated SSE relaxation. */
163
+ foveatedConeSize: number;
164
+ /** Minimum logical-pixel SSE relaxation outside the foveated cone. */
165
+ foveatedMinimumScreenSpaceErrorRelaxation: number;
166
+ /** Function used to interpolate foveated SSE relaxation. */
167
+ foveatedInterpolationCallback: FoveatedInterpolationCallback;
168
+ /** Seconds eligible peripheral requests wait after camera movement. */
169
+ foveatedTimeDelay: number;
91
170
  basePath: string;
92
171
  contentLoader?: (tile: Tile3D) => Promise<void>;
93
172
  i3s: Record<string, any>;
94
173
  };
95
174
 
175
+ const BYTES_PER_MEBIBYTE = 1024 * 1024;
176
+ const DEFAULT_CACHE_BYTES = 512 * BYTES_PER_MEBIBYTE;
177
+ const DEFAULT_MAXIMUM_CACHE_OVERFLOW_BYTES = 512 * BYTES_PER_MEBIBYTE;
178
+ const DEFAULT_I3S_CACHE_BYTES = 32 * BYTES_PER_MEBIBYTE;
179
+ const DEFAULT_I3S_MAXIMUM_CACHE_OVERFLOW_BYTES = BYTES_PER_MEBIBYTE;
180
+
181
+ /**
182
+ * Validates a byte-based tile-cache budget.
183
+ *
184
+ * Fractions are accepted because JavaScript memory estimates are numbers, but negative, infinite,
185
+ * and `NaN` values cannot define a stable cache threshold.
186
+ *
187
+ * @param value - Proposed cache budget in bytes.
188
+ * @param optionName - Public option name used in an actionable error message.
189
+ * @returns The validated byte budget.
190
+ */
191
+ function validateCacheByteLength(value: number, optionName: string): number {
192
+ if (!Number.isFinite(value) || value < 0) {
193
+ throw new Error(`${optionName} must be a finite number greater than or equal to 0`);
194
+ }
195
+ return value;
196
+ }
197
+
198
+ /**
199
+ * Resolves the base cache target from byte-native or deprecated mebibyte options.
200
+ *
201
+ * The byte-native option takes precedence so mixed migration configurations are deterministic.
202
+ * The fallback is already expressed in bytes and is used for both construction and runtime updates.
203
+ *
204
+ * @param options - Options that may contain byte-native or deprecated cache values.
205
+ * @param fallbackCacheBytes - Existing or default cache target in bytes.
206
+ * @returns The resolved and validated cache target in bytes.
207
+ */
208
+ function resolveCacheBytes(options: Tileset3DProps, fallbackCacheBytes: number): number {
209
+ const cacheBytes =
210
+ options.cacheBytes ??
211
+ (options.maximumMemoryUsage !== undefined
212
+ ? options.maximumMemoryUsage * BYTES_PER_MEBIBYTE
213
+ : fallbackCacheBytes);
214
+ return validateCacheByteLength(cacheBytes, 'cacheBytes');
215
+ }
216
+
217
+ /**
218
+ * Resolves cache overflow headroom from byte-native or deprecated mebibyte options.
219
+ *
220
+ * The byte-native option takes precedence so callers can migrate the two cache controls
221
+ * independently without unit ambiguity.
222
+ *
223
+ * @param options - Options that may contain byte-native or deprecated overflow values.
224
+ * @param fallbackOverflowBytes - Existing or default overflow headroom in bytes.
225
+ * @returns The resolved and validated overflow headroom in bytes.
226
+ */
227
+ function resolveMaximumCacheOverflowBytes(
228
+ options: Tileset3DProps,
229
+ fallbackOverflowBytes: number
230
+ ): number {
231
+ const maximumCacheOverflowBytes =
232
+ options.maximumCacheOverflowBytes ??
233
+ (options.memoryCacheOverflow !== undefined
234
+ ? options.memoryCacheOverflow * BYTES_PER_MEBIBYTE
235
+ : fallbackOverflowBytes);
236
+ return validateCacheByteLength(maximumCacheOverflowBytes, 'maximumCacheOverflowBytes');
237
+ }
238
+
96
239
  const DEFAULT_PROPS: Props = {
97
240
  description: '',
98
241
  ellipsoid: Ellipsoid.WGS84,
99
242
  modelMatrix: new Matrix4(),
100
243
  throttleRequests: true,
101
244
  maxRequests: 64,
102
- maximumMemoryUsage: 32,
103
- memoryCacheOverflow: 1,
245
+ cacheBytes: DEFAULT_CACHE_BYTES,
246
+ maximumCacheOverflowBytes: DEFAULT_MAXIMUM_CACHE_OVERFLOW_BYTES,
247
+ maximumMemoryUsage: DEFAULT_CACHE_BYTES / BYTES_PER_MEBIBYTE,
248
+ memoryCacheOverflow: DEFAULT_MAXIMUM_CACHE_OVERFLOW_BYTES / BYTES_PER_MEBIBYTE,
104
249
  maximumTilesSelected: 0,
105
250
  debounceTime: 0,
106
251
  onTileLoad: () => {},
@@ -112,8 +257,18 @@ const DEFAULT_PROPS: Props = {
112
257
  onUpdate: () => {},
113
258
  contentLoader: undefined,
114
259
  viewDistanceScale: 1.0,
260
+ progressiveResolutionHeightFraction: 0.3,
261
+ foveatedScreenSpaceError: true,
262
+ foveatedConeSize: 0.1,
263
+ foveatedMinimumScreenSpaceErrorRelaxation: 0,
264
+ foveatedInterpolationCallback: interpolateLinearly,
265
+ foveatedTimeDelay: 0.2,
115
266
  maximumScreenSpaceError: 8,
116
- memoryAdjustedScreenSpaceError: false,
267
+ dynamicScreenSpaceError: true,
268
+ dynamicScreenSpaceErrorDensity: 2.0e-4,
269
+ dynamicScreenSpaceErrorFactor: 24,
270
+ dynamicScreenSpaceErrorHeightFalloff: 0.25,
271
+ memoryAdjustedScreenSpaceError: true,
117
272
  loadTiles: true,
118
273
  updateTransforms: true,
119
274
  viewportTraversersMap: null,
@@ -174,14 +329,16 @@ export class Tileset3D {
174
329
  zoom = 1;
175
330
  boundingVolume: any = null;
176
331
 
177
- dynamicScreenSpaceErrorComputedDensity = 0.0;
332
+ /** Effective dynamic SSE density calculated for the most recently traversed viewport. */
333
+ dynamicScreenSpaceErrorComputedDensity = 0;
178
334
 
179
- maximumMemoryUsage = 32;
335
+ /** Estimated bytes currently retained by loaded tile content. */
180
336
  gpuMemoryUsageInBytes = 0;
337
+ /** Active SSE threshold after optional cache-pressure adjustment, in logical/CSS pixels. */
181
338
  memoryAdjustedScreenSpaceError = 0.0;
182
339
 
183
- private _cacheBytes = 0;
184
- private _cacheOverflowBytes = 0;
340
+ private _cacheBytes = DEFAULT_CACHE_BYTES;
341
+ private _maximumCacheOverflowBytes = DEFAULT_MAXIMUM_CACHE_OVERFLOW_BYTES;
185
342
 
186
343
  _frameNumber = 0;
187
344
  private _tiles: Record<string, Tile3D> = {};
@@ -195,6 +352,10 @@ export class Tileset3D {
195
352
  private _requestedTiles: Tile3D[] = [];
196
353
  private _emptyTiles: Tile3D[] = [];
197
354
  private frameStateData: any = {};
355
+ /** Last camera pose retained independently for each viewport. */
356
+ private _cameraMotionStates: Record<string, CameraMotionState> = {};
357
+ /** Pending traversal that releases requests after camera motion settles. */
358
+ private _deferredTraversalTimeout: ReturnType<typeof setTimeout> | null = null;
198
359
 
199
360
  _traverser: TilesetTraverser;
200
361
  _cache = new TilesetCache();
@@ -210,12 +371,34 @@ export class Tileset3D {
210
371
  * @param options Traversal and runtime options.
211
372
  */
212
373
  constructor(source: Tileset3DSource, options?: Tileset3DProps) {
213
- this.options = {...DEFAULT_PROPS, ...options};
214
- this.loadOptions = this.options.loadOptions || {};
215
-
216
374
  if (!isTileset3DSource(source)) {
217
375
  throw new Error('Tileset3D requires a Tileset3DSource instance');
218
376
  }
377
+
378
+ const suppliedOptions = options || {};
379
+ const usesTiles3DCacheDefaults = source.type === TILESET_TYPE.TILES3D;
380
+ const defaultCacheBytes = usesTiles3DCacheDefaults
381
+ ? DEFAULT_PROPS.cacheBytes
382
+ : DEFAULT_I3S_CACHE_BYTES;
383
+ const defaultMaximumCacheOverflowBytes = usesTiles3DCacheDefaults
384
+ ? DEFAULT_PROPS.maximumCacheOverflowBytes
385
+ : DEFAULT_I3S_MAXIMUM_CACHE_OVERFLOW_BYTES;
386
+ const cacheBytes = resolveCacheBytes(suppliedOptions, defaultCacheBytes);
387
+ const maximumCacheOverflowBytes = resolveMaximumCacheOverflowBytes(
388
+ suppliedOptions,
389
+ defaultMaximumCacheOverflowBytes
390
+ );
391
+ this.options = {
392
+ ...DEFAULT_PROPS,
393
+ ...suppliedOptions,
394
+ cacheBytes,
395
+ maximumCacheOverflowBytes,
396
+ maximumMemoryUsage: cacheBytes / BYTES_PER_MEBIBYTE,
397
+ memoryCacheOverflow: maximumCacheOverflowBytes / BYTES_PER_MEBIBYTE,
398
+ memoryAdjustedScreenSpaceError:
399
+ suppliedOptions.memoryAdjustedScreenSpaceError ?? usesTiles3DCacheDefaults
400
+ };
401
+ this.loadOptions = this.options.loadOptions || {};
219
402
  this.source = source;
220
403
 
221
404
  this.tileset = null;
@@ -237,8 +420,8 @@ export class Tileset3D {
237
420
  });
238
421
 
239
422
  this.memoryAdjustedScreenSpaceError = this.options.maximumScreenSpaceError;
240
- this._cacheBytes = this.options.maximumMemoryUsage * 1024 * 1024;
241
- this._cacheOverflowBytes = this.options.memoryCacheOverflow * 1024 * 1024;
423
+ this._cacheBytes = cacheBytes;
424
+ this._maximumCacheOverflowBytes = maximumCacheOverflowBytes;
242
425
 
243
426
  this.stats = new Stats({id: this.url});
244
427
  this._initializeStats();
@@ -247,6 +430,8 @@ export class Tileset3D {
247
430
  }
248
431
 
249
432
  destroy(): void {
433
+ this._clearDeferredTraversal();
434
+ this.source.destroy?.();
250
435
  this._destroy();
251
436
  }
252
437
 
@@ -262,14 +447,83 @@ export class Tileset3D {
262
447
  return this._frameNumber;
263
448
  }
264
449
 
450
+ /**
451
+ * Gets or sets the soft target, in bytes, for cached tile content not needed this frame.
452
+ *
453
+ * Current-frame tiles remain protected even when their estimated memory exceeds this target.
454
+ * Reducing the value makes unused least-recently-used tiles eligible for eviction on the next
455
+ * traversal.
456
+ * @default 536870912 for 3D Tiles; 33554432 for I3S
457
+ */
458
+ get cacheBytes(): number {
459
+ return this._cacheBytes;
460
+ }
461
+
462
+ set cacheBytes(value: number) {
463
+ this._cacheBytes = validateCacheByteLength(value, 'cacheBytes');
464
+ this.options.cacheBytes = this._cacheBytes;
465
+ this.options.maximumMemoryUsage = this._cacheBytes / BYTES_PER_MEBIBYTE;
466
+ }
467
+
468
+ /**
469
+ * Gets or sets the additional current-frame headroom, in bytes, before memory-adjusted SSE rises.
470
+ *
471
+ * The pressure ceiling is `cacheBytes + maximumCacheOverflowBytes`. This value does not change
472
+ * the base target used to evict tiles that were not touched in the current frame.
473
+ * @default 536870912 for 3D Tiles; 1048576 for I3S
474
+ */
475
+ get maximumCacheOverflowBytes(): number {
476
+ return this._maximumCacheOverflowBytes;
477
+ }
478
+
479
+ set maximumCacheOverflowBytes(value: number) {
480
+ this._maximumCacheOverflowBytes = validateCacheByteLength(value, 'maximumCacheOverflowBytes');
481
+ this.options.maximumCacheOverflowBytes = this._maximumCacheOverflowBytes;
482
+ this.options.memoryCacheOverflow = this._maximumCacheOverflowBytes / BYTES_PER_MEBIBYTE;
483
+ }
484
+
485
+ /**
486
+ * Gets or sets the soft cache target in mebibytes.
487
+ * @deprecated Use {@link Tileset3D.cacheBytes}. Assignments remain synchronized for compatibility.
488
+ */
489
+ get maximumMemoryUsage(): number {
490
+ return this.cacheBytes / BYTES_PER_MEBIBYTE;
491
+ }
492
+
493
+ set maximumMemoryUsage(value: number) {
494
+ this.cacheBytes = validateCacheByteLength(value * BYTES_PER_MEBIBYTE, 'maximumMemoryUsage');
495
+ }
496
+
265
497
  get queryParams(): string {
266
498
  const rootUrl = this.source.getTileUrl(this.url);
267
499
  const queryIndex = rootUrl.indexOf('?');
268
500
  return queryIndex >= 0 ? rootUrl.slice(queryIndex + 1) : '';
269
501
  }
270
502
 
503
+ /**
504
+ * Updates traversal and runtime options, synchronizing byte-native and deprecated cache values.
505
+ *
506
+ * For each budget, the byte-native option takes precedence when both unit forms are supplied.
507
+ * Unspecified budgets retain their current byte values, avoiding repeated MiB conversion.
508
+ *
509
+ * @param props - Partial runtime options to apply.
510
+ */
271
511
  setProps(props: Tileset3DProps): void {
272
- this.options = {...this.options, ...props};
512
+ const cacheBytes = resolveCacheBytes(props, this.cacheBytes);
513
+ const maximumCacheOverflowBytes = resolveMaximumCacheOverflowBytes(
514
+ props,
515
+ this.maximumCacheOverflowBytes
516
+ );
517
+ this.options = {
518
+ ...this.options,
519
+ ...props,
520
+ cacheBytes,
521
+ maximumCacheOverflowBytes,
522
+ maximumMemoryUsage: cacheBytes / BYTES_PER_MEBIBYTE,
523
+ memoryCacheOverflow: maximumCacheOverflowBytes / BYTES_PER_MEBIBYTE
524
+ };
525
+ this._cacheBytes = cacheBytes;
526
+ this._maximumCacheOverflowBytes = maximumCacheOverflowBytes;
273
527
  }
274
528
 
275
529
  getTileUrl(tilePath: string): string {
@@ -312,13 +566,20 @@ export class Tileset3D {
312
566
  return this.updatePromise;
313
567
  }
314
568
 
569
+ /**
570
+ * Moves the active LOD threshold toward the configured SSE or away from cache pressure.
571
+ *
572
+ * Usage below {@link Tileset3D.cacheBytes} restores quality in two-percent steps. Usage above
573
+ * `cacheBytes + maximumCacheOverflowBytes` reduces refinement in two-percent steps. Values in
574
+ * the overflow window leave the threshold unchanged, preventing rapid load/evict oscillation.
575
+ */
315
576
  adjustScreenSpaceError(): void {
316
- if (this.gpuMemoryUsageInBytes < this._cacheBytes) {
577
+ if (this.gpuMemoryUsageInBytes < this.cacheBytes) {
317
578
  this.memoryAdjustedScreenSpaceError = Math.max(
318
579
  this.memoryAdjustedScreenSpaceError / 1.02,
319
580
  this.options.maximumScreenSpaceError
320
581
  );
321
- } else if (this.gpuMemoryUsageInBytes > this._cacheBytes + this._cacheOverflowBytes) {
582
+ } else if (this.gpuMemoryUsageInBytes > this.cacheBytes + this.maximumCacheOverflowBytes) {
322
583
  this.memoryAdjustedScreenSpaceError *= 1.02;
323
584
  }
324
585
  }
@@ -335,6 +596,8 @@ export class Tileset3D {
335
596
  this._cache.reset();
336
597
  this._frameNumber++;
337
598
  this.traverseCounter = preparedViewports.length;
599
+ const currentTime = Date.now();
600
+ let minimumDeferredTraversalDelay = Number.POSITIVE_INFINITY;
338
601
  const viewportsToTraverse: string[] = [];
339
602
  for (const viewport of preparedViewports) {
340
603
  const id = viewport.id;
@@ -355,7 +618,70 @@ export class Tileset3D {
355
618
  continue;
356
619
  }
357
620
  const frameState = getFrameState(viewport as GeospatialViewport, this._frameNumber);
358
- this._traverser.traverse(this.roots[id], frameState, this.options);
621
+ const root = this.roots[id];
622
+ const cameraMotionUpdate = updateCameraMotionState(
623
+ this._cameraMotionStates[id],
624
+ frameState.camera.position,
625
+ frameState.camera.direction,
626
+ currentTime
627
+ );
628
+ this._cameraMotionStates[id] = cameraMotionUpdate.state;
629
+ frameState.camera.timeSinceMovement = cameraMotionUpdate.timeSinceMovement;
630
+ // Dynamic SSE reads the root's inverse computed transform before traversal updates tile
631
+ // visibility. Refresh it from the current tileset model matrix here so animated local
632
+ // box/sphere tilesets never calculate their height falloff one transform behind the frame.
633
+ updateRootTransformForDynamicScreenSpaceError(root, this.modelMatrix);
634
+ frameState.dynamicScreenSpaceErrorDensity =
635
+ this.type === TILESET_TYPE.TILES3D &&
636
+ this.options.dynamicScreenSpaceError &&
637
+ !frameState.viewport.orthographic
638
+ ? calculateDynamicScreenSpaceErrorDensity(root, frameState, this.options)
639
+ : 0;
640
+ // Keep the legacy diagnostic field, but traversal reads the per-viewport frame value above.
641
+ this.dynamicScreenSpaceErrorComputedDensity = frameState.dynamicScreenSpaceErrorDensity;
642
+ this._traverser.traverse(root, frameState, this.options);
643
+ if (Object.keys(this._traverser.deferredTiles).length > 0) {
644
+ minimumDeferredTraversalDelay = Math.min(
645
+ minimumDeferredTraversalDelay,
646
+ Math.max(this.options.foveatedTimeDelay - cameraMotionUpdate.timeSinceMovement, 0)
647
+ );
648
+ }
649
+ }
650
+
651
+ if (Number.isFinite(minimumDeferredTraversalDelay)) {
652
+ this._scheduleDeferredTraversal(minimumDeferredTraversalDelay);
653
+ } else {
654
+ this._clearDeferredTraversal();
655
+ }
656
+ }
657
+
658
+ /**
659
+ * Schedules one follow-up traversal when the moving-camera deferral window expires.
660
+ *
661
+ * Replacing an existing timer ensures continuing camera movement pushes the retry window forward
662
+ * instead of creating a queue of redundant traversals.
663
+ *
664
+ * @param delayInSeconds - Remaining stationary delay before deferred requests become eligible.
665
+ */
666
+ private _scheduleDeferredTraversal(delayInSeconds: number): void {
667
+ this._clearDeferredTraversal();
668
+ this._deferredTraversalTimeout = setTimeout(
669
+ () => {
670
+ this._deferredTraversalTimeout = null;
671
+ this.selectTiles().catch((error: unknown) => {
672
+ const traversalError = error instanceof Error ? error : new Error(String(error));
673
+ this._onTilesetError(traversalError);
674
+ });
675
+ },
676
+ Math.max(delayInSeconds * 1000, 0)
677
+ );
678
+ }
679
+
680
+ /** Clears a pending moving-camera follow-up traversal. */
681
+ private _clearDeferredTraversal(): void {
682
+ if (this._deferredTraversalTimeout !== null) {
683
+ clearTimeout(this._deferredTraversalTimeout);
684
+ this._deferredTraversalTimeout = null;
359
685
  }
360
686
  }
361
687
 
@@ -475,6 +801,9 @@ export class Tileset3D {
475
801
 
476
802
  _unloadTiles(): void {
477
803
  this._cache.unloadTiles(this, (tileset, tile) => tileset._unloadTile(tile));
804
+ if (this.options.memoryAdjustedScreenSpaceError) {
805
+ this.adjustScreenSpaceError();
806
+ }
478
807
  }
479
808
 
480
809
  _updateStats(): void {
@@ -571,6 +900,28 @@ export class Tileset3D {
571
900
  }
572
901
  }
573
902
 
903
+ /**
904
+ * Loads a source-managed lazy child-header group and reports failures through tile callbacks.
905
+ *
906
+ * Subtree metadata shares the normal loading counters but is not reported as render content and
907
+ * is not added to the GPU cache. Traversal is notified separately after the promise settles.
908
+ *
909
+ * @param tile - Runtime tile that owns the lazy subtree reference.
910
+ * @param frameState - View state that made the subtree eligible for refinement.
911
+ */
912
+ async _loadTileChildren(tile: Tile3D, frameState: FrameState): Promise<void> {
913
+ try {
914
+ this._onStartTileLoading();
915
+ await tile.loadChildren(frameState);
916
+ } catch (error: unknown) {
917
+ const tileError = error instanceof Error ? error : new Error('subtree load failed');
918
+ this._onSourceError(tileError, tile);
919
+ this._onTileLoadError(tile, tileError);
920
+ } finally {
921
+ this._onEndTileLoading();
922
+ }
923
+ }
924
+
574
925
  _onTileLoadError(tile: Tile3D, error: Error): void {
575
926
  this.stats.get(TILES_LOAD_FAILED).incrementCount();
576
927
 
@@ -71,24 +71,32 @@ export class TilesetCache {
71
71
  }
72
72
  }
73
73
 
74
- unloadTiles(tileset, unloadCallback): void {
74
+ /**
75
+ * Unloads least-recently-used tiles until the byte-native cache target is met.
76
+ *
77
+ * Tiles touched after the frame sentinel remain protected, so {@link Tileset3D.cacheBytes} is a
78
+ * soft target rather than a hard cap on the visible working set. Overflow headroom affects
79
+ * memory-adjusted SSE, not eviction of unused tiles.
80
+ *
81
+ * @param tileset - Tileset that owns the cache and reports estimated content memory.
82
+ * @param unloadCallback - Callback that releases content and decrements the memory estimate.
83
+ */
84
+ unloadTiles(
85
+ tileset: Tileset3D,
86
+ unloadCallback?: (tileset: Tileset3D, tile: Tile3D) => void
87
+ ): void {
75
88
  const trimTiles = this._trimTiles;
76
89
  this._trimTiles = false;
77
90
 
78
91
  const list = this._list;
79
92
 
80
- const maximumMemoryUsageInBytes = tileset.maximumMemoryUsage * 1024 * 1024;
81
-
82
93
  // Traverse the list only to the sentinel since tiles/nodes to the
83
94
  // right of the sentinel were used this frame.
84
95
  // The sub-list to the left of the sentinel is ordered from LRU to MRU.
85
96
  const sentinel = this._sentinel;
86
97
  let node = list.head;
87
98
 
88
- while (
89
- node !== sentinel &&
90
- (tileset.gpuMemoryUsageInBytes > maximumMemoryUsageInBytes || trimTiles)
91
- ) {
99
+ while (node !== sentinel && (tileset.gpuMemoryUsageInBytes > tileset.cacheBytes || trimTiles)) {
92
100
  // @ts-expect-error
93
101
  const tile = node.item;
94
102
  // @ts-expect-error
@@ -73,6 +73,16 @@ export type TileContentLoadResult = {
73
73
  nestedTileset?: TilesetJSON;
74
74
  };
75
75
 
76
+ /** Result of a source-managed lazy child-header request. */
77
+ export type TileChildrenLoadResult = {
78
+ /** Whether a subtree resource was loaded and installed. */
79
+ loaded: boolean;
80
+ /** Number of available tiles described by the subtree, including its existing root placeholder. */
81
+ tileCount: number;
82
+ /** Number of lazy child-subtree references installed at the new boundary. */
83
+ childSubtreeCount: number;
84
+ };
85
+
76
86
  /**
77
87
  * Flags describing compression and texture formats observed while streaming tile content.
78
88
  */
@@ -157,6 +167,9 @@ export interface Tileset3DSource {
157
167
  */
158
168
  initialize(): Promise<void>;
159
169
 
170
+ /** Releases source-local caches and prevents late asynchronous work from mutating runtime tiles. */
171
+ destroy?(): void;
172
+
160
173
  /**
161
174
  * Returns normalized source metadata after initialization completes.
162
175
  */
@@ -186,6 +199,14 @@ export interface Tileset3DSource {
186
199
  */
187
200
  loadTileContent(tile: Tile3D): Promise<TileContentLoadResult>;
188
201
 
202
+ /**
203
+ * Loads and installs a complete lazy child-header group for a runtime tile.
204
+ *
205
+ * Sources that implement this hook retain ownership of resource parsing, caching, query
206
+ * inheritance, and archive resolution. Traversal calls it only after visibility and LOD checks.
207
+ */
208
+ loadTileChildren?(tile: Tile3D, frameState: FrameState): Promise<TileChildrenLoadResult>;
209
+
189
210
  /**
190
211
  * Resolves a tile-relative path to the final request URL.
191
212
  */