@loaders.gl/tiles 5.0.0-alpha.2 → 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 (109) hide show
  1. package/LICENSE +1 -1
  2. package/dist/dist.dev.js +17325 -5650
  3. package/dist/dist.min.js +4 -1
  4. package/dist/index.cjs +1924 -178
  5. package/dist/index.cjs.map +3 -3
  6. package/dist/index.d.ts +9 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +6 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/point-cloud/point-cloud-tileset.d.ts +4 -0
  11. package/dist/point-cloud/point-cloud-tileset.d.ts.map +1 -1
  12. package/dist/point-cloud/point-cloud-tileset.js +51 -12
  13. package/dist/point-cloud/point-cloud-tileset.js.map +1 -1
  14. package/dist/point-cloud/types.d.ts +17 -0
  15. package/dist/point-cloud/types.d.ts.map +1 -1
  16. package/dist/raster-set.d.ts +10 -8
  17. package/dist/raster-set.d.ts.map +1 -1
  18. package/dist/raster-set.js +18 -1
  19. package/dist/raster-set.js.map +1 -1
  20. package/dist/spatial/format-spatial-reference.d.ts +67 -0
  21. package/dist/spatial/format-spatial-reference.d.ts.map +1 -0
  22. package/dist/spatial/format-spatial-reference.js +233 -0
  23. package/dist/spatial/format-spatial-reference.js.map +1 -0
  24. package/dist/spatial/get-spatial-coordinate-frame.d.ts +9 -0
  25. package/dist/spatial/get-spatial-coordinate-frame.d.ts.map +1 -0
  26. package/dist/spatial/get-spatial-coordinate-frame.js +58 -0
  27. package/dist/spatial/get-spatial-coordinate-frame.js.map +1 -0
  28. package/dist/spatial/i3s-elevation.d.ts +20 -0
  29. package/dist/spatial/i3s-elevation.d.ts.map +1 -0
  30. package/dist/spatial/i3s-elevation.js +90 -0
  31. package/dist/spatial/i3s-elevation.js.map +1 -0
  32. package/dist/spatial/i3s-spatial-transformer.d.ts +189 -0
  33. package/dist/spatial/i3s-spatial-transformer.d.ts.map +1 -0
  34. package/dist/spatial/i3s-spatial-transformer.js +686 -0
  35. package/dist/spatial/i3s-spatial-transformer.js.map +1 -0
  36. package/dist/spatial/spatial-coordinate-transformer.d.ts +48 -0
  37. package/dist/spatial/spatial-coordinate-transformer.d.ts.map +1 -0
  38. package/dist/spatial/spatial-coordinate-transformer.js +211 -0
  39. package/dist/spatial/spatial-coordinate-transformer.js.map +1 -0
  40. package/dist/spatial/spatial-resource-registry.d.ts +44 -0
  41. package/dist/spatial/spatial-resource-registry.d.ts.map +1 -0
  42. package/dist/spatial/spatial-resource-registry.js +67 -0
  43. package/dist/spatial/spatial-resource-registry.js.map +1 -0
  44. package/dist/spatial/spatial-types.d.ts +190 -0
  45. package/dist/spatial/spatial-types.d.ts.map +1 -0
  46. package/dist/spatial/spatial-types.js +192 -0
  47. package/dist/spatial/spatial-types.js.map +1 -0
  48. package/dist/tileset-3d/common/tile-3d.d.ts +49 -1
  49. package/dist/tileset-3d/common/tile-3d.d.ts.map +1 -1
  50. package/dist/tileset-3d/common/tile-3d.js +132 -64
  51. package/dist/tileset-3d/common/tile-3d.js.map +1 -1
  52. package/dist/tileset-3d/common/tileset-3d.d.ts +25 -0
  53. package/dist/tileset-3d/common/tileset-3d.d.ts.map +1 -1
  54. package/dist/tileset-3d/common/tileset-3d.js +49 -7
  55. package/dist/tileset-3d/common/tileset-3d.js.map +1 -1
  56. package/dist/tileset-3d/common/tileset-source.d.ts +14 -0
  57. package/dist/tileset-3d/common/tileset-source.d.ts.map +1 -1
  58. package/dist/tileset-3d/common/tileset-source.js.map +1 -1
  59. package/dist/tileset-3d/common/tileset-traverser.d.ts +6 -1
  60. package/dist/tileset-3d/common/tileset-traverser.d.ts.map +1 -1
  61. package/dist/tileset-3d/common/tileset-traverser.js +11 -6
  62. package/dist/tileset-3d/common/tileset-traverser.js.map +1 -1
  63. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts +26 -1
  64. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts.map +1 -1
  65. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js +62 -18
  66. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js.map +1 -1
  67. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts +1 -7
  68. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts.map +1 -1
  69. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js +32 -55
  70. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js.map +1 -1
  71. package/dist/tileset-3d/format-i3s/i3s-source.d.ts +14 -1
  72. package/dist/tileset-3d/format-i3s/i3s-source.d.ts.map +1 -1
  73. package/dist/tileset-3d/format-i3s/i3s-source.js +107 -17
  74. package/dist/tileset-3d/format-i3s/i3s-source.js.map +1 -1
  75. package/dist/tileset-3d/helpers/bounding-volume.d.ts.map +1 -1
  76. package/dist/tileset-3d/helpers/bounding-volume.js +3 -33
  77. package/dist/tileset-3d/helpers/bounding-volume.js.map +1 -1
  78. package/dist/tileset-3d/helpers/frame-state.d.ts +3 -1
  79. package/dist/tileset-3d/helpers/frame-state.d.ts.map +1 -1
  80. package/dist/tileset-3d/helpers/frame-state.js +1 -0
  81. package/dist/tileset-3d/helpers/frame-state.js.map +1 -1
  82. package/dist/tileset-3d/helpers/i3s-lod.d.ts.map +1 -1
  83. package/dist/tileset-3d/helpers/i3s-lod.js +20 -8
  84. package/dist/tileset-3d/helpers/i3s-lod.js.map +1 -1
  85. package/dist/types.d.ts +3 -0
  86. package/dist/types.d.ts.map +1 -1
  87. package/package.json +12 -9
  88. package/src/index.ts +42 -1
  89. package/src/point-cloud/point-cloud-tileset.ts +66 -12
  90. package/src/point-cloud/types.ts +17 -0
  91. package/src/raster-set.ts +46 -11
  92. package/src/spatial/format-spatial-reference.ts +311 -0
  93. package/src/spatial/get-spatial-coordinate-frame.ts +74 -0
  94. package/src/spatial/i3s-elevation.ts +96 -0
  95. package/src/spatial/i3s-spatial-transformer.ts +957 -0
  96. package/src/spatial/spatial-coordinate-transformer.ts +276 -0
  97. package/src/spatial/spatial-resource-registry.ts +79 -0
  98. package/src/spatial/spatial-types.ts +422 -0
  99. package/src/tileset-3d/common/tile-3d.ts +156 -69
  100. package/src/tileset-3d/common/tileset-3d.ts +77 -6
  101. package/src/tileset-3d/common/tileset-source.ts +15 -0
  102. package/src/tileset-3d/common/tileset-traverser.ts +11 -6
  103. package/src/tileset-3d/format-3d-tiles/implicit-tiling.ts +100 -22
  104. package/src/tileset-3d/format-3d-tiles/tiles-3d-source.ts +36 -56
  105. package/src/tileset-3d/format-i3s/i3s-source.ts +119 -18
  106. package/src/tileset-3d/helpers/bounding-volume.ts +3 -55
  107. package/src/tileset-3d/helpers/frame-state.ts +3 -0
  108. package/src/tileset-3d/helpers/i3s-lod.ts +22 -8
  109. package/src/types.ts +3 -0
