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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +26 -0
  2. package/dist/dist.dev.js +10015 -8172
  3. package/dist/dist.min.js +3 -4
  4. package/dist/index.cjs +1676 -477
  5. package/dist/index.cjs.map +3 -3
  6. package/dist/index.d.ts +10 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +4 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/spatial/format-spatial-reference.d.ts +1 -0
  11. package/dist/spatial/format-spatial-reference.d.ts.map +1 -1
  12. package/dist/spatial/format-spatial-reference.js +104 -46
  13. package/dist/spatial/format-spatial-reference.js.map +1 -1
  14. package/dist/spatial/get-spatial-coordinate-frame.d.ts +12 -1
  15. package/dist/spatial/get-spatial-coordinate-frame.d.ts.map +1 -1
  16. package/dist/spatial/get-spatial-coordinate-frame.js +105 -24
  17. package/dist/spatial/get-spatial-coordinate-frame.js.map +1 -1
  18. package/dist/spatial/normalize-crs-identifier.d.ts +15 -0
  19. package/dist/spatial/normalize-crs-identifier.d.ts.map +1 -0
  20. package/dist/spatial/normalize-crs-identifier.js +22 -0
  21. package/dist/spatial/normalize-crs-identifier.js.map +1 -0
  22. package/dist/spatial/spatial-coordinate-transformer.d.ts +4 -0
  23. package/dist/spatial/spatial-coordinate-transformer.d.ts.map +1 -1
  24. package/dist/spatial/spatial-coordinate-transformer.js +102 -18
  25. package/dist/spatial/spatial-coordinate-transformer.js.map +1 -1
  26. package/dist/spatial/spatial-resource-registry.js +3 -3
  27. package/dist/spatial/spatial-resource-registry.js.map +1 -1
  28. package/dist/spatial/tiles-3d-spatial-transformer.d.ts +32 -0
  29. package/dist/spatial/tiles-3d-spatial-transformer.d.ts.map +1 -0
  30. package/dist/spatial/tiles-3d-spatial-transformer.js +175 -0
  31. package/dist/spatial/tiles-3d-spatial-transformer.js.map +1 -0
  32. package/dist/tileset-3d/common/tile-3d-contracts.d.ts +63 -0
  33. package/dist/tileset-3d/common/tile-3d-contracts.d.ts.map +1 -0
  34. package/dist/tileset-3d/common/tile-3d-contracts.js +51 -0
  35. package/dist/tileset-3d/common/tile-3d-contracts.js.map +1 -0
  36. package/dist/tileset-3d/common/tile-3d.d.ts +33 -5
  37. package/dist/tileset-3d/common/tile-3d.d.ts.map +1 -1
  38. package/dist/tileset-3d/common/tile-3d.js +179 -29
  39. package/dist/tileset-3d/common/tile-3d.js.map +1 -1
  40. package/dist/tileset-3d/common/tileset-3d.d.ts +15 -25
  41. package/dist/tileset-3d/common/tileset-3d.d.ts.map +1 -1
  42. package/dist/tileset-3d/common/tileset-3d.js +35 -42
  43. package/dist/tileset-3d/common/tileset-3d.js.map +1 -1
  44. package/dist/tileset-3d/common/tileset-content-traversal.d.ts +37 -0
  45. package/dist/tileset-3d/common/tileset-content-traversal.d.ts.map +1 -0
  46. package/dist/tileset-3d/common/tileset-content-traversal.js +80 -0
  47. package/dist/tileset-3d/common/tileset-content-traversal.js.map +1 -0
  48. package/dist/tileset-3d/common/tileset-observability.d.ts +60 -0
  49. package/dist/tileset-3d/common/tileset-observability.d.ts.map +1 -0
  50. package/dist/tileset-3d/common/tileset-observability.js +39 -0
  51. package/dist/tileset-3d/common/tileset-observability.js.map +1 -0
  52. package/dist/tileset-3d/common/tileset-source.d.ts +11 -1
  53. package/dist/tileset-3d/common/tileset-source.d.ts.map +1 -1
  54. package/dist/tileset-3d/common/tileset-source.js.map +1 -1
  55. package/dist/tileset-3d/common/tileset-traverser.d.ts.map +1 -1
  56. package/dist/tileset-3d/common/tileset-traverser.js +4 -3
  57. package/dist/tileset-3d/common/tileset-traverser.js.map +1 -1
  58. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts +47 -1
  59. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts.map +1 -1
  60. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js +327 -30
  61. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js.map +1 -1
  62. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts +34 -0
  63. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts.map +1 -1
  64. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js +278 -14
  65. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js.map +1 -1
  66. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts +5 -3
  67. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts.map +1 -1
  68. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js +7 -4
  69. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js.map +1 -1
  70. package/dist/tileset-3d/format-i3s/i3s-source.d.ts +4 -2
  71. package/dist/tileset-3d/format-i3s/i3s-source.d.ts.map +1 -1
  72. package/dist/tileset-3d/format-i3s/i3s-source.js +60 -9
  73. package/dist/tileset-3d/format-i3s/i3s-source.js.map +1 -1
  74. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.d.ts.map +1 -1
  75. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js +1 -1
  76. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js.map +1 -1
  77. package/dist/tileset-3d/helpers/bounding-volume.d.ts +7 -3
  78. package/dist/tileset-3d/helpers/bounding-volume.d.ts.map +1 -1
  79. package/dist/tileset-3d/helpers/bounding-volume.js +19 -61
  80. package/dist/tileset-3d/helpers/bounding-volume.js.map +1 -1
  81. package/dist/tileset-3d/helpers/frame-state.d.ts +5 -0
  82. package/dist/tileset-3d/helpers/frame-state.d.ts.map +1 -1
  83. package/dist/tileset-3d/helpers/frame-state.js +11 -5
  84. package/dist/tileset-3d/helpers/frame-state.js.map +1 -1
  85. package/dist/tileset-3d/helpers/s2-bounding-volume.d.ts +17 -0
  86. package/dist/tileset-3d/helpers/s2-bounding-volume.d.ts.map +1 -0
  87. package/dist/tileset-3d/helpers/s2-bounding-volume.js +20 -0
  88. package/dist/tileset-3d/helpers/s2-bounding-volume.js.map +1 -0
  89. package/dist/types.d.ts +2 -0
  90. package/dist/types.d.ts.map +1 -1
  91. package/package.json +11 -12
  92. package/src/index.ts +18 -1
  93. package/src/spatial/format-spatial-reference.ts +127 -53
  94. package/src/spatial/get-spatial-coordinate-frame.ts +126 -31
  95. package/src/spatial/normalize-crs-identifier.ts +26 -0
  96. package/src/spatial/spatial-coordinate-transformer.ts +124 -24
  97. package/src/spatial/spatial-resource-registry.ts +3 -3
  98. package/src/spatial/tiles-3d-spatial-transformer.ts +206 -0
  99. package/src/tileset-3d/common/tile-3d-contracts.ts +115 -0
  100. package/src/tileset-3d/common/tile-3d.ts +208 -32
  101. package/src/tileset-3d/common/tileset-3d.ts +37 -61
  102. package/src/tileset-3d/common/tileset-content-traversal.ts +114 -0
  103. package/src/tileset-3d/common/tileset-observability.ts +100 -0
  104. package/src/tileset-3d/common/tileset-source.ts +19 -1
  105. package/src/tileset-3d/common/tileset-traverser.ts +4 -3
  106. package/src/tileset-3d/format-3d-tiles/implicit-tiling.ts +481 -34
  107. package/src/tileset-3d/format-3d-tiles/tiles-3d-source.ts +405 -14
  108. package/src/tileset-3d/format-3d-tiles/tileset-3d-traverser.ts +7 -4
  109. package/src/tileset-3d/format-i3s/i3s-source.ts +84 -13
  110. package/src/tileset-3d/format-i3s/i3s-tileset-traverser.ts +2 -1
  111. package/src/tileset-3d/helpers/bounding-volume.ts +29 -62
  112. package/src/tileset-3d/helpers/frame-state.ts +15 -7
  113. package/src/tileset-3d/helpers/s2-bounding-volume.ts +35 -0
  114. package/src/types.ts +2 -0
