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

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 (205) hide show
  1. package/LICENSE +14 -3
  2. package/README.md +26 -0
  3. package/dist/dist.dev.js +20341 -5081
  4. package/dist/dist.min.js +3 -1
  5. package/dist/index.cjs +7904 -2071
  6. package/dist/index.cjs.map +3 -4
  7. package/dist/index.d.ts +25 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +11 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/point-cloud/point-cloud-tile.d.ts.map +1 -1
  12. package/dist/point-cloud/point-cloud-tile.js +3 -0
  13. package/dist/point-cloud/point-cloud-tile.js.map +1 -1
  14. package/dist/point-cloud/point-cloud-tileset.d.ts +4 -0
  15. package/dist/point-cloud/point-cloud-tileset.d.ts.map +1 -1
  16. package/dist/point-cloud/point-cloud-tileset.js +54 -12
  17. package/dist/point-cloud/point-cloud-tileset.js.map +1 -1
  18. package/dist/point-cloud/types.d.ts +17 -0
  19. package/dist/point-cloud/types.d.ts.map +1 -1
  20. package/dist/point-cloud/types.js +3 -0
  21. package/dist/point-cloud/types.js.map +1 -1
  22. package/dist/raster-set.d.ts +48 -31
  23. package/dist/raster-set.d.ts.map +1 -1
  24. package/dist/raster-set.js +104 -24
  25. package/dist/raster-set.js.map +1 -1
  26. package/dist/spatial/format-spatial-reference.d.ts +68 -0
  27. package/dist/spatial/format-spatial-reference.d.ts.map +1 -0
  28. package/dist/spatial/format-spatial-reference.js +291 -0
  29. package/dist/spatial/format-spatial-reference.js.map +1 -0
  30. package/dist/spatial/get-spatial-coordinate-frame.d.ts +20 -0
  31. package/dist/spatial/get-spatial-coordinate-frame.d.ts.map +1 -0
  32. package/dist/spatial/get-spatial-coordinate-frame.js +139 -0
  33. package/dist/spatial/get-spatial-coordinate-frame.js.map +1 -0
  34. package/dist/spatial/i3s-elevation.d.ts +20 -0
  35. package/dist/spatial/i3s-elevation.d.ts.map +1 -0
  36. package/dist/spatial/i3s-elevation.js +90 -0
  37. package/dist/spatial/i3s-elevation.js.map +1 -0
  38. package/dist/spatial/i3s-spatial-transformer.d.ts +205 -0
  39. package/dist/spatial/i3s-spatial-transformer.d.ts.map +1 -0
  40. package/dist/spatial/i3s-spatial-transformer.js +731 -0
  41. package/dist/spatial/i3s-spatial-transformer.js.map +1 -0
  42. package/dist/spatial/normalize-crs-identifier.d.ts +15 -0
  43. package/dist/spatial/normalize-crs-identifier.d.ts.map +1 -0
  44. package/dist/spatial/normalize-crs-identifier.js +22 -0
  45. package/dist/spatial/normalize-crs-identifier.js.map +1 -0
  46. package/dist/spatial/spatial-coordinate-transformer.d.ts +52 -0
  47. package/dist/spatial/spatial-coordinate-transformer.d.ts.map +1 -0
  48. package/dist/spatial/spatial-coordinate-transformer.js +295 -0
  49. package/dist/spatial/spatial-coordinate-transformer.js.map +1 -0
  50. package/dist/spatial/spatial-resource-registry.d.ts +47 -0
  51. package/dist/spatial/spatial-resource-registry.d.ts.map +1 -0
  52. package/dist/spatial/spatial-resource-registry.js +76 -0
  53. package/dist/spatial/spatial-resource-registry.js.map +1 -0
  54. package/dist/spatial/spatial-types.d.ts +190 -0
  55. package/dist/spatial/spatial-types.d.ts.map +1 -0
  56. package/dist/spatial/spatial-types.js +192 -0
  57. package/dist/spatial/spatial-types.js.map +1 -0
  58. package/dist/spatial/tiles-3d-spatial-transformer.d.ts +32 -0
  59. package/dist/spatial/tiles-3d-spatial-transformer.d.ts.map +1 -0
  60. package/dist/spatial/tiles-3d-spatial-transformer.js +163 -0
  61. package/dist/spatial/tiles-3d-spatial-transformer.js.map +1 -0
  62. package/dist/tileset-2d/adapter.d.ts +2 -3
  63. package/dist/tileset-2d/adapter.d.ts.map +1 -1
  64. package/dist/tileset-2d/tileset-2d.d.ts +2 -3
  65. package/dist/tileset-2d/tileset-2d.d.ts.map +1 -1
  66. package/dist/tileset-2d/tileset-2d.js.map +1 -1
  67. package/dist/tileset-3d/common/indexed-archive-tileset-source.d.ts +3 -3
  68. package/dist/tileset-3d/common/indexed-archive-tileset-source.d.ts.map +1 -1
  69. package/dist/tileset-3d/common/indexed-archive-tileset-source.js +9 -4
  70. package/dist/tileset-3d/common/indexed-archive-tileset-source.js.map +1 -1
  71. package/dist/tileset-3d/common/tile-3d-contracts.d.ts +63 -0
  72. package/dist/tileset-3d/common/tile-3d-contracts.d.ts.map +1 -0
  73. package/dist/tileset-3d/common/tile-3d-contracts.js +51 -0
  74. package/dist/tileset-3d/common/tile-3d-contracts.js.map +1 -0
  75. package/dist/tileset-3d/common/tile-3d.d.ts +176 -5
  76. package/dist/tileset-3d/common/tile-3d.d.ts.map +1 -1
  77. package/dist/tileset-3d/common/tile-3d.js +523 -89
  78. package/dist/tileset-3d/common/tile-3d.js.map +1 -1
  79. package/dist/tileset-3d/common/tileset-3d.d.ts +199 -9
  80. package/dist/tileset-3d/common/tileset-3d.d.ts.map +1 -1
  81. package/dist/tileset-3d/common/tileset-3d.js +345 -32
  82. package/dist/tileset-3d/common/tileset-3d.js.map +1 -1
  83. package/dist/tileset-3d/common/tileset-cache.d.ts +11 -1
  84. package/dist/tileset-3d/common/tileset-cache.d.ts.map +1 -1
  85. package/dist/tileset-3d/common/tileset-cache.js +11 -3
  86. package/dist/tileset-3d/common/tileset-cache.js.map +1 -1
  87. package/dist/tileset-3d/common/tileset-content-traversal.d.ts +37 -0
  88. package/dist/tileset-3d/common/tileset-content-traversal.d.ts.map +1 -0
  89. package/dist/tileset-3d/common/tileset-content-traversal.js +80 -0
  90. package/dist/tileset-3d/common/tileset-content-traversal.js.map +1 -0
  91. package/dist/tileset-3d/common/tileset-observability.d.ts +60 -0
  92. package/dist/tileset-3d/common/tileset-observability.d.ts.map +1 -0
  93. package/dist/tileset-3d/common/tileset-observability.js +39 -0
  94. package/dist/tileset-3d/common/tileset-observability.js.map +1 -0
  95. package/dist/tileset-3d/common/tileset-source.d.ts +51 -9
  96. package/dist/tileset-3d/common/tileset-source.d.ts.map +1 -1
  97. package/dist/tileset-3d/common/tileset-source.js.map +1 -1
  98. package/dist/tileset-3d/common/tileset-traverser.d.ts +33 -1
  99. package/dist/tileset-3d/common/tileset-traverser.d.ts.map +1 -1
  100. package/dist/tileset-3d/common/tileset-traverser.js +61 -14
  101. package/dist/tileset-3d/common/tileset-traverser.js.map +1 -1
  102. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts +182 -0
  103. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts.map +1 -0
  104. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js +681 -0
  105. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js.map +1 -0
  106. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts +127 -6
  107. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts.map +1 -1
  108. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js +485 -27
  109. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js.map +1 -1
  110. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts +104 -0
  111. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts.map +1 -1
  112. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js +286 -0
  113. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js.map +1 -1
  114. package/dist/tileset-3d/format-i3s/i3s-pending-tiles-register.d.ts.map +1 -1
  115. package/dist/tileset-3d/format-i3s/i3s-pending-tiles-register.js +3 -0
  116. package/dist/tileset-3d/format-i3s/i3s-pending-tiles-register.js.map +1 -1
  117. package/dist/tileset-3d/format-i3s/i3s-source.d.ts +23 -6
  118. package/dist/tileset-3d/format-i3s/i3s-source.d.ts.map +1 -1
  119. package/dist/tileset-3d/format-i3s/i3s-source.js +176 -25
  120. package/dist/tileset-3d/format-i3s/i3s-source.js.map +1 -1
  121. package/dist/tileset-3d/format-i3s/i3s-tile-manager.d.ts.map +1 -1
  122. package/dist/tileset-3d/format-i3s/i3s-tile-manager.js +3 -0
  123. package/dist/tileset-3d/format-i3s/i3s-tile-manager.js.map +1 -1
  124. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.d.ts.map +1 -1
  125. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js +4 -1
  126. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js.map +1 -1
  127. package/dist/tileset-3d/helpers/3d-tiles-options.d.ts.map +1 -1
  128. package/dist/tileset-3d/helpers/3d-tiles-options.js +3 -0
  129. package/dist/tileset-3d/helpers/3d-tiles-options.js.map +1 -1
  130. package/dist/tileset-3d/helpers/bounding-volume.d.ts +7 -3
  131. package/dist/tileset-3d/helpers/bounding-volume.d.ts.map +1 -1
  132. package/dist/tileset-3d/helpers/bounding-volume.js +20 -91
  133. package/dist/tileset-3d/helpers/bounding-volume.js.map +1 -1
  134. package/dist/tileset-3d/helpers/frame-state.d.ts +67 -2
  135. package/dist/tileset-3d/helpers/frame-state.d.ts.map +1 -1
  136. package/dist/tileset-3d/helpers/frame-state.js +92 -10
  137. package/dist/tileset-3d/helpers/frame-state.js.map +1 -1
  138. package/dist/tileset-3d/helpers/i3s-lod.d.ts.map +1 -1
  139. package/dist/tileset-3d/helpers/i3s-lod.js +20 -8
  140. package/dist/tileset-3d/helpers/i3s-lod.js.map +1 -1
  141. package/dist/tileset-3d/helpers/s2-bounding-volume.d.ts +17 -0
  142. package/dist/tileset-3d/helpers/s2-bounding-volume.d.ts.map +1 -0
  143. package/dist/tileset-3d/helpers/s2-bounding-volume.js +20 -0
  144. package/dist/tileset-3d/helpers/s2-bounding-volume.js.map +1 -0
  145. package/dist/tileset-3d/helpers/tiles-3d-lod.d.ts +61 -9
  146. package/dist/tileset-3d/helpers/tiles-3d-lod.d.ts.map +1 -1
  147. package/dist/tileset-3d/helpers/tiles-3d-lod.js +198 -93
  148. package/dist/tileset-3d/helpers/tiles-3d-lod.js.map +1 -1
  149. package/dist/tileset-3d/helpers/tiles-3d-request-priority.d.ts +134 -0
  150. package/dist/tileset-3d/helpers/tiles-3d-request-priority.d.ts.map +1 -0
  151. package/dist/tileset-3d/helpers/tiles-3d-request-priority.js +179 -0
  152. package/dist/tileset-3d/helpers/tiles-3d-request-priority.js.map +1 -0
  153. package/dist/types.d.ts +7 -0
  154. package/dist/types.d.ts.map +1 -1
  155. package/dist/types.js +3 -0
  156. package/dist/types.js.map +1 -1
  157. package/dist/utils/doubly-linked-list.d.ts.map +1 -1
  158. package/dist/utils/doubly-linked-list.js +1 -0
  159. package/dist/utils/doubly-linked-list.js.map +1 -1
  160. package/dist/utils/managed-array.d.ts.map +1 -1
  161. package/dist/utils/managed-array.js +1 -0
  162. package/dist/utils/managed-array.js.map +1 -1
  163. package/package.json +12 -10
  164. package/src/index.ts +79 -1
  165. package/src/point-cloud/point-cloud-tile.ts +4 -0
  166. package/src/point-cloud/point-cloud-tileset.ts +70 -12
  167. package/src/point-cloud/types.ts +21 -0
  168. package/src/raster-set.ts +197 -67
  169. package/src/spatial/format-spatial-reference.ts +385 -0
  170. package/src/spatial/get-spatial-coordinate-frame.ts +169 -0
  171. package/src/spatial/i3s-elevation.ts +96 -0
  172. package/src/spatial/i3s-spatial-transformer.ts +1021 -0
  173. package/src/spatial/normalize-crs-identifier.ts +26 -0
  174. package/src/spatial/spatial-coordinate-transformer.ts +377 -0
  175. package/src/spatial/spatial-resource-registry.ts +90 -0
  176. package/src/spatial/spatial-types.ts +422 -0
  177. package/src/spatial/tiles-3d-spatial-transformer.ts +194 -0
  178. package/src/tileset-2d/adapter.ts +2 -3
  179. package/src/tileset-2d/tileset-2d.ts +2 -3
  180. package/src/tileset-3d/common/indexed-archive-tileset-source.ts +13 -13
  181. package/src/tileset-3d/common/tile-3d-contracts.ts +115 -0
  182. package/src/tileset-3d/common/tile-3d.ts +629 -98
  183. package/src/tileset-3d/common/tileset-3d.ts +512 -40
  184. package/src/tileset-3d/common/tileset-cache.ts +15 -7
  185. package/src/tileset-3d/common/tileset-content-traversal.ts +114 -0
  186. package/src/tileset-3d/common/tileset-observability.ts +100 -0
  187. package/src/tileset-3d/common/tileset-source.ts +63 -13
  188. package/src/tileset-3d/common/tileset-traverser.ts +73 -15
  189. package/src/tileset-3d/format-3d-tiles/implicit-tiling.ts +1059 -0
  190. package/src/tileset-3d/format-3d-tiles/tiles-3d-source.ts +674 -28
  191. package/src/tileset-3d/format-3d-tiles/tileset-3d-traverser.ts +328 -0
  192. package/src/tileset-3d/format-i3s/i3s-pending-tiles-register.ts +4 -0
  193. package/src/tileset-3d/format-i3s/i3s-source.ts +220 -29
  194. package/src/tileset-3d/format-i3s/i3s-tile-manager.ts +4 -0
  195. package/src/tileset-3d/format-i3s/i3s-tileset-traverser.ts +6 -1
  196. package/src/tileset-3d/helpers/3d-tiles-options.ts +4 -0
  197. package/src/tileset-3d/helpers/bounding-volume.ts +31 -114
  198. package/src/tileset-3d/helpers/frame-state.ts +152 -13
  199. package/src/tileset-3d/helpers/i3s-lod.ts +22 -8
  200. package/src/tileset-3d/helpers/s2-bounding-volume.ts +35 -0
  201. package/src/tileset-3d/helpers/tiles-3d-lod.ts +291 -112
  202. package/src/tileset-3d/helpers/tiles-3d-request-priority.ts +301 -0
  203. package/src/types.ts +11 -0
  204. package/src/utils/doubly-linked-list.ts +2 -0
  205. package/src/utils/managed-array.ts +2 -0