@@ -7,6 +7,7 @@
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';
@@ -33,6 +34,57 @@ import {
33
34
 
34
35
  const scratchVector = new Vector3();
35
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
+
36
88
  function defined(x) {
37
89
  return x !== undefined && x !== null;
38
90
  }
@@ -69,6 +121,8 @@ export class Tile3D {
69
121
  refine: TILE_REFINEMENT;
70
122
  type: string;
71
123
  contentUrl: string;
124
+ /** Resolved render-content URLs in source order; empty for contentless tiles. */
125
+ contentUrls: string[] = [];
72
126
  /** Different refinement algorithms used by I3S and 3D tiles */
73
127
  lodMetricType = 'geometricError';
74
128
  /**
@@ -91,6 +145,17 @@ export class Tile3D {
91
145
  * not the content's metadata in the tileset JSON file.
92
146
  */
93
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> = [];
94
159
  contentState: number = TILE_CONTENT_STATE.UNLOADED;
95
160
  gpuMemoryUsageInBytes: number = 0;
96
161
 
@@ -143,8 +208,26 @@ export class Tile3D {
143
208
  private _visible: boolean | undefined = undefined;
144
209
 
145
210
  private _contentBoundingVolume: any;
211
+ private _contentBoundingVolumes: any[] = [];
212
+ /** Source content-volume headers retained after render content is unloaded. */
213
+ private _contentBoundingVolumeHeaders: any[] = [];
146
214
  private _viewerRequestVolume: any;
147
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
+
148
231
  _initialTransform: Matrix4 = new Matrix4();
149
232
 
150
233
  // Used by traverser, cannot be marked private
@@ -199,6 +282,12 @@ export class Tile3D {
199
282
  this.refine = this._getRefine(header.refine);
200
283
  this.type = header.type;
201
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);
202
291
  this.childrenState = header.implicitSubtree ? 'unloaded' : 'ready';
203
292
 
204
293
  this._initializeLodMetric(header);
@@ -368,7 +457,11 @@ export class Tile3D {
368
457
  * Memory usage of tile on GPU
369
458
  */
370
459
  _getGpuMemoryUsageInBytes(): number {
371
- 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
+ );
372
465
  }
373
466
 
374
467
  /**
@@ -487,6 +580,11 @@ export class Tile3D {
487
580
  try {
488
581
  const loadResult = await this.tileset.source.loadTileContent(this);
489
582
 
583
+ if (loadResult.contents) {
584
+ this.contents = loadResult.contents;
585
+ this.content = this.contents[0] || null;
586
+ }
587
+
490
588
  if (this.tileset.options.contentLoader) {
491
589
  await this.tileset.options.contentLoader(this);
492
590
  }
@@ -585,6 +683,9 @@ export class Tile3D {
585
683
  this.refine = this._getRefine(materializedHeader.refine);
586
684
  this.type = materializedHeader.type;
587
685
  this.contentUrl = materializedHeader.contentUrl;
686
+ this.contentUrls =
687
+ materializedHeader.contentUrls ||
688
+ (materializedHeader.contentUrl ? [materializedHeader.contentUrl] : []);
588
689
  this._initializeLodMetric(materializedHeader);
589
690
  this._updateLodMetricScale();
590
691
  this._initializeBoundingVolumes(this.header);
@@ -595,9 +696,12 @@ export class Tile3D {
595
696
 
596
697
  // Unloads the tile's content.
597
698
  unloadContent() {
598
- if (this.content && this.content.destroy) {
599
- this.content.destroy();
699
+ for (const content of this.contents.length ? this.contents : [this.content]) {
700
+ if (content && content.destroy) {
701
+ content.destroy();
702
+ }
600
703
  }
704
+ this.contents = [];
601
705
  this.content = null;
602
706
  if (this.header.content && this.header.content.destroy) {
603
707
  this.header.content.destroy();
@@ -732,47 +836,22 @@ export class Tile3D {
732
836
  return cullingVolume.computeVisibilityWithPlaneMask(boundingVolume, parentVisibilityPlaneMask);
733
837
  }
734
838
 
735
- // Assuming the tile's bounding volume intersects the culling volume, determines
736
- // whether the tile's content's bounding volume intersects the culling volume.
737
- // @param {FrameState} frameState The frame state.
738
- // @returns {Intersect} The result of the intersection: the tile's content is completely outside, completely inside, or intersecting the culling volume.
739
- contentVisibility() {
740
- return true;
741
-
742
- // TODO restore
743
- /*
744
- // Assumes the tile's bounding volume intersects the culling volume already, so
745
- // just return Intersect.INSIDE if there is no content bounding volume.
746
- if (!defined(this.contentBoundingVolume)) {
747
- return Intersect.INSIDE;
748
- }
749
-
750
- if (this._visibilityPlaneMask === CullingVolume.MASK_INSIDE) {
751
- // The tile's bounding volume is completely inside the culling volume so
752
- // the content bounding volume must also be inside.
753
- return Intersect.INSIDE;
754
- }
755
-
756
- // PERFORMANCE_IDEA: is it possible to burn less CPU on this test since we know the
757
- // tile's (not the content's) bounding volume intersects the culling volume?
758
- const cullingVolume = frameState.cullingVolume;
759
- const boundingVolume = tile.contentBoundingVolume;
760
-
761
- const tileset = this.tileset;
762
- const clippingPlanes = tileset.clippingPlanes;
763
- if (defined(clippingPlanes) && clippingPlanes.enabled) {
764
- const intersection = clippingPlanes.computeIntersectionWithBoundingVolume(
765
- boundingVolume,
766
- tileset.clippingPlanesOriginMatrix
767
- );
768
- this._isClipped = intersection !== Intersect.INSIDE;
769
- if (intersection === Intersect.OUTSIDE) {
770
- return Intersect.OUTSIDE;
771
- }
772
- }
773
-
774
- return cullingVolume.computeVisibility(boundingVolume);
775
- */
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
+ );
776
855
  }
777
856
 
778
857
  /**
@@ -898,6 +977,7 @@ export class Tile3D {
898
977
 
899
978
  _initializeBoundingVolumes(tileHeader) {
900
979
  this._contentBoundingVolume = null;
980
+ this._contentBoundingVolumeHeaders = [];
901
981
  this._viewerRequestVolume = null;
902
982
 
903
983
  this._updateBoundingVolume(tileHeader);
@@ -906,6 +986,7 @@ export class Tile3D {
906
986
  _initializeContent(tileHeader) {
907
987
  // Empty tile by default
908
988
  this.content = {_tileset: this.tileset, _tile: this};
989
+ this.contents = [];
909
990
  this.hasEmptyContent = true;
910
991
  this.contentState = TILE_CONTENT_STATE.UNLOADED;
911
992
 
@@ -913,7 +994,7 @@ export class Tile3D {
913
994
  // This is `false` until the tile's content is loaded.
914
995
  this.hasTilesetContent = false;
915
996
 
916
- if (tileHeader.contentUrl) {
997
+ if (tileHeader.contentUrl || tileHeader.contentUrls?.length) {
917
998
  this.content = null;
918
999
  this.hasEmptyContent = false;
919
1000
  }
@@ -961,18 +1042,23 @@ export class Tile3D {
961
1042
  * `content`.
962
1043
  */
963
1044
  _isTileset(): boolean {
964
- return this.content?.shape === 'tileset3d';
1045
+ return (this.contents.length ? this.contents : [this.content]).some(
1046
+ content => content?.shape === 'tileset3d'
1047
+ );
965
1048
  }
966
1049
 
967
1050
  _onContentLoaded() {
968
1051
  // Vector and Geometry tile rendering do not support the skip LOD optimization.
969
- switch (this.content && this.content.type) {
970
- case 'vctr':
971
- case 'geom':
972
- // @ts-ignore
973
- this.tileset._traverser.disableSkipLevelOfDetail = true;
974
- break;
975
- 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
+ }
976
1062
  }
977
1063
 
978
1064
  // The content may be tileset json
@@ -1003,23 +1089,24 @@ export class Tile3D {
1003
1089
  }
1004
1090
 
1005
1091
  const content = header.content;
1006
- if (!content) {
1007
- return;
1008
- }
1009
-
1010
- // TODO Cesium specific
1011
- // Non-leaf tiles may have a content bounding-volume, which is a tight-fit bounding volume
1012
- // around only the features in the tile. This box is useful for culling for rendering,
1013
- // but not for culling for traversing the tree since it does not guarantee spatial coherence, i.e.,
1014
- // since it only bounds features in the tile, not the entire tile, children may be
1015
- // outside of this box.
1016
- if (content.boundingVolume) {
1017
- this._contentBoundingVolume = createBoundingVolume(
1018
- content.boundingVolume,
1019
- this.computedTransform,
1020
- this._contentBoundingVolume
1021
- );
1092
+ if (content) {
1093
+ this._contentBoundingVolumeHeaders = Array.isArray(content) ? content : [content];
1022
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;
1023
1110
  }
1024
1111
 
1025
1112
  /**
@@ -22,6 +22,11 @@ import type {FoveatedInterpolationCallback} from '../helpers/tiles-3d-request-pr
22
22
  import type {GeospatialViewport, Viewport} from '../../types';
23
23
  import {Tile3D} from './tile-3d';
24
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';
25
30
 
26
31
  import {TilesetTraverser} from './tileset-traverser';
27
32
  import {
@@ -72,8 +77,16 @@ export type Tileset3DProps = {
72
77
 
73
78
  ellipsoid?: object;
74
79
  modelMatrix?: Matrix4;
80
+ /** Automatic CRS output options and expert source-metadata recovery overrides. */
81
+ spatial?: TilesetSpatialOptions;
75
82
 
76
83
  maximumScreenSpaceError?: number;
84
+ /**
85
+ * Enables replacement traversal that may skip hierarchy levels while retaining ready ancestors
86
+ * as coverage. This trades temporary overdraw for faster refinement on deep trees.
87
+ * @default false
88
+ */
89
+ skipLevelOfDetail?: boolean;
77
90
  /** Enables perspective dynamic SSE to reduce distant, horizon-facing refinement. */
78
91
  dynamicScreenSpaceError?: boolean;
79
92
  /** Base dynamic SSE fog density in inverse meters. */
@@ -139,6 +152,8 @@ type Props = {
139
152
  onTraversalComplete: (selectedTiles: Tile3D[]) => Tile3D[];
140
153
  onUpdate: () => void;
141
154
  maximumScreenSpaceError: number;
155
+ /** Whether replacement traversal may skip levels while retaining ancestor coverage. */
156
+ skipLevelOfDetail: boolean;
142
157
  /** Whether perspective dynamic SSE is enabled. */
143
158
  dynamicScreenSpaceError: boolean;
144
159
  /** Base dynamic SSE fog density in inverse meters. */
@@ -170,6 +185,8 @@ type Props = {
170
185
  basePath: string;
171
186
  contentLoader?: (tile: Tile3D) => Promise<void>;
172
187
  i3s: Record<string, any>;
188
+ /** Automatic CRS output options and expert source-metadata recovery overrides. */
189
+ spatial: TilesetSpatialOptions;
173
190
  };
174
191
 
175
192
  const BYTES_PER_MEBIBYTE = 1024 * 1024;
@@ -264,6 +281,7 @@ const DEFAULT_PROPS: Props = {
264
281
  foveatedInterpolationCallback: interpolateLinearly,
265
282
  foveatedTimeDelay: 0.2,
266
283
  maximumScreenSpaceError: 8,
284
+ skipLevelOfDetail: false,
267
285
  dynamicScreenSpaceError: true,
268
286
  dynamicScreenSpaceErrorDensity: 2.0e-4,
269
287
  dynamicScreenSpaceErrorFactor: 24,
@@ -275,7 +293,8 @@ const DEFAULT_PROPS: Props = {
275
293
  loadOptions: {fetch: {}},
276
294
  attributions: [],
277
295
  basePath: '',
278
- i3s: {}
296
+ i3s: {},
297
+ spatial: {}
279
298
  };
280
299
 
281
300
  const TILES_TOTAL = 'Tiles In Tileset(s)';
@@ -314,6 +333,18 @@ export class Tileset3D {
314
333
  root: Tile3D | null = null;
315
334
  roots: Record<string, Tile3D> = {};
316
335
  asset: Record<string, any> = {};
336
+ /** Inline metadata schema declared by the source tileset, when present. */
337
+ schema: Record<string, any> | null = null;
338
+ /** External metadata schema URI declared by the source tileset, when present. */
339
+ schemaUri: string | null = null;
340
+ /** Metadata groups declared by the source tileset, in source order. */
341
+ groups: Array<Record<string, any>> = [];
342
+ /** Tileset-wide metadata entity, preserved without property-table decoding. */
343
+ metadata: Record<string, any> | null = null;
344
+ /** Tileset statistics metadata, preserved for application-level inspection. */
345
+ statistics: unknown = null;
346
+ /** Normalized source and target coordinate reference system metadata. */
347
+ spatialReference: TilesetSpatialReference = createTilesetSpatialReference({});
317
348
 
318
349
  description = '';
319
350
  properties: any;
@@ -422,6 +453,13 @@ export class Tileset3D {
422
453
  this.memoryAdjustedScreenSpaceError = this.options.maximumScreenSpaceError;
423
454
  this._cacheBytes = cacheBytes;
424
455
  this._maximumCacheOverflowBytes = maximumCacheOverflowBytes;
456
+ const metadata = this._getSourceMetadata();
457
+ if (metadata) {
458
+ this.spatialReference = applyTilesetSpatialOptions(
459
+ metadata.spatialReference,
460
+ this.options.spatial
461
+ );
462
+ }
425
463
 
426
464
  this.stats = new Stats({id: this.url});
427
465
  this._initializeStats();
@@ -812,10 +850,13 @@ export class Tileset3D {
812
850
  for (const tile of this.selectedTiles) {
813
851
  if (tile.contentAvailable && tile.content) {
814
852
  tilesRenderable++;
815
- if (tile.content.pointCount) {
816
- pointsRenderable += tile.content.pointCount;
817
- } else {
818
- pointsRenderable += tile.content.vertexCount;
853
+ const contents = tile.contents.length ? tile.contents : [tile.content];
854
+ for (const content of contents) {
855
+ if (content.pointCount) {
856
+ pointsRenderable += content.pointCount;
857
+ } else {
858
+ pointsRenderable += content.vertexCount || 0;
859
+ }
819
860
  }
820
861
  }
821
862
  }
@@ -835,7 +876,8 @@ export class Tileset3D {
835
876
  }
836
877
  if (
837
878
  initialMetadata?.tileset?.root &&
838
- typeof initialMetadata.tileset.root.then !== 'function'
879
+ typeof initialMetadata.tileset.root.then !== 'function' &&
880
+ !this.source.prepareTileset
839
881
  ) {
840
882
  this.root = this._initializeTileHeaders(initialMetadata.tileset, null);
841
883
  this._applyViewState();
@@ -844,6 +886,7 @@ export class Tileset3D {
844
886
  await initializePromise;
845
887
  const metadata = this.source.getMetadata();
846
888
  this._syncSourceState(metadata);
889
+ await this.source.prepareTileset?.(this);
847
890
  if (!this.root) {
848
891
  this.root = this._initializeTileHeaders(metadata.tileset, null);
849
892
  }
@@ -1049,6 +1092,20 @@ export class Tileset3D {
1049
1092
  this.refine = metadata.refine;
1050
1093
  this.contentFormats = this.source.contentFormats;
1051
1094
  this.asset = this.source.asset || this.asset;
1095
+ const tileset = metadata.tileset as Record<string, any>;
1096
+ this.schema = tileset.schema || null;
1097
+ this.schemaUri = tileset.schemaUri || null;
1098
+ this.groups = tileset.groups || [];
1099
+ this.metadata = tileset.metadata || null;
1100
+ this.statistics = tileset.statistics ?? null;
1101
+ const spatialReference = applyTilesetSpatialOptions(
1102
+ metadata.spatialReference,
1103
+ this.options.spatial
1104
+ );
1105
+ this.spatialReference = preserveTransformedSpatialReference(
1106
+ this.spatialReference,
1107
+ spatialReference
1108
+ );
1052
1109
  this.properties = this.source.properties ?? this.properties;
1053
1110
  this.extras = this.source.extras ?? this.extras;
1054
1111
  if (this.options.attributions?.length) {
@@ -1085,3 +1142,17 @@ export class Tileset3D {
1085
1142
  }
1086
1143
  }
1087
1144
  }
1145
+
1146
+ /** Preserve a completed runtime operation when asynchronous source metadata describes the same request. */
1147
+ function preserveTransformedSpatialReference(
1148
+ current: TilesetSpatialReference,
1149
+ next: TilesetSpatialReference
1150
+ ): TilesetSpatialReference {
1151
+ return current?.status === 'transformed' &&
1152
+ current.sourceCrs === next.sourceCrs &&
1153
+ current.targetCrs === next.targetCrs &&
1154
+ current.targetHeightReference === next.targetHeightReference &&
1155
+ current.outputCoordinates === next.outputCoordinates
1156
+ ? current
1157
+ : next;
1158
+ }
@@ -9,6 +9,7 @@ import type {Tile3D} from './tile-3d';
9
9
  import type {Tileset3D} from './tileset-3d';
10
10
  import type {FrameState} from '../helpers/frame-state';
11
11
  import type {TILESET_TYPE} from '../../constants';
12
+ import type {TilesetSpatialReference} from '../../spatial/spatial-types';
12
13
 
13
14
  /**
14
15
  * Parsed top-level tileset payload consumed by {@link Tileset3D} or a {@link Tileset3DSource}.
@@ -69,8 +70,12 @@ export type TilesetSourceInput = TilesetJSON | TilesetSourceRequest;
69
70
  export type TileContentLoadResult = {
70
71
  /** Whether the tile content request produced usable content. */
71
72
  loaded: boolean;
73
+ /** All payloads loaded for a tile, in source order. */
74
+ contents?: any[];
72
75
  /** Nested tileset metadata to install below the loaded tile, if any. */
73
76
  nestedTileset?: TilesetJSON;
77
+ /** Nested tilesets found among multiple content payloads. */
78
+ nestedTilesets?: TilesetJSON[];
74
79
  };
75
80
 
76
81
  /** Result of a source-managed lazy child-header request. */
@@ -128,6 +133,8 @@ export type TilesetSourceMetadata = {
128
133
  lodMetricValue: number;
129
134
  /** Root refinement mode used as fallback by runtime tiles. */
130
135
  refine: string;
136
+ /** Format-discovered coordinate reference system metadata. */
137
+ spatialReference?: TilesetSpatialReference;
131
138
  };
132
139
 
133
140
  /**
@@ -167,6 +174,14 @@ export interface Tileset3DSource {
167
174
  */
168
175
  initialize(): Promise<void>;
169
176
 
177
+ /**
178
+ * Prepares option-dependent asynchronous source state before runtime headers are constructed.
179
+ *
180
+ * I3S uses this hook to sample elevation surfaces for its root bound after `Tileset3D` has
181
+ * applied spatial options. Sources without asynchronous header preparation omit the hook.
182
+ */
183
+ prepareTileset?(tileset: Tileset3D): Promise<void>;
184
+
170
185
  /** Releases source-local caches and prevents late asynchronous work from mutating runtime tiles. */
171
186
  destroy?(): void;
172
187
 
@@ -140,7 +140,9 @@ export class TilesetTraverser {
140
140
  // Always load tiles in the base traversal
141
141
  // Select tiles that can't refine further
142
142
  this.loadTile(tile, frameState);
143
- if (stoppedRefining) {
143
+ // Skip-LOD keeps a ready ancestor selected while descendants stream in. This prevents
144
+ // replacement holes when traversal jumps over one or more hierarchy levels.
145
+ if (stoppedRefining || this.options.skipLevelOfDetail) {
144
146
  this.selectTile(tile, frameState);
145
147
  }
146
148
  }
@@ -228,7 +230,7 @@ export class TilesetTraverser {
228
230
 
229
231
  // tile to render in the browser
230
232
  selectTile(tile: Tile3D, frameState: FrameState): void {
231
- if (this.shouldSelectTile(tile)) {
233
+ if (this.shouldSelectTile(tile, frameState)) {
232
234
  // The tile can be selected right away and does not require traverseAndSelect
233
235
  tile._selectedFrame = frameState.frameNumber;
234
236
  this.selectedTiles[tile.id] = tile;
@@ -292,10 +294,13 @@ export class TilesetTraverser {
292
294
  return tile.hasUnloadedContent || tile.contentExpired;
293
295
  }
294
296
 
295
- shouldSelectTile(tile: Tile3D): boolean {
296
- // if select tile is in current frame
297
- // and content available
298
- return tile.contentAvailable && !this.options.skipLevelOfDetail;
297
+ /**
298
+ * Returns whether a tile's content is available and visible for rendering.
299
+ * @param tile Tile considered for rendering.
300
+ * @param frameState Current culling state.
301
+ */
302
+ shouldSelectTile(tile: Tile3D, frameState: FrameState): boolean {
303
+ return tile.contentAvailable && tile.contentVisibility(frameState) !== 'outside';
299
304
  }
300
305
 
301
306
  /** Decide if tile LoD (level of detail) is not sufficient under current viewport */