@@ -8,6 +8,8 @@
8
8
  import {Vector3, Matrix4} from '@math.gl/core';
9
9
  import {CullingVolume} from '@math.gl/culling';
10
10
  import type {CullingResult} from '@math.gl/culling';
11
+ import {getTile3DFeatureIdSets} from './tile-3d-contracts';
12
+ import type {Tile3DContent, Tile3DFeatureIdSet, Tile3DMetadataContext} from './tile-3d-contracts';
11
13
 
12
14
  // Note: circular dependency
13
15
  import type {Tileset3D} from './tileset-3d';
@@ -85,6 +87,14 @@ export function getContentVisibility(
85
87
  return intersectingVolume ? 'intersecting' : 'inside';
86
88
  }
87
89
 
90
+ function isNestedTilesetPayload(payload: unknown): boolean {
91
+ if (!payload || typeof payload !== 'object') {
92
+ return false;
93
+ }
94
+ const record = payload as Record<string, any>;
95
+ return record.shape === 'tileset3d' || record.type === 'tileset3d' || Boolean(record.tileset);
96
+ }
97
+
88
98
  function defined(x) {
89
99
  return x !== undefined && x !== null;
90
100
  }
@@ -137,6 +147,9 @@ export class Tile3D {
137
147
  */
138
148
  private _unscaledLodMetricValue: number = 0;
139
149
 
150
+ /** Whether the source format defines geometric error in transform-local units. */
151
+ private _scaleGeometricError: boolean = true;
152
+
140
153
  /** @todo math.gl is not exporting BoundingVolume base type? */
141
154
  boundingVolume: any = null;
142
155
 
@@ -147,6 +160,14 @@ export class Tile3D {
147
160
  content: any = null;
148
161
  /** Loaded payloads in the same order as {@link contentUrls}; `content` remains the primary payload. */
149
162
  contents: any[] = [];
163
+ /** Ordered renderer-neutral descriptors for each source content entry. */
164
+ contentEntries: Tile3DContent[] = [];
165
+ /** Raw metadata references inherited by this tile and its content. */
166
+ metadataContext: Tile3DMetadataContext = {};
167
+ /** Feature-id declarations normalized from supported content extensions. */
168
+ featureIdSets: Tile3DFeatureIdSet[] = [];
169
+ /** Decoded 3D Tiles vector topology for the primary loaded content, when present. */
170
+ vectorContent: unknown = null;
150
171
  /**
151
172
  * Metadata entity attached to this tile in the source tileset, if present.
152
173
  *
@@ -228,6 +249,7 @@ export class Tile3D {
228
249
  return this._viewerRequestVolume;
229
250
  }
230
251
 
252
+ /** Accumulated tile transforms from the source JSON, excluding the tileset model matrix. */
231
253
  _initialTransform: Matrix4 = new Matrix4();
232
254
 
233
255
  // Used by traverser, cannot be marked private
@@ -283,11 +305,7 @@ export class Tile3D {
283
305
  this.type = header.type;
284
306
  this.contentUrl = header.contentUrl;
285
307
  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);
308
+ this._initializeMetadata(header);
291
309
  this.childrenState = header.implicitSubtree ? 'unloaded' : 'ready';
292
310
 
293
311
  this._initializeLodMetric(header);
@@ -295,6 +313,7 @@ export class Tile3D {
295
313
  this._updateLodMetricScale();
296
314
  this._initializeBoundingVolumes(header);
297
315
  this._initializeContent(header);
316
+ this._initializeContentEntries(header);
298
317
  this._initializeRenderingState(header);
299
318
 
300
319
  Object.seal(this);
@@ -407,12 +426,16 @@ export class Tile3D {
407
426
  }
408
427
 
409
428
  /**
410
- * Get bounding box in cartographic coordinates
429
+ * Get the current world-space bounding volume in cartographic coordinates.
411
430
  * @returns [min, max] each in [longitude, latitude, altitude]
412
431
  */
413
432
  get boundingBox(): CartographicBounds {
414
433
  if (!this._boundingBox) {
415
- this._boundingBox = getCartographicBounds(this.header.boundingVolume, this.boundingVolume);
434
+ this._boundingBox = getCartographicBounds(
435
+ this.header.boundingVolume,
436
+ this.boundingVolume,
437
+ !this.computedTransform.equals(this._initialTransform)
438
+ );
416
439
  }
417
440
  return this._boundingBox;
418
441
  }
@@ -579,20 +602,7 @@ export class Tile3D {
579
602
  }
580
603
 
581
604
  try {
582
- const loadResult = await this.tileset.source.loadTileContent(this);
583
-
584
- if (loadResult.contents) {
585
- this.contents = loadResult.contents;
586
- this.content = this.contents[0] || null;
587
- }
588
-
589
- if (this.tileset.options.contentLoader) {
590
- await this.tileset.options.contentLoader(this);
591
- }
592
-
593
- this.contentState = TILE_CONTENT_STATE.READY;
594
- this._onContentLoaded();
595
- return loadResult;
605
+ return await this._loadContentFromSource();
596
606
  } catch (error) {
597
607
  // Tile is unloaded before the content finishes loading
598
608
  this.contentState = TILE_CONTENT_STATE.FAILED;
@@ -602,6 +612,44 @@ export class Tile3D {
602
612
  }
603
613
  }
604
614
 
615
+ /** Loads content for complete dataset operations without viewport-based request priority. */
616
+ async loadContentForTraversal(): Promise<TileContentLoadResult> {
617
+ if (this.hasEmptyContent) {
618
+ return {loaded: false};
619
+ }
620
+ if (this.content) {
621
+ return {loaded: true, contents: this.contents};
622
+ }
623
+
624
+ this.contentState = TILE_CONTENT_STATE.LOADING;
625
+ try {
626
+ return await this._loadContentFromSource();
627
+ } catch (error) {
628
+ this.contentState = TILE_CONTENT_STATE.FAILED;
629
+ throw error;
630
+ }
631
+ }
632
+
633
+ /** Loads source content and updates the tile's renderer-neutral payload state. */
634
+ private async _loadContentFromSource(): Promise<TileContentLoadResult> {
635
+ const loadResult = await this.tileset.source.loadTileContent(this);
636
+ if (loadResult.contents) {
637
+ this.contents = loadResult.contents;
638
+ this.content = this.contents[0] || null;
639
+ }
640
+ this.vectorContent =
641
+ this.contents.find(content => Boolean(content?.vectorContent))?.vectorContent || null;
642
+
643
+ if (this.tileset.options.contentLoader) {
644
+ await this.tileset.options.contentLoader(this);
645
+ }
646
+
647
+ this.contentState = TILE_CONTENT_STATE.READY;
648
+ this._onContentLoaded();
649
+ this._updateContentEntriesAfterLoad(loadResult);
650
+ return loadResult;
651
+ }
652
+
605
653
  /**
606
654
  * Requests and installs this tile's lazy child-header group through its source.
607
655
  *
@@ -665,34 +713,137 @@ export class Tile3D {
665
713
  /**
666
714
  * Replaces a contentless implicit placeholder with its materialized subtree-root header.
667
715
  *
668
- * Transform state is intentionally preserved: the placeholder already owns the composed
669
- * transform, and reapplying the root transform here would double-transform bounds and geometric
670
- * error. Content and raw geometric error are reinitialized from the newly available header.
716
+ * The placeholder transform is preserved when the subtree omits a root transform. An explicit
717
+ * subtree-root transform replaces it and is recomposed against the runtime parent. Content,
718
+ * metadata, bounds, and raw geometric error are reinitialized from the available header.
671
719
  *
672
720
  * @param materializedHeader - Available root header produced from one subtree resource.
673
721
  */
674
722
  applyImplicitSubtreeHeader(materializedHeader: Record<string, any>): void {
675
723
  const existingTransform = this.header.transform;
676
724
  const existingTransformMatrix = this.header.transformMatrix;
725
+ const hasMaterializedTransform = materializedHeader.transform !== undefined;
677
726
  this.header = {
678
727
  ...this.header,
679
728
  ...materializedHeader,
680
- transform: existingTransform,
681
- transformMatrix: existingTransformMatrix,
729
+ transform: hasMaterializedTransform ? materializedHeader.transform : existingTransform,
730
+ transformMatrix: hasMaterializedTransform
731
+ ? materializedHeader.transformMatrix
732
+ : existingTransformMatrix,
682
733
  implicitSubtree: undefined
683
734
  };
735
+ if (hasMaterializedTransform) {
736
+ this._initializeTransforms(this.header);
737
+ }
684
738
  this.refine = this._getRefine(materializedHeader.refine);
685
739
  this.type = materializedHeader.type;
686
740
  this.contentUrl = materializedHeader.contentUrl;
687
741
  this.contentUrls =
688
742
  materializedHeader.contentUrls ||
689
743
  (materializedHeader.contentUrl ? [materializedHeader.contentUrl] : []);
744
+ this._initializeMetadata(this.header);
690
745
  this._initializeLodMetric(materializedHeader);
691
746
  this._updateLodMetricScale();
692
747
  this._initializeBoundingVolumes(this.header);
748
+ this._boundingBox = undefined;
693
749
  // Use the merged header so inherited viewer-request-volume metadata remains available when a
694
750
  // materialized implicit tile has no render content of its own.
695
751
  this._initializeContent(this.header);
752
+ this._initializeContentEntries(this.header);
753
+ }
754
+
755
+ /** Refreshes tile and content metadata from a source or materialized tile header. */
756
+ private _initializeMetadata(header: Record<string, any>): void {
757
+ this.metadata = header.metadata || null;
758
+ const contentHeaders = Array.isArray(header.content) ? header.content : [header.content];
759
+ this.contentMetadata = contentHeaders
760
+ .filter(Boolean)
761
+ .map(contentHeader => contentHeader.metadata || null);
762
+ const firstContentHeader = contentHeaders.find(Boolean) || {};
763
+ const groupReference = firstContentHeader.group;
764
+ const group =
765
+ typeof groupReference === 'number'
766
+ ? this.tileset.groups?.[groupReference] || null
767
+ : groupReference || null;
768
+ const metadataContext: Tile3DMetadataContext = {
769
+ tileset: this.tileset.metadata || null,
770
+ group,
771
+ tile: this.metadata,
772
+ content: this.contentMetadata[0] || null,
773
+ subtree: header.implicitMetadata || null
774
+ };
775
+ if (this.tileset.groups) {
776
+ metadataContext.groups = this.tileset.groups;
777
+ }
778
+ this.metadataContext = metadataContext;
779
+ }
780
+
781
+ /** Updates payload and renderability while retaining immutable metadata descriptors. */
782
+ private _updateContentEntriesAfterLoad(loadResult: TileContentLoadResult): void {
783
+ if (loadResult.contentEntries) {
784
+ this.contentEntries = loadResult.contentEntries.map((entry, index) => ({
785
+ ...entry,
786
+ index,
787
+ boundingVolume:
788
+ entry.boundingVolume || this._contentBoundingVolumes[index] || this.boundingVolume,
789
+ payload: entry.payload ?? this.contents[index] ?? null,
790
+ featureIds: entry.featureIds?.length
791
+ ? entry.featureIds
792
+ : getTile3DFeatureIdSets(entry.payload ?? this.contents[index]),
793
+ renderable: Boolean(
794
+ entry.renderable !== false &&
795
+ (entry.payload ?? this.contents[index]) &&
796
+ !isNestedTilesetPayload(entry.payload ?? this.contents[index])
797
+ )
798
+ }));
799
+ this.featureIdSets = this.contentEntries.flatMap(entry => entry.featureIds);
800
+ return;
801
+ }
802
+ this.contentEntries = this.contentEntries.map((entry, index) => ({
803
+ ...entry,
804
+ payload: this.contents[index] ?? null,
805
+ featureIds: getTile3DFeatureIdSets(this.contents[index]),
806
+ renderable: Boolean(this.contents[index]) && !isNestedTilesetPayload(this.contents[index])
807
+ }));
808
+ this.featureIdSets = this.contentEntries.flatMap(entry => entry.featureIds);
809
+ }
810
+
811
+ /** Returns one ordered content descriptor, or null when the index is out of range. */
812
+ getContentEntry(index: number): Tile3DContent | null {
813
+ return this.contentEntries[index] || null;
814
+ }
815
+
816
+ /** Returns whether an ordered content entry currently has renderable payload. */
817
+ isContentRenderable(index: number): boolean {
818
+ return Boolean(this.contentEntries[index]?.renderable);
819
+ }
820
+
821
+ /** Initializes additive renderer-neutral descriptors from a normalized tile header. */
822
+ private _initializeContentEntries(tileHeader: Record<string, any>): void {
823
+ const headers = Array.isArray(tileHeader.content)
824
+ ? tileHeader.content
825
+ : tileHeader.content
826
+ ? [tileHeader.content]
827
+ : [];
828
+ this.contentEntries = headers.map((contentHeader, index) => {
829
+ const normalizedContentHeader = contentHeader || {};
830
+ return {
831
+ index,
832
+ uri: this.contentUrls[index] || normalizedContentHeader.uri || normalizedContentHeader.url,
833
+ group: normalizedContentHeader.group,
834
+ type: normalizedContentHeader.type,
835
+ payload: null,
836
+ metadata: normalizedContentHeader.metadata || null,
837
+ boundingVolume: this._contentBoundingVolumes[index] || this.boundingVolume,
838
+ featureIds: this._getFeatureIdSets(normalizedContentHeader),
839
+ renderable: false
840
+ };
841
+ });
842
+ }
843
+
844
+ /** Extracts renderer-neutral feature-id declarations without decoding property values. */
845
+ private _getFeatureIdSets(contentHeader: Record<string, any>): Tile3DFeatureIdSet[] {
846
+ return getTile3DFeatureIdSets(contentHeader);
696
847
  }
697
848
 
698
849
  // Unloads the tile's content.
@@ -704,6 +855,13 @@ export class Tile3D {
704
855
  }
705
856
  this.contents = [];
706
857
  this.content = null;
858
+ this.vectorContent = null;
859
+ this.contentEntries = this.contentEntries.map(entry => ({
860
+ ...entry,
861
+ payload: null,
862
+ renderable: false
863
+ }));
864
+ this.featureIdSets = this.contentEntries.flatMap(entry => entry.featureIds);
707
865
  if (this.header.content && this.header.content.destroy) {
708
866
  this.header.content.destroy();
709
867
  }
@@ -848,9 +1006,15 @@ export class Tile3D {
848
1006
  * @param frameState Current camera, culling, and optional clipping-plane state.
849
1007
  * @returns The content visibility classification.
850
1008
  */
851
- contentVisibility(frameState: FrameState): CullingResult {
1009
+ contentVisibility(frameState: FrameState, contentIndex?: number): CullingResult {
1010
+ const contentVolumes =
1011
+ contentIndex === undefined
1012
+ ? this._contentBoundingVolumes
1013
+ : this._contentBoundingVolumes[contentIndex]
1014
+ ? [this._contentBoundingVolumes[contentIndex]]
1015
+ : [];
852
1016
  return getContentVisibility(
853
- this._contentBoundingVolumes,
1017
+ contentVolumes,
854
1018
  this.boundingVolume,
855
1019
  frameState.cullingVolume,
856
1020
  this._visibilityPlaneMask,
@@ -913,6 +1077,10 @@ export class Tile3D {
913
1077
 
914
1078
  /** Initializes the tile's LOD metric from its header or its nearest available ancestor. */
915
1079
  _initializeLodMetric(header: {[key: string]: any}): void {
1080
+ this._scaleGeometricError =
1081
+ typeof header._scaleGeometricError === 'boolean'
1082
+ ? header._scaleGeometricError
1083
+ : (this.parent?._scaleGeometricError ?? true);
916
1084
  if ('lodMetricType' in header) {
917
1085
  this.lodMetricType = header.lodMetricType;
918
1086
  } else {
@@ -969,7 +1137,7 @@ export class Tile3D {
969
1137
  * I3S `maxScreenThreshold` is already a screen-space metric and must not be transform-scaled.
970
1138
  */
971
1139
  _updateLodMetricScale(): void {
972
- if (this.lodMetricType !== LOD_METRIC_TYPE.GEOMETRIC_ERROR) {
1140
+ if (this.lodMetricType !== LOD_METRIC_TYPE.GEOMETRIC_ERROR || !this._scaleGeometricError) {
973
1141
  this.lodMetricValue = this._unscaledLodMetricValue;
974
1142
  return;
975
1143
  }
@@ -1074,11 +1242,13 @@ export class Tile3D {
1074
1242
  }
1075
1243
 
1076
1244
  _updateBoundingVolume(header) {
1245
+ this._boundingBox = undefined;
1077
1246
  // Update the bounding volumes
1078
1247
  this.boundingVolume = createBoundingVolume(
1079
1248
  header.boundingVolume,
1080
1249
  this.computedTransform,
1081
- this.boundingVolume
1250
+ this.boundingVolume,
1251
+ this._initialTransform
1082
1252
  );
1083
1253
 
1084
1254
  // Viewer request volumes constrain traversal, not just render content. A contentless implicit
@@ -1088,7 +1258,8 @@ export class Tile3D {
1088
1258
  this._viewerRequestVolume = createBoundingVolume(
1089
1259
  header.viewerRequestVolume,
1090
1260
  this.computedTransform,
1091
- this._viewerRequestVolume
1261
+ this._viewerRequestVolume,
1262
+ this._initialTransform
1092
1263
  );
1093
1264
  }
1094
1265
 
@@ -1106,7 +1277,12 @@ export class Tile3D {
1106
1277
  this._contentBoundingVolumes = contentHeaders.length
1107
1278
  ? contentHeaders.map(headerEntry =>
1108
1279
  headerEntry?.boundingVolume
1109
- ? createBoundingVolume(headerEntry.boundingVolume, this.computedTransform)
1280
+ ? createBoundingVolume(
1281
+ headerEntry.boundingVolume,
1282
+ this.computedTransform,
1283
+ undefined,
1284
+ this._initialTransform
1285
+ )
1110
1286
  : this.boundingVolume
1111
1287
  )
1112
1288
  : [this.boundingVolume];
@@ -58,17 +58,6 @@ export type Tileset3DProps = {
58
58
  * @default 536870912
59
59
  */
60
60
  maximumCacheOverflowBytes?: number;
61
- /**
62
- * Soft cache target in mebibytes.
63
- * @deprecated Use {@link Tileset3DProps.cacheBytes}; when both are supplied, `cacheBytes` wins.
64
- */
65
- maximumMemoryUsage?: number;
66
- /**
67
- * Cache overflow headroom in mebibytes.
68
- * @deprecated Use {@link Tileset3DProps.maximumCacheOverflowBytes}; when both are supplied, the
69
- * byte-native option wins.
70
- */
71
- memoryCacheOverflow?: number;
72
61
  maximumTilesSelected?: number;
73
62
  debounceTime?: number;
74
63
 
@@ -158,10 +147,6 @@ type Props = {
158
147
  cacheBytes: number;
159
148
  /** Current-frame headroom, in bytes, before memory-adjusted SSE rises. */
160
149
  maximumCacheOverflowBytes: number;
161
- /** @deprecated Byte-native code should use `cacheBytes`. */
162
- maximumMemoryUsage: number;
163
- /** @deprecated Byte-native code should use `maximumCacheOverflowBytes`. */
164
- memoryCacheOverflow: number;
165
150
  maximumTilesSelected: number;
166
151
  debounceTime: number;
167
152
  onTileLoad: (tile: Tile3D) => void;
@@ -241,31 +226,21 @@ function validateCacheByteLength(value: number, optionName: string): number {
241
226
  }
242
227
 
243
228
  /**
244
- * Resolves the base cache target from byte-native or deprecated mebibyte options.
229
+ * Resolves the base cache target from the byte-native option or its fallback.
245
230
  *
246
- * The byte-native option takes precedence so mixed migration configurations are deterministic.
247
- * The fallback is already expressed in bytes and is used for both construction and runtime updates.
248
- *
249
- * @param options - Options that may contain byte-native or deprecated cache values.
231
+ * @param options - Options that may contain a byte-native cache value.
250
232
  * @param fallbackCacheBytes - Existing or default cache target in bytes.
251
233
  * @returns The resolved and validated cache target in bytes.
252
234
  */
253
235
  function resolveCacheBytes(options: Tileset3DProps, fallbackCacheBytes: number): number {
254
- const cacheBytes =
255
- options.cacheBytes ??
256
- (options.maximumMemoryUsage !== undefined
257
- ? options.maximumMemoryUsage * BYTES_PER_MEBIBYTE
258
- : fallbackCacheBytes);
236
+ const cacheBytes = options.cacheBytes ?? fallbackCacheBytes;
259
237
  return validateCacheByteLength(cacheBytes, 'cacheBytes');
260
238
  }
261
239
 
262
240
  /**
263
- * Resolves cache overflow headroom from byte-native or deprecated mebibyte options.
264
- *
265
- * The byte-native option takes precedence so callers can migrate the two cache controls
266
- * independently without unit ambiguity.
241
+ * Resolves cache overflow headroom from the byte-native option or its fallback.
267
242
  *
268
- * @param options - Options that may contain byte-native or deprecated overflow values.
243
+ * @param options - Options that may contain a byte-native overflow value.
269
244
  * @param fallbackOverflowBytes - Existing or default overflow headroom in bytes.
270
245
  * @returns The resolved and validated overflow headroom in bytes.
271
246
  */
@@ -273,11 +248,7 @@ function resolveMaximumCacheOverflowBytes(
273
248
  options: Tileset3DProps,
274
249
  fallbackOverflowBytes: number
275
250
  ): number {
276
- const maximumCacheOverflowBytes =
277
- options.maximumCacheOverflowBytes ??
278
- (options.memoryCacheOverflow !== undefined
279
- ? options.memoryCacheOverflow * BYTES_PER_MEBIBYTE
280
- : fallbackOverflowBytes);
251
+ const maximumCacheOverflowBytes = options.maximumCacheOverflowBytes ?? fallbackOverflowBytes;
281
252
  return validateCacheByteLength(maximumCacheOverflowBytes, 'maximumCacheOverflowBytes');
282
253
  }
283
254
 
@@ -289,8 +260,6 @@ const DEFAULT_PROPS: Props = {
289
260
  maxRequests: 64,
290
261
  cacheBytes: DEFAULT_CACHE_BYTES,
291
262
  maximumCacheOverflowBytes: DEFAULT_MAXIMUM_CACHE_OVERFLOW_BYTES,
292
- maximumMemoryUsage: DEFAULT_CACHE_BYTES / BYTES_PER_MEBIBYTE,
293
- memoryCacheOverflow: DEFAULT_MAXIMUM_CACHE_OVERFLOW_BYTES / BYTES_PER_MEBIBYTE,
294
263
  maximumTilesSelected: 0,
295
264
  debounceTime: 0,
296
265
  onTileLoad: () => {},
@@ -456,8 +425,6 @@ export class Tileset3D {
456
425
  ...suppliedOptions,
457
426
  cacheBytes,
458
427
  maximumCacheOverflowBytes,
459
- maximumMemoryUsage: cacheBytes / BYTES_PER_MEBIBYTE,
460
- memoryCacheOverflow: maximumCacheOverflowBytes / BYTES_PER_MEBIBYTE,
461
428
  memoryAdjustedScreenSpaceError:
462
429
  suppliedOptions.memoryAdjustedScreenSpaceError ?? usesTiles3DCacheDefaults
463
430
  };
@@ -517,6 +484,26 @@ export class Tileset3D {
517
484
  return this._frameNumber;
518
485
  }
519
486
 
487
+ /**
488
+ * Returns the tiles requested during the most recent completed traversal.
489
+ *
490
+ * A defensive copy makes request ordering deterministic for diagnostics and prevents callers
491
+ * from mutating the scheduler's working set.
492
+ */
493
+ get requestedTiles(): readonly Tile3D[] {
494
+ return this._requestedTiles.slice();
495
+ }
496
+
497
+ /**
498
+ * Returns empty tiles considered during the most recent completed traversal.
499
+ *
500
+ * Empty tiles can still be useful hierarchy placeholders, but their IDs are included in
501
+ * observability snapshots so applications can distinguish traversal work from render content.
502
+ */
503
+ get emptyTiles(): readonly Tile3D[] {
504
+ return this._emptyTiles.slice();
505
+ }
506
+
520
507
  /**
521
508
  * Gets or sets the soft target, in bytes, for cached tile content not needed this frame.
522
509
  *
@@ -532,7 +519,6 @@ export class Tileset3D {
532
519
  set cacheBytes(value: number) {
533
520
  this._cacheBytes = validateCacheByteLength(value, 'cacheBytes');
534
521
  this.options.cacheBytes = this._cacheBytes;
535
- this.options.maximumMemoryUsage = this._cacheBytes / BYTES_PER_MEBIBYTE;
536
522
  }
537
523
 
538
524
  /**
@@ -549,19 +535,6 @@ export class Tileset3D {
549
535
  set maximumCacheOverflowBytes(value: number) {
550
536
  this._maximumCacheOverflowBytes = validateCacheByteLength(value, 'maximumCacheOverflowBytes');
551
537
  this.options.maximumCacheOverflowBytes = this._maximumCacheOverflowBytes;
552
- this.options.memoryCacheOverflow = this._maximumCacheOverflowBytes / BYTES_PER_MEBIBYTE;
553
- }
554
-
555
- /**
556
- * Gets or sets the soft cache target in mebibytes.
557
- * @deprecated Use {@link Tileset3D.cacheBytes}. Assignments remain synchronized for compatibility.
558
- */
559
- get maximumMemoryUsage(): number {
560
- return this.cacheBytes / BYTES_PER_MEBIBYTE;
561
- }
562
-
563
- set maximumMemoryUsage(value: number) {
564
- this.cacheBytes = validateCacheByteLength(value * BYTES_PER_MEBIBYTE, 'maximumMemoryUsage');
565
538
  }
566
539
 
567
540
  get queryParams(): string {
@@ -571,10 +544,7 @@ export class Tileset3D {
571
544
  }
572
545
 
573
546
  /**
574
- * Updates traversal and runtime options, synchronizing byte-native and deprecated cache values.
575
- *
576
- * For each budget, the byte-native option takes precedence when both unit forms are supplied.
577
- * Unspecified budgets retain their current byte values, avoiding repeated MiB conversion.
547
+ * Updates traversal and runtime options while retaining unspecified byte budgets.
578
548
  *
579
549
  * @param props - Partial runtime options to apply.
580
550
  */
@@ -588,9 +558,7 @@ export class Tileset3D {
588
558
  ...this.options,
589
559
  ...props,
590
560
  cacheBytes,
591
- maximumCacheOverflowBytes,
592
- maximumMemoryUsage: cacheBytes / BYTES_PER_MEBIBYTE,
593
- memoryCacheOverflow: maximumCacheOverflowBytes / BYTES_PER_MEBIBYTE
561
+ maximumCacheOverflowBytes
594
562
  };
595
563
  this._cacheBytes = cacheBytes;
596
564
  this._maximumCacheOverflowBytes = maximumCacheOverflowBytes;
@@ -662,6 +630,14 @@ export class Tileset3D {
662
630
  return;
663
631
  }
664
632
  const preparedViewports = viewports instanceof Array ? viewports : [viewports];
633
+ const activeViewportIds = new Set(preparedViewports.map(viewport => viewport.id));
634
+ // A viewport may be removed between frames. Discard its completed state before aggregating
635
+ // requested and selected tiles so observability reflects only the current traversal inputs.
636
+ for (const frameStateId of Object.keys(this.frameStateData)) {
637
+ if (!activeViewportIds.has(frameStateId)) {
638
+ delete this.frameStateData[frameStateId];
639
+ }
640
+ }
665
641
 
666
642
  this._cache.reset();
667
643
  this._frameNumber++;
@@ -909,7 +885,7 @@ export class Tileset3D {
909
885
  if (
910
886
  initialMetadata?.tileset?.root &&
911
887
  typeof initialMetadata.tileset.root.then !== 'function' &&
912
- !this.source.prepareTileset
888
+ (!this.source.prepareTileset || this.spatialReference?.status === 'native')
913
889
  ) {
914
890
  this.root = this._initializeTileHeaders(initialMetadata.tileset, null);
915
891
  this._applyViewState();