@@ -7,14 +7,26 @@
7
7
  import {Matrix4, Vector3} from '@math.gl/core';
8
8
  import {Ellipsoid} from '@math.gl/geospatial';
9
9
  import {Stats} from '@probe.gl/stats';
10
- import {RequestScheduler, LoaderWithParser, LoaderOptions} from '@loaders.gl/loader-utils';
10
+ import {RequestScheduler, Loader, 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';
17
24
  import {TILESET_TYPE} from '../../constants';
25
+ import type {TilesetSpatialOptions, TilesetSpatialReference} from '../../spatial/spatial-types';
26
+ import {
27
+ applyTilesetSpatialOptions,
28
+ createTilesetSpatialReference
29
+ } from '../../spatial/spatial-types';
18
30
 
19
31
  import {TilesetTraverser} from './tileset-traverser';
20
32
  import {
@@ -34,8 +46,18 @@ export type Tileset3DProps = {
34
46
  loadOptions?: LoaderOptions;
35
47
  loadTiles?: boolean;
36
48
  basePath?: string;
37
- maximumMemoryUsage?: number;
38
- memoryCacheOverflow?: number;
49
+ /**
50
+ * Soft target, in bytes, for cached tile content that is not required by the current frame.
51
+ * The listed default applies to 3D Tiles; I3S retains its existing 32 MiB default.
52
+ * @default 536870912
53
+ */
54
+ cacheBytes?: number;
55
+ /**
56
+ * Additional bytes that the current-frame working set may use before memory-adjusted SSE rises.
57
+ * The listed default applies to 3D Tiles; I3S retains its existing 1 MiB default.
58
+ * @default 536870912
59
+ */
60
+ maximumCacheOverflowBytes?: number;
39
61
  maximumTilesSelected?: number;
40
62
  debounceTime?: number;
41
63
 
@@ -44,12 +66,64 @@ export type Tileset3DProps = {
44
66
 
45
67
  ellipsoid?: object;
46
68
  modelMatrix?: Matrix4;
69
+ /** Automatic CRS output options and expert source-metadata recovery overrides. */
70
+ spatial?: TilesetSpatialOptions;
47
71
 
48
72
  maximumScreenSpaceError?: number;
73
+ /**
74
+ * Enables replacement traversal that may skip hierarchy levels while retaining ready ancestors
75
+ * as coverage. This trades temporary overdraw for faster refinement on deep trees.
76
+ * Defaults to `true` for 3D Tiles and `false` for I3S. Set to `false` to request every level.
77
+ */
78
+ skipLevelOfDetail?: boolean;
79
+ /**
80
+ * SSE above which tiles remain part of the non-skipping base traversal.
81
+ * @default 1024
82
+ */
83
+ baseScreenSpaceError?: number;
84
+ /**
85
+ * Required SSE reduction from the nearest requested or loaded ancestor before loading a tile.
86
+ * @default 16
87
+ */
88
+ skipScreenSpaceErrorFactor?: number;
89
+ /**
90
+ * Minimum hierarchy levels between requested tiles during skip-LOD traversal.
91
+ * @default 1
92
+ */
93
+ skipLevels?: number;
94
+ /**
95
+ * Requests only final tiles that meet the active SSE target when skip-LOD is enabled.
96
+ * @default false
97
+ */
98
+ immediatelyLoadDesiredLevelOfDetail?: boolean;
99
+ /** Enables perspective dynamic SSE to reduce distant, horizon-facing refinement. */
100
+ dynamicScreenSpaceError?: boolean;
101
+ /** Base dynamic SSE fog density in inverse meters. */
102
+ dynamicScreenSpaceErrorDensity?: number;
103
+ /** Maximum dynamic SSE reduction in logical/CSS pixels. */
104
+ dynamicScreenSpaceErrorFactor?: number;
105
+ /** Fraction of tileset height at which dynamic SSE begins to fade, clamped to `[0, 1]`. */
106
+ dynamicScreenSpaceErrorHeightFalloff?: number;
107
+ /**
108
+ * Enables adaptive LOD reduction when estimated tile memory exceeds the overflow ceiling.
109
+ * Defaults to `true` for 3D Tiles and retains the existing `false` default for I3S.
110
+ */
49
111
  memoryAdjustedScreenSpaceError?: boolean;
50
112
  viewportTraversersMap?: any;
51
113
  updateTransforms?: boolean;
52
114
  viewDistanceScale?: number;
115
+ /** Reduced viewport-height fraction used to prioritize coarse initial coverage; `0` disables it. */
116
+ progressiveResolutionHeightFraction?: number;
117
+ /** Whether requests nearer the center of a perspective viewport receive higher priority. */
118
+ foveatedScreenSpaceError?: boolean;
119
+ /** Fraction of the perspective field of view that receives no foveated SSE relaxation. */
120
+ foveatedConeSize?: number;
121
+ /** Minimum logical-pixel SSE relaxation immediately outside the foveated cone. */
122
+ foveatedMinimumScreenSpaceErrorRelaxation?: number;
123
+ /** Function that increases SSE relaxation from the foveated cone toward the viewport edge. */
124
+ foveatedInterpolationCallback?: FoveatedInterpolationCallback;
125
+ /** Seconds peripheral requests may wait after the camera moves. */
126
+ foveatedTimeDelay?: number;
53
127
 
54
128
  onTileLoad?: (tile: Tile3D) => any;
55
129
  onTileUnload?: (tile: Tile3D) => any;
@@ -69,8 +143,10 @@ type Props = {
69
143
  modelMatrix: Matrix4;
70
144
  throttleRequests: boolean;
71
145
  maxRequests: number;
72
- maximumMemoryUsage: number;
73
- memoryCacheOverflow: number;
146
+ /** Soft target, in bytes, for evictable cached tile content. */
147
+ cacheBytes: number;
148
+ /** Current-frame headroom, in bytes, before memory-adjusted SSE rises. */
149
+ maximumCacheOverflowBytes: number;
74
150
  maximumTilesSelected: number;
75
151
  debounceTime: number;
76
152
  onTileLoad: (tile: Tile3D) => void;
@@ -81,6 +157,25 @@ type Props = {
81
157
  onTraversalComplete: (selectedTiles: Tile3D[]) => Tile3D[];
82
158
  onUpdate: () => void;
83
159
  maximumScreenSpaceError: number;
160
+ /** Whether replacement traversal may skip levels while retaining ancestor coverage. */
161
+ skipLevelOfDetail: boolean;
162
+ /** SSE above which tiles remain part of the non-skipping base traversal. */
163
+ baseScreenSpaceError: number;
164
+ /** Required SSE reduction between requested skip-LOD tiles. */
165
+ skipScreenSpaceErrorFactor: number;
166
+ /** Minimum hierarchy levels between requested skip-LOD tiles. */
167
+ skipLevels: number;
168
+ /** Whether skip-LOD requests only the final desired tiles. */
169
+ immediatelyLoadDesiredLevelOfDetail: boolean;
170
+ /** Whether perspective dynamic SSE is enabled. */
171
+ dynamicScreenSpaceError: boolean;
172
+ /** Base dynamic SSE fog density in inverse meters. */
173
+ dynamicScreenSpaceErrorDensity: number;
174
+ /** Maximum dynamic SSE reduction in logical/CSS pixels. */
175
+ dynamicScreenSpaceErrorFactor: number;
176
+ /** Fraction of tileset height at which dynamic SSE begins to fade. */
177
+ dynamicScreenSpaceErrorHeightFalloff: number;
178
+ /** Whether cache pressure may adapt the active SSE threshold. */
84
179
  memoryAdjustedScreenSpaceError: boolean;
85
180
  viewportTraversersMap: Record<string, any> | null;
86
181
  attributions: string[];
@@ -88,19 +183,83 @@ type Props = {
88
183
  loadOptions: LoaderOptions;
89
184
  updateTransforms: boolean;
90
185
  viewDistanceScale: number;
186
+ /** Reduced viewport-height fraction used to prioritize coarse initial coverage. */
187
+ progressiveResolutionHeightFraction: number;
188
+ /** Whether perspective requests are prioritized around the viewport center. */
189
+ foveatedScreenSpaceError: boolean;
190
+ /** Fraction of the perspective field of view with no foveated SSE relaxation. */
191
+ foveatedConeSize: number;
192
+ /** Minimum logical-pixel SSE relaxation outside the foveated cone. */
193
+ foveatedMinimumScreenSpaceErrorRelaxation: number;
194
+ /** Function used to interpolate foveated SSE relaxation. */
195
+ foveatedInterpolationCallback: FoveatedInterpolationCallback;
196
+ /** Seconds eligible peripheral requests wait after camera movement. */
197
+ foveatedTimeDelay: number;
91
198
  basePath: string;
92
199
  contentLoader?: (tile: Tile3D) => Promise<void>;
93
200
  i3s: Record<string, any>;
201
+ /** Automatic CRS output options and expert source-metadata recovery overrides. */
202
+ spatial: TilesetSpatialOptions;
94
203
  };
95
204
 
205
+ const BYTES_PER_MEBIBYTE = 1024 * 1024;
206
+ const DEFAULT_CACHE_BYTES = 512 * BYTES_PER_MEBIBYTE;
207
+ const DEFAULT_MAXIMUM_CACHE_OVERFLOW_BYTES = 512 * BYTES_PER_MEBIBYTE;
208
+ const DEFAULT_I3S_CACHE_BYTES = 32 * BYTES_PER_MEBIBYTE;
209
+ const DEFAULT_I3S_MAXIMUM_CACHE_OVERFLOW_BYTES = BYTES_PER_MEBIBYTE;
210
+
211
+ /**
212
+ * Validates a byte-based tile-cache budget.
213
+ *
214
+ * Fractions are accepted because JavaScript memory estimates are numbers, but negative, infinite,
215
+ * and `NaN` values cannot define a stable cache threshold.
216
+ *
217
+ * @param value - Proposed cache budget in bytes.
218
+ * @param optionName - Public option name used in an actionable error message.
219
+ * @returns The validated byte budget.
220
+ */
221
+ function validateCacheByteLength(value: number, optionName: string): number {
222
+ if (!Number.isFinite(value) || value < 0) {
223
+ throw new Error(`${optionName} must be a finite number greater than or equal to 0`);
224
+ }
225
+ return value;
226
+ }
227
+
228
+ /**
229
+ * Resolves the base cache target from the byte-native option or its fallback.
230
+ *
231
+ * @param options - Options that may contain a byte-native cache value.
232
+ * @param fallbackCacheBytes - Existing or default cache target in bytes.
233
+ * @returns The resolved and validated cache target in bytes.
234
+ */
235
+ function resolveCacheBytes(options: Tileset3DProps, fallbackCacheBytes: number): number {
236
+ const cacheBytes = options.cacheBytes ?? fallbackCacheBytes;
237
+ return validateCacheByteLength(cacheBytes, 'cacheBytes');
238
+ }
239
+
240
+ /**
241
+ * Resolves cache overflow headroom from the byte-native option or its fallback.
242
+ *
243
+ * @param options - Options that may contain a byte-native overflow value.
244
+ * @param fallbackOverflowBytes - Existing or default overflow headroom in bytes.
245
+ * @returns The resolved and validated overflow headroom in bytes.
246
+ */
247
+ function resolveMaximumCacheOverflowBytes(
248
+ options: Tileset3DProps,
249
+ fallbackOverflowBytes: number
250
+ ): number {
251
+ const maximumCacheOverflowBytes = options.maximumCacheOverflowBytes ?? fallbackOverflowBytes;
252
+ return validateCacheByteLength(maximumCacheOverflowBytes, 'maximumCacheOverflowBytes');
253
+ }
254
+
96
255
  const DEFAULT_PROPS: Props = {
97
256
  description: '',
98
257
  ellipsoid: Ellipsoid.WGS84,
99
258
  modelMatrix: new Matrix4(),
100
259
  throttleRequests: true,
101
260
  maxRequests: 64,
102
- maximumMemoryUsage: 32,
103
- memoryCacheOverflow: 1,
261
+ cacheBytes: DEFAULT_CACHE_BYTES,
262
+ maximumCacheOverflowBytes: DEFAULT_MAXIMUM_CACHE_OVERFLOW_BYTES,
104
263
  maximumTilesSelected: 0,
105
264
  debounceTime: 0,
106
265
  onTileLoad: () => {},
@@ -112,15 +271,31 @@ const DEFAULT_PROPS: Props = {
112
271
  onUpdate: () => {},
113
272
  contentLoader: undefined,
114
273
  viewDistanceScale: 1.0,
274
+ progressiveResolutionHeightFraction: 0.3,
275
+ foveatedScreenSpaceError: true,
276
+ foveatedConeSize: 0.1,
277
+ foveatedMinimumScreenSpaceErrorRelaxation: 0,
278
+ foveatedInterpolationCallback: interpolateLinearly,
279
+ foveatedTimeDelay: 0.2,
115
280
  maximumScreenSpaceError: 8,
116
- memoryAdjustedScreenSpaceError: false,
281
+ skipLevelOfDetail: false,
282
+ baseScreenSpaceError: 1024,
283
+ skipScreenSpaceErrorFactor: 16,
284
+ skipLevels: 1,
285
+ immediatelyLoadDesiredLevelOfDetail: false,
286
+ dynamicScreenSpaceError: true,
287
+ dynamicScreenSpaceErrorDensity: 2.0e-4,
288
+ dynamicScreenSpaceErrorFactor: 24,
289
+ dynamicScreenSpaceErrorHeightFalloff: 0.25,
290
+ memoryAdjustedScreenSpaceError: true,
117
291
  loadTiles: true,
118
292
  updateTransforms: true,
119
293
  viewportTraversersMap: null,
120
294
  loadOptions: {fetch: {}},
121
295
  attributions: [],
122
296
  basePath: '',
123
- i3s: {}
297
+ i3s: {},
298
+ spatial: {}
124
299
  };
125
300
 
126
301
  const TILES_TOTAL = 'Tiles In Tileset(s)';
@@ -148,7 +323,7 @@ export class Tileset3D {
148
323
 
149
324
  type: TILESET_TYPE;
150
325
  tileset: TilesetJSON | null;
151
- loader: LoaderWithParser;
326
+ loader: Loader;
152
327
  url: string;
153
328
  basePath: string;
154
329
  modelMatrix: Matrix4;
@@ -159,6 +334,18 @@ export class Tileset3D {
159
334
  root: Tile3D | null = null;
160
335
  roots: Record<string, Tile3D> = {};
161
336
  asset: Record<string, any> = {};
337
+ /** Inline metadata schema declared by the source tileset, when present. */
338
+ schema: Record<string, any> | null = null;
339
+ /** External metadata schema URI declared by the source tileset, when present. */
340
+ schemaUri: string | null = null;
341
+ /** Metadata groups declared by the source tileset, in source order. */
342
+ groups: Array<Record<string, any>> = [];
343
+ /** Tileset-wide metadata entity, preserved without property-table decoding. */
344
+ metadata: Record<string, any> | null = null;
345
+ /** Tileset statistics metadata, preserved for application-level inspection. */
346
+ statistics: unknown = null;
347
+ /** Normalized source and target coordinate reference system metadata. */
348
+ spatialReference: TilesetSpatialReference = createTilesetSpatialReference({});
162
349
 
163
350
  description = '';
164
351
  properties: any;
@@ -174,14 +361,16 @@ export class Tileset3D {
174
361
  zoom = 1;
175
362
  boundingVolume: any = null;
176
363
 
177
- dynamicScreenSpaceErrorComputedDensity = 0.0;
364
+ /** Effective dynamic SSE density calculated for the most recently traversed viewport. */
365
+ dynamicScreenSpaceErrorComputedDensity = 0;
178
366
 
179
- maximumMemoryUsage = 32;
367
+ /** Estimated bytes currently retained by loaded tile content. */
180
368
  gpuMemoryUsageInBytes = 0;
369
+ /** Active SSE threshold after optional cache-pressure adjustment, in logical/CSS pixels. */
181
370
  memoryAdjustedScreenSpaceError = 0.0;
182
371
 
183
- private _cacheBytes = 0;
184
- private _cacheOverflowBytes = 0;
372
+ private _cacheBytes = DEFAULT_CACHE_BYTES;
373
+ private _maximumCacheOverflowBytes = DEFAULT_MAXIMUM_CACHE_OVERFLOW_BYTES;
185
374
 
186
375
  _frameNumber = 0;
187
376
  private _tiles: Record<string, Tile3D> = {};
@@ -195,12 +384,17 @@ export class Tileset3D {
195
384
  private _requestedTiles: Tile3D[] = [];
196
385
  private _emptyTiles: Tile3D[] = [];
197
386
  private frameStateData: any = {};
387
+ /** Last camera pose retained independently for each viewport. */
388
+ private _cameraMotionStates: Record<string, CameraMotionState> = {};
389
+ /** Pending traversal that releases requests after camera motion settles. */
390
+ private _deferredTraversalTimeout: ReturnType<typeof setTimeout> | null = null;
198
391
 
199
392
  _traverser: TilesetTraverser;
200
393
  _cache = new TilesetCache();
201
394
  _requestScheduler: RequestScheduler;
202
395
 
203
- private _heldTiles: Set<string> = new Set();
396
+ /** Drawn fallback instances retained independently for each viewport tree. */
397
+ private _heldTiles: Set<Tile3D> = new Set();
204
398
  private updatePromise: Promise<number> | null = null;
205
399
  tilesetInitializationPromise: Promise<void>;
206
400
 
@@ -210,12 +404,33 @@ export class Tileset3D {
210
404
  * @param options Traversal and runtime options.
211
405
  */
212
406
  constructor(source: Tileset3DSource, options?: Tileset3DProps) {
213
- this.options = {...DEFAULT_PROPS, ...options};
214
- this.loadOptions = this.options.loadOptions || {};
215
-
216
407
  if (!isTileset3DSource(source)) {
217
408
  throw new Error('Tileset3D requires a Tileset3DSource instance');
218
409
  }
410
+
411
+ const suppliedOptions = options || {};
412
+ const usesTiles3DCacheDefaults = source.type === TILESET_TYPE.TILES3D;
413
+ const defaultCacheBytes = usesTiles3DCacheDefaults
414
+ ? DEFAULT_PROPS.cacheBytes
415
+ : DEFAULT_I3S_CACHE_BYTES;
416
+ const defaultMaximumCacheOverflowBytes = usesTiles3DCacheDefaults
417
+ ? DEFAULT_PROPS.maximumCacheOverflowBytes
418
+ : DEFAULT_I3S_MAXIMUM_CACHE_OVERFLOW_BYTES;
419
+ const cacheBytes = resolveCacheBytes(suppliedOptions, defaultCacheBytes);
420
+ const maximumCacheOverflowBytes = resolveMaximumCacheOverflowBytes(
421
+ suppliedOptions,
422
+ defaultMaximumCacheOverflowBytes
423
+ );
424
+ this.options = {
425
+ ...DEFAULT_PROPS,
426
+ ...suppliedOptions,
427
+ cacheBytes,
428
+ maximumCacheOverflowBytes,
429
+ skipLevelOfDetail: suppliedOptions.skipLevelOfDetail ?? source.type === TILESET_TYPE.TILES3D,
430
+ memoryAdjustedScreenSpaceError:
431
+ suppliedOptions.memoryAdjustedScreenSpaceError ?? usesTiles3DCacheDefaults
432
+ };
433
+ this.loadOptions = this.options.loadOptions || {};
219
434
  this.source = source;
220
435
 
221
436
  this.tileset = null;
@@ -237,8 +452,15 @@ export class Tileset3D {
237
452
  });
238
453
 
239
454
  this.memoryAdjustedScreenSpaceError = this.options.maximumScreenSpaceError;
240
- this._cacheBytes = this.options.maximumMemoryUsage * 1024 * 1024;
241
- this._cacheOverflowBytes = this.options.memoryCacheOverflow * 1024 * 1024;
455
+ this._cacheBytes = cacheBytes;
456
+ this._maximumCacheOverflowBytes = maximumCacheOverflowBytes;
457
+ const metadata = this._getSourceMetadata();
458
+ if (metadata) {
459
+ this.spatialReference = applyTilesetSpatialOptions(
460
+ metadata.spatialReference,
461
+ this.options.spatial
462
+ );
463
+ }
242
464
 
243
465
  this.stats = new Stats({id: this.url});
244
466
  this._initializeStats();
@@ -247,6 +469,8 @@ export class Tileset3D {
247
469
  }
248
470
 
249
471
  destroy(): void {
472
+ this._clearDeferredTraversal();
473
+ this.source.destroy?.();
250
474
  this._destroy();
251
475
  }
252
476
 
@@ -262,14 +486,84 @@ export class Tileset3D {
262
486
  return this._frameNumber;
263
487
  }
264
488
 
489
+ /**
490
+ * Returns the tiles requested during the most recent completed traversal.
491
+ *
492
+ * A defensive copy makes request ordering deterministic for diagnostics and prevents callers
493
+ * from mutating the scheduler's working set.
494
+ */
495
+ get requestedTiles(): readonly Tile3D[] {
496
+ return this._requestedTiles.slice();
497
+ }
498
+
499
+ /**
500
+ * Returns empty tiles considered during the most recent completed traversal.
501
+ *
502
+ * Empty tiles can still be useful hierarchy placeholders, but their IDs are included in
503
+ * observability snapshots so applications can distinguish traversal work from render content.
504
+ */
505
+ get emptyTiles(): readonly Tile3D[] {
506
+ return this._emptyTiles.slice();
507
+ }
508
+
509
+ /**
510
+ * Gets or sets the soft target, in bytes, for cached tile content not needed this frame.
511
+ *
512
+ * Current-frame tiles remain protected even when their estimated memory exceeds this target.
513
+ * Reducing the value makes unused least-recently-used tiles eligible for eviction on the next
514
+ * traversal.
515
+ * @default 536870912 for 3D Tiles; 33554432 for I3S
516
+ */
517
+ get cacheBytes(): number {
518
+ return this._cacheBytes;
519
+ }
520
+
521
+ set cacheBytes(value: number) {
522
+ this._cacheBytes = validateCacheByteLength(value, 'cacheBytes');
523
+ this.options.cacheBytes = this._cacheBytes;
524
+ }
525
+
526
+ /**
527
+ * Gets or sets the additional current-frame headroom, in bytes, before memory-adjusted SSE rises.
528
+ *
529
+ * The pressure ceiling is `cacheBytes + maximumCacheOverflowBytes`. This value does not change
530
+ * the base target used to evict tiles that were not touched in the current frame.
531
+ * @default 536870912 for 3D Tiles; 1048576 for I3S
532
+ */
533
+ get maximumCacheOverflowBytes(): number {
534
+ return this._maximumCacheOverflowBytes;
535
+ }
536
+
537
+ set maximumCacheOverflowBytes(value: number) {
538
+ this._maximumCacheOverflowBytes = validateCacheByteLength(value, 'maximumCacheOverflowBytes');
539
+ this.options.maximumCacheOverflowBytes = this._maximumCacheOverflowBytes;
540
+ }
541
+
265
542
  get queryParams(): string {
266
543
  const rootUrl = this.source.getTileUrl(this.url);
267
544
  const queryIndex = rootUrl.indexOf('?');
268
545
  return queryIndex >= 0 ? rootUrl.slice(queryIndex + 1) : '';
269
546
  }
270
547
 
548
+ /**
549
+ * Updates traversal and runtime options while retaining unspecified byte budgets.
550
+ *
551
+ * @param props - Partial runtime options to apply.
552
+ */
271
553
  setProps(props: Tileset3DProps): void {
272
- this.options = {...this.options, ...props};
554
+ const cacheBytes = resolveCacheBytes(props, this.cacheBytes);
555
+ const maximumCacheOverflowBytes = resolveMaximumCacheOverflowBytes(
556
+ props,
557
+ this.maximumCacheOverflowBytes
558
+ );
559
+ this.options = {
560
+ ...this.options,
561
+ ...props,
562
+ cacheBytes,
563
+ maximumCacheOverflowBytes
564
+ };
565
+ this._cacheBytes = cacheBytes;
566
+ this._maximumCacheOverflowBytes = maximumCacheOverflowBytes;
273
567
  }
274
568
 
275
569
  getTileUrl(tilePath: string): string {
@@ -312,13 +606,20 @@ export class Tileset3D {
312
606
  return this.updatePromise;
313
607
  }
314
608
 
609
+ /**
610
+ * Moves the active LOD threshold toward the configured SSE or away from cache pressure.
611
+ *
612
+ * Usage below {@link Tileset3D.cacheBytes} restores quality in two-percent steps. Usage above
613
+ * `cacheBytes + maximumCacheOverflowBytes` reduces refinement in two-percent steps. Values in
614
+ * the overflow window leave the threshold unchanged, preventing rapid load/evict oscillation.
615
+ */
315
616
  adjustScreenSpaceError(): void {
316
- if (this.gpuMemoryUsageInBytes < this._cacheBytes) {
617
+ if (this.gpuMemoryUsageInBytes < this.cacheBytes) {
317
618
  this.memoryAdjustedScreenSpaceError = Math.max(
318
619
  this.memoryAdjustedScreenSpaceError / 1.02,
319
620
  this.options.maximumScreenSpaceError
320
621
  );
321
- } else if (this.gpuMemoryUsageInBytes > this._cacheBytes + this._cacheOverflowBytes) {
622
+ } else if (this.gpuMemoryUsageInBytes > this.cacheBytes + this.maximumCacheOverflowBytes) {
322
623
  this.memoryAdjustedScreenSpaceError *= 1.02;
323
624
  }
324
625
  }
@@ -331,10 +632,20 @@ export class Tileset3D {
331
632
  return;
332
633
  }
333
634
  const preparedViewports = viewports instanceof Array ? viewports : [viewports];
635
+ const activeViewportIds = new Set(preparedViewports.map(viewport => viewport.id));
636
+ // A viewport may be removed between frames. Discard its completed state before aggregating
637
+ // requested and selected tiles so observability reflects only the current traversal inputs.
638
+ for (const frameStateId of Object.keys(this.frameStateData)) {
639
+ if (!activeViewportIds.has(frameStateId)) {
640
+ delete this.frameStateData[frameStateId];
641
+ }
642
+ }
334
643
 
335
644
  this._cache.reset();
336
645
  this._frameNumber++;
337
646
  this.traverseCounter = preparedViewports.length;
647
+ const currentTime = Date.now();
648
+ let minimumDeferredTraversalDelay = Number.POSITIVE_INFINITY;
338
649
  const viewportsToTraverse: string[] = [];
339
650
  for (const viewport of preparedViewports) {
340
651
  const id = viewport.id;
@@ -355,7 +666,70 @@ export class Tileset3D {
355
666
  continue;
356
667
  }
357
668
  const frameState = getFrameState(viewport as GeospatialViewport, this._frameNumber);
358
- this._traverser.traverse(this.roots[id], frameState, this.options);
669
+ const root = this.roots[id];
670
+ const cameraMotionUpdate = updateCameraMotionState(
671
+ this._cameraMotionStates[id],
672
+ frameState.camera.position,
673
+ frameState.camera.direction,
674
+ currentTime
675
+ );
676
+ this._cameraMotionStates[id] = cameraMotionUpdate.state;
677
+ frameState.camera.timeSinceMovement = cameraMotionUpdate.timeSinceMovement;
678
+ // Dynamic SSE reads the root's inverse computed transform before traversal updates tile
679
+ // visibility. Refresh it from the current tileset model matrix here so animated local
680
+ // box/sphere tilesets never calculate their height falloff one transform behind the frame.
681
+ updateRootTransformForDynamicScreenSpaceError(root, this.modelMatrix);
682
+ frameState.dynamicScreenSpaceErrorDensity =
683
+ this.type === TILESET_TYPE.TILES3D &&
684
+ this.options.dynamicScreenSpaceError &&
685
+ !frameState.viewport.orthographic
686
+ ? calculateDynamicScreenSpaceErrorDensity(root, frameState, this.options)
687
+ : 0;
688
+ // Keep the legacy diagnostic field, but traversal reads the per-viewport frame value above.
689
+ this.dynamicScreenSpaceErrorComputedDensity = frameState.dynamicScreenSpaceErrorDensity;
690
+ this._traverser.traverse(root, frameState, this.options);
691
+ if (Object.keys(this._traverser.deferredTiles).length > 0) {
692
+ minimumDeferredTraversalDelay = Math.min(
693
+ minimumDeferredTraversalDelay,
694
+ Math.max(this.options.foveatedTimeDelay - cameraMotionUpdate.timeSinceMovement, 0)
695
+ );
696
+ }
697
+ }
698
+
699
+ if (Number.isFinite(minimumDeferredTraversalDelay)) {
700
+ this._scheduleDeferredTraversal(minimumDeferredTraversalDelay);
701
+ } else {
702
+ this._clearDeferredTraversal();
703
+ }
704
+ }
705
+
706
+ /**
707
+ * Schedules one follow-up traversal when the moving-camera deferral window expires.
708
+ *
709
+ * Replacing an existing timer ensures continuing camera movement pushes the retry window forward
710
+ * instead of creating a queue of redundant traversals.
711
+ *
712
+ * @param delayInSeconds - Remaining stationary delay before deferred requests become eligible.
713
+ */
714
+ private _scheduleDeferredTraversal(delayInSeconds: number): void {
715
+ this._clearDeferredTraversal();
716
+ this._deferredTraversalTimeout = setTimeout(
717
+ () => {
718
+ this._deferredTraversalTimeout = null;
719
+ this.selectTiles().catch((error: unknown) => {
720
+ const traversalError = error instanceof Error ? error : new Error(String(error));
721
+ this._onTilesetError(traversalError);
722
+ });
723
+ },
724
+ Math.max(delayInSeconds * 1000, 0)
725
+ );
726
+ }
727
+
728
+ /** Clears a pending moving-camera follow-up traversal. */
729
+ private _clearDeferredTraversal(): void {
730
+ if (this._deferredTraversalTimeout !== null) {
731
+ clearTimeout(this._deferredTraversalTimeout);
732
+ this._deferredTraversalTimeout = null;
359
733
  }
360
734
  }
361
735
 
@@ -410,28 +784,50 @@ export class Tileset3D {
410
784
 
411
785
  this.selectedTiles = this.options.onTraversalComplete(this.selectedTiles);
412
786
 
413
- const selectedIds = new Set(this.selectedTiles.map(tile => tile.id));
787
+ const selectedTiles = new Set(this.selectedTiles);
414
788
  const hasUndrawnTiles = this.selectedTiles.some(tile => !tile.tileDrawn);
415
789
 
416
790
  let heldBackCount = 0;
417
791
  if (hasUndrawnTiles) {
418
- for (const tileId of selectedIds) {
419
- this._heldTiles.add(tileId);
792
+ const activeRoots = new Set(
793
+ Object.keys(this.frameStateData).map(viewportId => this.roots[viewportId])
794
+ );
795
+ const descendantReadiness = new Map<Tile3D, boolean>();
796
+ for (const tile of this.selectedTiles) {
797
+ for (let ancestor = tile.parent; ancestor; ancestor = ancestor.parent) {
798
+ descendantReadiness.set(
799
+ ancestor,
800
+ (descendantReadiness.get(ancestor) ?? true) && tile.tileDrawn
801
+ );
802
+ }
420
803
  }
421
- for (const tileId of this._heldTiles) {
422
- if (selectedIds.has(tileId)) continue;
423
-
424
- const tile = this._tiles[tileId];
425
- if (tile && tile.contentAvailable) {
804
+ // Drawing can finish between traversals. Promote only previously selected
805
+ // geometry that actually drew; pending tiles cannot provide visible fallback.
806
+ for (const tile of previousSelectedTiles) {
807
+ if (tile.tileDrawn) this._heldTiles.add(tile);
808
+ }
809
+ for (const tile of this._heldTiles) {
810
+ let root = tile;
811
+ while (root.parent) root = root.parent;
812
+ // Removed viewport trees cannot contribute fallback to the remaining views.
813
+ if (!activeRoots.has(root)) {
814
+ this._heldTiles.delete(tile);
815
+ continue;
816
+ }
817
+ if (selectedTiles.has(tile)) continue;
818
+ if (
819
+ tile.contentAvailable &&
820
+ !this._hasDrawnReplacement(tile, selectedTiles, descendantReadiness)
821
+ ) {
426
822
  tile._selectedFrame = this._frameNumber;
427
823
  this.selectedTiles.push(tile);
428
824
  heldBackCount++;
429
825
  } else {
430
- this._heldTiles.delete(tileId);
826
+ this._heldTiles.delete(tile);
431
827
  }
432
828
  }
433
829
  } else {
434
- this._heldTiles = selectedIds;
830
+ this._heldTiles = selectedTiles;
435
831
  }
436
832
 
437
833
  if (heldBackCount > 0) {
@@ -453,6 +849,24 @@ export class Tileset3D {
453
849
  }
454
850
  }
455
851
 
852
+ /**
853
+ * Releases fallback geometry once its own selected replacement has drawn.
854
+ * Unrelated pending branches must not delay this region's handoff. When no related
855
+ * replacement is selected, retain the fallback until the complete selection has drawn.
856
+ */
857
+ private _hasDrawnReplacement(
858
+ tile: Tile3D,
859
+ selectedTiles: Set<Tile3D>,
860
+ descendantReadiness: Map<Tile3D, boolean>
861
+ ): boolean {
862
+ for (let ancestor = tile.parent; ancestor; ancestor = ancestor.parent) {
863
+ if (selectedTiles.has(ancestor)) {
864
+ return ancestor.tileDrawn;
865
+ }
866
+ }
867
+ return descendantReadiness.get(tile) === true;
868
+ }
869
+
456
870
  _tilesChanged(oldSelectedTiles: Tile3D[], selectedTiles: Tile3D[]): boolean {
457
871
  if (oldSelectedTiles.length !== selectedTiles.length) {
458
872
  return true;
@@ -475,6 +889,9 @@ export class Tileset3D {
475
889
 
476
890
  _unloadTiles(): void {
477
891
  this._cache.unloadTiles(this, (tileset, tile) => tileset._unloadTile(tile));
892
+ if (this.options.memoryAdjustedScreenSpaceError) {
893
+ this.adjustScreenSpaceError();
894
+ }
478
895
  }
479
896
 
480
897
  _updateStats(): void {
@@ -483,10 +900,13 @@ export class Tileset3D {
483
900
  for (const tile of this.selectedTiles) {
484
901
  if (tile.contentAvailable && tile.content) {
485
902
  tilesRenderable++;
486
- if (tile.content.pointCount) {
487
- pointsRenderable += tile.content.pointCount;
488
- } else {
489
- pointsRenderable += tile.content.vertexCount;
903
+ const contents = tile.contents.length ? tile.contents : [tile.content];
904
+ for (const content of contents) {
905
+ if (content.pointCount) {
906
+ pointsRenderable += content.pointCount;
907
+ } else {
908
+ pointsRenderable += content.vertexCount || 0;
909
+ }
490
910
  }
491
911
  }
492
912
  }
@@ -506,7 +926,8 @@ export class Tileset3D {
506
926
  }
507
927
  if (
508
928
  initialMetadata?.tileset?.root &&
509
- typeof initialMetadata.tileset.root.then !== 'function'
929
+ typeof initialMetadata.tileset.root.then !== 'function' &&
930
+ (!this.source.prepareTileset || this.spatialReference?.status === 'native')
510
931
  ) {
511
932
  this.root = this._initializeTileHeaders(initialMetadata.tileset, null);
512
933
  this._applyViewState();
@@ -515,6 +936,7 @@ export class Tileset3D {
515
936
  await initializePromise;
516
937
  const metadata = this.source.getMetadata();
517
938
  this._syncSourceState(metadata);
939
+ await this.source.prepareTileset?.(this);
518
940
  if (!this.root) {
519
941
  this.root = this._initializeTileHeaders(metadata.tileset, null);
520
942
  }
@@ -571,6 +993,28 @@ export class Tileset3D {
571
993
  }
572
994
  }
573
995
 
996
+ /**
997
+ * Loads a source-managed lazy child-header group and reports failures through tile callbacks.
998
+ *
999
+ * Subtree metadata shares the normal loading counters but is not reported as render content and
1000
+ * is not added to the GPU cache. Traversal is notified separately after the promise settles.
1001
+ *
1002
+ * @param tile - Runtime tile that owns the lazy subtree reference.
1003
+ * @param frameState - View state that made the subtree eligible for refinement.
1004
+ */
1005
+ async _loadTileChildren(tile: Tile3D, frameState: FrameState): Promise<void> {
1006
+ try {
1007
+ this._onStartTileLoading();
1008
+ await tile.loadChildren(frameState);
1009
+ } catch (error: unknown) {
1010
+ const tileError = error instanceof Error ? error : new Error('subtree load failed');
1011
+ this._onSourceError(tileError, tile);
1012
+ this._onTileLoadError(tile, tileError);
1013
+ } finally {
1014
+ this._onEndTileLoading();
1015
+ }
1016
+ }
1017
+
574
1018
  _onTileLoadError(tile: Tile3D, error: Error): void {
575
1019
  this.stats.get(TILES_LOAD_FAILED).incrementCount();
576
1020
 
@@ -698,6 +1142,20 @@ export class Tileset3D {
698
1142
  this.refine = metadata.refine;
699
1143
  this.contentFormats = this.source.contentFormats;
700
1144
  this.asset = this.source.asset || this.asset;
1145
+ const tileset = metadata.tileset as Record<string, any>;
1146
+ this.schema = tileset.schema || null;
1147
+ this.schemaUri = tileset.schemaUri || null;
1148
+ this.groups = tileset.groups || [];
1149
+ this.metadata = tileset.metadata || null;
1150
+ this.statistics = tileset.statistics ?? null;
1151
+ const spatialReference = applyTilesetSpatialOptions(
1152
+ metadata.spatialReference,
1153
+ this.options.spatial
1154
+ );
1155
+ this.spatialReference = preserveTransformedSpatialReference(
1156
+ this.spatialReference,
1157
+ spatialReference
1158
+ );
701
1159
  this.properties = this.source.properties ?? this.properties;
702
1160
  this.extras = this.source.extras ?? this.extras;
703
1161
  if (this.options.attributions?.length) {
@@ -734,3 +1192,17 @@ export class Tileset3D {
734
1192
  }
735
1193
  }
736
1194
  }
1195
+
1196
+ /** Preserve a completed runtime operation when asynchronous source metadata describes the same request. */
1197
+ function preserveTransformedSpatialReference(
1198
+ current: TilesetSpatialReference,
1199
+ next: TilesetSpatialReference
1200
+ ): TilesetSpatialReference {
1201
+ return current?.status === 'transformed' &&
1202
+ current.sourceCrs === next.sourceCrs &&
1203
+ current.targetCrs === next.targetCrs &&
1204
+ current.targetHeightReference === next.targetHeightReference &&
1205
+ current.outputCoordinates === next.outputCoordinates
1206
+ ? current
1207
+ : next;
1208
+ }