@loaders.gl/tiles 5.0.0-alpha.5 → 5.0.0-alpha.7

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 (106) hide show
  1. package/dist/dist.dev.js +5122 -2641
  2. package/dist/dist.min.js +4 -2
  3. package/dist/index.cjs +1670 -403
  4. package/dist/index.cjs.map +3 -3
  5. package/dist/index.d.ts +8 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/spatial/format-spatial-reference.d.ts +1 -0
  10. package/dist/spatial/format-spatial-reference.d.ts.map +1 -1
  11. package/dist/spatial/format-spatial-reference.js +104 -46
  12. package/dist/spatial/format-spatial-reference.js.map +1 -1
  13. package/dist/spatial/get-spatial-coordinate-frame.d.ts +12 -1
  14. package/dist/spatial/get-spatial-coordinate-frame.d.ts.map +1 -1
  15. package/dist/spatial/get-spatial-coordinate-frame.js +105 -24
  16. package/dist/spatial/get-spatial-coordinate-frame.js.map +1 -1
  17. package/dist/spatial/normalize-crs-identifier.d.ts +15 -0
  18. package/dist/spatial/normalize-crs-identifier.d.ts.map +1 -0
  19. package/dist/spatial/normalize-crs-identifier.js +22 -0
  20. package/dist/spatial/normalize-crs-identifier.js.map +1 -0
  21. package/dist/spatial/spatial-coordinate-transformer.d.ts +4 -0
  22. package/dist/spatial/spatial-coordinate-transformer.d.ts.map +1 -1
  23. package/dist/spatial/spatial-coordinate-transformer.js +89 -12
  24. package/dist/spatial/spatial-coordinate-transformer.js.map +1 -1
  25. package/dist/spatial/tiles-3d-spatial-transformer.d.ts +32 -0
  26. package/dist/spatial/tiles-3d-spatial-transformer.d.ts.map +1 -0
  27. package/dist/spatial/tiles-3d-spatial-transformer.js +175 -0
  28. package/dist/spatial/tiles-3d-spatial-transformer.js.map +1 -0
  29. package/dist/tileset-3d/common/indexed-archive-tileset-source.d.ts +3 -3
  30. package/dist/tileset-3d/common/indexed-archive-tileset-source.d.ts.map +1 -1
  31. package/dist/tileset-3d/common/indexed-archive-tileset-source.js.map +1 -1
  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 +29 -5
  37. package/dist/tileset-3d/common/tile-3d.d.ts.map +1 -1
  38. package/dist/tileset-3d/common/tile-3d.js +151 -20
  39. package/dist/tileset-3d/common/tile-3d.js.map +1 -1
  40. package/dist/tileset-3d/common/tileset-3d.d.ts +44 -2
  41. package/dist/tileset-3d/common/tileset-3d.d.ts.map +1 -1
  42. package/dist/tileset-3d/common/tileset-3d.js +31 -1
  43. package/dist/tileset-3d/common/tileset-3d.js.map +1 -1
  44. package/dist/tileset-3d/common/tileset-observability.d.ts +60 -0
  45. package/dist/tileset-3d/common/tileset-observability.d.ts.map +1 -0
  46. package/dist/tileset-3d/common/tileset-observability.js +39 -0
  47. package/dist/tileset-3d/common/tileset-observability.js.map +1 -0
  48. package/dist/tileset-3d/common/tileset-source.d.ts +11 -8
  49. package/dist/tileset-3d/common/tileset-source.d.ts.map +1 -1
  50. package/dist/tileset-3d/common/tileset-source.js.map +1 -1
  51. package/dist/tileset-3d/common/tileset-traverser.d.ts +14 -0
  52. package/dist/tileset-3d/common/tileset-traverser.d.ts.map +1 -1
  53. package/dist/tileset-3d/common/tileset-traverser.js +21 -5
  54. package/dist/tileset-3d/common/tileset-traverser.js.map +1 -1
  55. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts +47 -1
  56. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts.map +1 -1
  57. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js +327 -30
  58. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js.map +1 -1
  59. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts +34 -2
  60. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts.map +1 -1
  61. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js +265 -14
  62. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js.map +1 -1
  63. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts +70 -3
  64. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts.map +1 -1
  65. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js +216 -4
  66. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js.map +1 -1
  67. package/dist/tileset-3d/format-i3s/i3s-source.d.ts +5 -3
  68. package/dist/tileset-3d/format-i3s/i3s-source.d.ts.map +1 -1
  69. package/dist/tileset-3d/format-i3s/i3s-source.js +14 -7
  70. package/dist/tileset-3d/format-i3s/i3s-source.js.map +1 -1
  71. package/dist/tileset-3d/helpers/bounding-volume.d.ts +7 -3
  72. package/dist/tileset-3d/helpers/bounding-volume.d.ts.map +1 -1
  73. package/dist/tileset-3d/helpers/bounding-volume.js +19 -61
  74. package/dist/tileset-3d/helpers/bounding-volume.js.map +1 -1
  75. package/dist/tileset-3d/helpers/frame-state.d.ts +4 -0
  76. package/dist/tileset-3d/helpers/frame-state.d.ts.map +1 -1
  77. package/dist/tileset-3d/helpers/frame-state.js +6 -1
  78. package/dist/tileset-3d/helpers/frame-state.js.map +1 -1
  79. package/dist/tileset-3d/helpers/s2-bounding-volume.d.ts +17 -0
  80. package/dist/tileset-3d/helpers/s2-bounding-volume.d.ts.map +1 -0
  81. package/dist/tileset-3d/helpers/s2-bounding-volume.js +20 -0
  82. package/dist/tileset-3d/helpers/s2-bounding-volume.js.map +1 -0
  83. package/dist/types.d.ts +2 -0
  84. package/dist/types.d.ts.map +1 -1
  85. package/package.json +14 -13
  86. package/src/index.ts +13 -1
  87. package/src/spatial/format-spatial-reference.ts +127 -53
  88. package/src/spatial/get-spatial-coordinate-frame.ts +126 -31
  89. package/src/spatial/normalize-crs-identifier.ts +26 -0
  90. package/src/spatial/spatial-coordinate-transformer.ts +106 -15
  91. package/src/spatial/tiles-3d-spatial-transformer.ts +206 -0
  92. package/src/tileset-3d/common/indexed-archive-tileset-source.ts +4 -9
  93. package/src/tileset-3d/common/tile-3d-contracts.ts +115 -0
  94. package/src/tileset-3d/common/tile-3d.ts +178 -20
  95. package/src/tileset-3d/common/tileset-3d.ts +63 -3
  96. package/src/tileset-3d/common/tileset-observability.ts +100 -0
  97. package/src/tileset-3d/common/tileset-source.ts +11 -12
  98. package/src/tileset-3d/common/tileset-traverser.ts +32 -5
  99. package/src/tileset-3d/format-3d-tiles/implicit-tiling.ts +481 -34
  100. package/src/tileset-3d/format-3d-tiles/tiles-3d-source.ts +390 -17
  101. package/src/tileset-3d/format-3d-tiles/tileset-3d-traverser.ts +243 -4
  102. package/src/tileset-3d/format-i3s/i3s-source.ts +25 -9
  103. package/src/tileset-3d/helpers/bounding-volume.ts +29 -62
  104. package/src/tileset-3d/helpers/frame-state.ts +6 -1
  105. package/src/tileset-3d/helpers/s2-bounding-volume.ts +35 -0
  106. 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
  }
@@ -495,7 +518,8 @@ export class Tile3D {
495
518
  // eslint-disable-next-line complexity
496
519
  private _calculateRequestPriority(allowUnloadedContent: boolean): number {
497
520
  const traverser = this.tileset._traverser;
498
- const {skipLevelOfDetail} = traverser.options;
521
+ const skipLevelOfDetail =
522
+ traverser.options.skipLevelOfDetail && !traverser.disableSkipLevelOfDetail;
499
523
 
500
524
  /*
501
525
  * Tiles that are outside of the camera's frustum could be skipped if we are in 'ADD' mode
@@ -584,6 +608,8 @@ export class Tile3D {
584
608
  this.contents = loadResult.contents;
585
609
  this.content = this.contents[0] || null;
586
610
  }
611
+ this.vectorContent =
612
+ this.contents.find(content => Boolean(content?.vectorContent))?.vectorContent || null;
587
613
 
588
614
  if (this.tileset.options.contentLoader) {
589
615
  await this.tileset.options.contentLoader(this);
@@ -591,6 +617,7 @@ export class Tile3D {
591
617
 
592
618
  this.contentState = TILE_CONTENT_STATE.READY;
593
619
  this._onContentLoaded();
620
+ this._updateContentEntriesAfterLoad(loadResult);
594
621
  return loadResult;
595
622
  } catch (error) {
596
623
  // Tile is unloaded before the content finishes loading
@@ -664,34 +691,137 @@ export class Tile3D {
664
691
  /**
665
692
  * Replaces a contentless implicit placeholder with its materialized subtree-root header.
666
693
  *
667
- * Transform state is intentionally preserved: the placeholder already owns the composed
668
- * transform, and reapplying the root transform here would double-transform bounds and geometric
669
- * error. Content and raw geometric error are reinitialized from the newly available header.
694
+ * The placeholder transform is preserved when the subtree omits a root transform. An explicit
695
+ * subtree-root transform replaces it and is recomposed against the runtime parent. Content,
696
+ * metadata, bounds, and raw geometric error are reinitialized from the available header.
670
697
  *
671
698
  * @param materializedHeader - Available root header produced from one subtree resource.
672
699
  */
673
700
  applyImplicitSubtreeHeader(materializedHeader: Record<string, any>): void {
674
701
  const existingTransform = this.header.transform;
675
702
  const existingTransformMatrix = this.header.transformMatrix;
703
+ const hasMaterializedTransform = materializedHeader.transform !== undefined;
676
704
  this.header = {
677
705
  ...this.header,
678
706
  ...materializedHeader,
679
- transform: existingTransform,
680
- transformMatrix: existingTransformMatrix,
707
+ transform: hasMaterializedTransform ? materializedHeader.transform : existingTransform,
708
+ transformMatrix: hasMaterializedTransform
709
+ ? materializedHeader.transformMatrix
710
+ : existingTransformMatrix,
681
711
  implicitSubtree: undefined
682
712
  };
713
+ if (hasMaterializedTransform) {
714
+ this._initializeTransforms(this.header);
715
+ }
683
716
  this.refine = this._getRefine(materializedHeader.refine);
684
717
  this.type = materializedHeader.type;
685
718
  this.contentUrl = materializedHeader.contentUrl;
686
719
  this.contentUrls =
687
720
  materializedHeader.contentUrls ||
688
721
  (materializedHeader.contentUrl ? [materializedHeader.contentUrl] : []);
722
+ this._initializeMetadata(this.header);
689
723
  this._initializeLodMetric(materializedHeader);
690
724
  this._updateLodMetricScale();
691
725
  this._initializeBoundingVolumes(this.header);
726
+ this._boundingBox = undefined;
692
727
  // Use the merged header so inherited viewer-request-volume metadata remains available when a
693
728
  // materialized implicit tile has no render content of its own.
694
729
  this._initializeContent(this.header);
730
+ this._initializeContentEntries(this.header);
731
+ }
732
+
733
+ /** Refreshes tile and content metadata from a source or materialized tile header. */
734
+ private _initializeMetadata(header: Record<string, any>): void {
735
+ this.metadata = header.metadata || null;
736
+ const contentHeaders = Array.isArray(header.content) ? header.content : [header.content];
737
+ this.contentMetadata = contentHeaders
738
+ .filter(Boolean)
739
+ .map(contentHeader => contentHeader.metadata || null);
740
+ const firstContentHeader = contentHeaders.find(Boolean) || {};
741
+ const groupReference = firstContentHeader.group;
742
+ const group =
743
+ typeof groupReference === 'number'
744
+ ? this.tileset.groups?.[groupReference] || null
745
+ : groupReference || null;
746
+ const metadataContext: Tile3DMetadataContext = {
747
+ tileset: this.tileset.metadata || null,
748
+ group,
749
+ tile: this.metadata,
750
+ content: this.contentMetadata[0] || null,
751
+ subtree: header.implicitMetadata || null
752
+ };
753
+ if (this.tileset.groups) {
754
+ metadataContext.groups = this.tileset.groups;
755
+ }
756
+ this.metadataContext = metadataContext;
757
+ }
758
+
759
+ /** Updates payload and renderability while retaining immutable metadata descriptors. */
760
+ private _updateContentEntriesAfterLoad(loadResult: TileContentLoadResult): void {
761
+ if (loadResult.contentEntries) {
762
+ this.contentEntries = loadResult.contentEntries.map((entry, index) => ({
763
+ ...entry,
764
+ index,
765
+ boundingVolume:
766
+ entry.boundingVolume || this._contentBoundingVolumes[index] || this.boundingVolume,
767
+ payload: entry.payload ?? this.contents[index] ?? null,
768
+ featureIds: entry.featureIds?.length
769
+ ? entry.featureIds
770
+ : getTile3DFeatureIdSets(entry.payload ?? this.contents[index]),
771
+ renderable: Boolean(
772
+ entry.renderable !== false &&
773
+ (entry.payload ?? this.contents[index]) &&
774
+ !isNestedTilesetPayload(entry.payload ?? this.contents[index])
775
+ )
776
+ }));
777
+ this.featureIdSets = this.contentEntries.flatMap(entry => entry.featureIds);
778
+ return;
779
+ }
780
+ this.contentEntries = this.contentEntries.map((entry, index) => ({
781
+ ...entry,
782
+ payload: this.contents[index] ?? null,
783
+ featureIds: getTile3DFeatureIdSets(this.contents[index]),
784
+ renderable: Boolean(this.contents[index]) && !isNestedTilesetPayload(this.contents[index])
785
+ }));
786
+ this.featureIdSets = this.contentEntries.flatMap(entry => entry.featureIds);
787
+ }
788
+
789
+ /** Returns one ordered content descriptor, or null when the index is out of range. */
790
+ getContentEntry(index: number): Tile3DContent | null {
791
+ return this.contentEntries[index] || null;
792
+ }
793
+
794
+ /** Returns whether an ordered content entry currently has renderable payload. */
795
+ isContentRenderable(index: number): boolean {
796
+ return Boolean(this.contentEntries[index]?.renderable);
797
+ }
798
+
799
+ /** Initializes additive renderer-neutral descriptors from a normalized tile header. */
800
+ private _initializeContentEntries(tileHeader: Record<string, any>): void {
801
+ const headers = Array.isArray(tileHeader.content)
802
+ ? tileHeader.content
803
+ : tileHeader.content
804
+ ? [tileHeader.content]
805
+ : [];
806
+ this.contentEntries = headers.map((contentHeader, index) => {
807
+ const normalizedContentHeader = contentHeader || {};
808
+ return {
809
+ index,
810
+ uri: this.contentUrls[index] || normalizedContentHeader.uri || normalizedContentHeader.url,
811
+ group: normalizedContentHeader.group,
812
+ type: normalizedContentHeader.type,
813
+ payload: null,
814
+ metadata: normalizedContentHeader.metadata || null,
815
+ boundingVolume: this._contentBoundingVolumes[index] || this.boundingVolume,
816
+ featureIds: this._getFeatureIdSets(normalizedContentHeader),
817
+ renderable: false
818
+ };
819
+ });
820
+ }
821
+
822
+ /** Extracts renderer-neutral feature-id declarations without decoding property values. */
823
+ private _getFeatureIdSets(contentHeader: Record<string, any>): Tile3DFeatureIdSet[] {
824
+ return getTile3DFeatureIdSets(contentHeader);
695
825
  }
696
826
 
697
827
  // Unloads the tile's content.
@@ -703,6 +833,13 @@ export class Tile3D {
703
833
  }
704
834
  this.contents = [];
705
835
  this.content = null;
836
+ this.vectorContent = null;
837
+ this.contentEntries = this.contentEntries.map(entry => ({
838
+ ...entry,
839
+ payload: null,
840
+ renderable: false
841
+ }));
842
+ this.featureIdSets = this.contentEntries.flatMap(entry => entry.featureIds);
706
843
  if (this.header.content && this.header.content.destroy) {
707
844
  this.header.content.destroy();
708
845
  }
@@ -790,9 +927,12 @@ export class Tile3D {
790
927
  }
791
928
 
792
929
  this._foveatedFactor = calculateFoveatedFactor(this.boundingVolume, frameState.camera);
930
+ const traverser = this.tileset._traverser;
931
+ const skipLevelOfDetail =
932
+ traverser.options.skipLevelOfDetail && !traverser.disableSkipLevelOfDetail;
793
933
  const deferralEligible = isFoveatedRequestDeferred({
794
934
  refinement: this.refine,
795
- skipLevelOfDetail: this.tileset._traverser.options.skipLevelOfDetail,
935
+ skipLevelOfDetail,
796
936
  foveatedScreenSpaceError: options.foveatedScreenSpaceError,
797
937
  foveatedConeSize: options.foveatedConeSize,
798
938
  minimumScreenSpaceErrorRelaxation: options.foveatedMinimumScreenSpaceErrorRelaxation,
@@ -844,9 +984,15 @@ export class Tile3D {
844
984
  * @param frameState Current camera, culling, and optional clipping-plane state.
845
985
  * @returns The content visibility classification.
846
986
  */
847
- contentVisibility(frameState: FrameState): CullingResult {
987
+ contentVisibility(frameState: FrameState, contentIndex?: number): CullingResult {
988
+ const contentVolumes =
989
+ contentIndex === undefined
990
+ ? this._contentBoundingVolumes
991
+ : this._contentBoundingVolumes[contentIndex]
992
+ ? [this._contentBoundingVolumes[contentIndex]]
993
+ : [];
848
994
  return getContentVisibility(
849
- this._contentBoundingVolumes,
995
+ contentVolumes,
850
996
  this.boundingVolume,
851
997
  frameState.cullingVolume,
852
998
  this._visibilityPlaneMask,
@@ -909,6 +1055,10 @@ export class Tile3D {
909
1055
 
910
1056
  /** Initializes the tile's LOD metric from its header or its nearest available ancestor. */
911
1057
  _initializeLodMetric(header: {[key: string]: any}): void {
1058
+ this._scaleGeometricError =
1059
+ typeof header._scaleGeometricError === 'boolean'
1060
+ ? header._scaleGeometricError
1061
+ : (this.parent?._scaleGeometricError ?? true);
912
1062
  if ('lodMetricType' in header) {
913
1063
  this.lodMetricType = header.lodMetricType;
914
1064
  } else {
@@ -965,7 +1115,7 @@ export class Tile3D {
965
1115
  * I3S `maxScreenThreshold` is already a screen-space metric and must not be transform-scaled.
966
1116
  */
967
1117
  _updateLodMetricScale(): void {
968
- if (this.lodMetricType !== LOD_METRIC_TYPE.GEOMETRIC_ERROR) {
1118
+ if (this.lodMetricType !== LOD_METRIC_TYPE.GEOMETRIC_ERROR || !this._scaleGeometricError) {
969
1119
  this.lodMetricValue = this._unscaledLodMetricValue;
970
1120
  return;
971
1121
  }
@@ -1070,11 +1220,13 @@ export class Tile3D {
1070
1220
  }
1071
1221
 
1072
1222
  _updateBoundingVolume(header) {
1223
+ this._boundingBox = undefined;
1073
1224
  // Update the bounding volumes
1074
1225
  this.boundingVolume = createBoundingVolume(
1075
1226
  header.boundingVolume,
1076
1227
  this.computedTransform,
1077
- this.boundingVolume
1228
+ this.boundingVolume,
1229
+ this._initialTransform
1078
1230
  );
1079
1231
 
1080
1232
  // Viewer request volumes constrain traversal, not just render content. A contentless implicit
@@ -1084,7 +1236,8 @@ export class Tile3D {
1084
1236
  this._viewerRequestVolume = createBoundingVolume(
1085
1237
  header.viewerRequestVolume,
1086
1238
  this.computedTransform,
1087
- this._viewerRequestVolume
1239
+ this._viewerRequestVolume,
1240
+ this._initialTransform
1088
1241
  );
1089
1242
  }
1090
1243
 
@@ -1102,7 +1255,12 @@ export class Tile3D {
1102
1255
  this._contentBoundingVolumes = contentHeaders.length
1103
1256
  ? contentHeaders.map(headerEntry =>
1104
1257
  headerEntry?.boundingVolume
1105
- ? createBoundingVolume(headerEntry.boundingVolume, this.computedTransform)
1258
+ ? createBoundingVolume(
1259
+ headerEntry.boundingVolume,
1260
+ this.computedTransform,
1261
+ undefined,
1262
+ this._initialTransform
1263
+ )
1106
1264
  : this.boundingVolume
1107
1265
  )
1108
1266
  : [this.boundingVolume];
@@ -7,7 +7,7 @@
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
13
  import {getFrameState, limitSelectedTiles, updateCameraMotionState} from '../helpers/frame-state';
@@ -87,6 +87,26 @@ export type Tileset3DProps = {
87
87
  * @default false
88
88
  */
89
89
  skipLevelOfDetail?: boolean;
90
+ /**
91
+ * SSE above which tiles remain part of the non-skipping base traversal.
92
+ * @default 1024
93
+ */
94
+ baseScreenSpaceError?: number;
95
+ /**
96
+ * Required SSE reduction from the nearest requested or loaded ancestor before loading a tile.
97
+ * @default 16
98
+ */
99
+ skipScreenSpaceErrorFactor?: number;
100
+ /**
101
+ * Minimum hierarchy levels between requested tiles during skip-LOD traversal.
102
+ * @default 1
103
+ */
104
+ skipLevels?: number;
105
+ /**
106
+ * Requests only final tiles that meet the active SSE target when skip-LOD is enabled.
107
+ * @default false
108
+ */
109
+ immediatelyLoadDesiredLevelOfDetail?: boolean;
90
110
  /** Enables perspective dynamic SSE to reduce distant, horizon-facing refinement. */
91
111
  dynamicScreenSpaceError?: boolean;
92
112
  /** Base dynamic SSE fog density in inverse meters. */
@@ -154,6 +174,14 @@ type Props = {
154
174
  maximumScreenSpaceError: number;
155
175
  /** Whether replacement traversal may skip levels while retaining ancestor coverage. */
156
176
  skipLevelOfDetail: boolean;
177
+ /** SSE above which tiles remain part of the non-skipping base traversal. */
178
+ baseScreenSpaceError: number;
179
+ /** Required SSE reduction between requested skip-LOD tiles. */
180
+ skipScreenSpaceErrorFactor: number;
181
+ /** Minimum hierarchy levels between requested skip-LOD tiles. */
182
+ skipLevels: number;
183
+ /** Whether skip-LOD requests only the final desired tiles. */
184
+ immediatelyLoadDesiredLevelOfDetail: boolean;
157
185
  /** Whether perspective dynamic SSE is enabled. */
158
186
  dynamicScreenSpaceError: boolean;
159
187
  /** Base dynamic SSE fog density in inverse meters. */
@@ -282,6 +310,10 @@ const DEFAULT_PROPS: Props = {
282
310
  foveatedTimeDelay: 0.2,
283
311
  maximumScreenSpaceError: 8,
284
312
  skipLevelOfDetail: false,
313
+ baseScreenSpaceError: 1024,
314
+ skipScreenSpaceErrorFactor: 16,
315
+ skipLevels: 1,
316
+ immediatelyLoadDesiredLevelOfDetail: false,
285
317
  dynamicScreenSpaceError: true,
286
318
  dynamicScreenSpaceErrorDensity: 2.0e-4,
287
319
  dynamicScreenSpaceErrorFactor: 24,
@@ -322,7 +354,7 @@ export class Tileset3D {
322
354
 
323
355
  type: TILESET_TYPE;
324
356
  tileset: TilesetJSON | null;
325
- loader: LoaderWithParser;
357
+ loader: Loader;
326
358
  url: string;
327
359
  basePath: string;
328
360
  modelMatrix: Matrix4;
@@ -485,6 +517,26 @@ export class Tileset3D {
485
517
  return this._frameNumber;
486
518
  }
487
519
 
520
+ /**
521
+ * Returns the tiles requested during the most recent completed traversal.
522
+ *
523
+ * A defensive copy makes request ordering deterministic for diagnostics and prevents callers
524
+ * from mutating the scheduler's working set.
525
+ */
526
+ get requestedTiles(): readonly Tile3D[] {
527
+ return this._requestedTiles.slice();
528
+ }
529
+
530
+ /**
531
+ * Returns empty tiles considered during the most recent completed traversal.
532
+ *
533
+ * Empty tiles can still be useful hierarchy placeholders, but their IDs are included in
534
+ * observability snapshots so applications can distinguish traversal work from render content.
535
+ */
536
+ get emptyTiles(): readonly Tile3D[] {
537
+ return this._emptyTiles.slice();
538
+ }
539
+
488
540
  /**
489
541
  * Gets or sets the soft target, in bytes, for cached tile content not needed this frame.
490
542
  *
@@ -630,6 +682,14 @@ export class Tileset3D {
630
682
  return;
631
683
  }
632
684
  const preparedViewports = viewports instanceof Array ? viewports : [viewports];
685
+ const activeViewportIds = new Set(preparedViewports.map(viewport => viewport.id));
686
+ // A viewport may be removed between frames. Discard its completed state before aggregating
687
+ // requested and selected tiles so observability reflects only the current traversal inputs.
688
+ for (const frameStateId of Object.keys(this.frameStateData)) {
689
+ if (!activeViewportIds.has(frameStateId)) {
690
+ delete this.frameStateData[frameStateId];
691
+ }
692
+ }
633
693
 
634
694
  this._cache.reset();
635
695
  this._frameNumber++;
@@ -877,7 +937,7 @@ export class Tileset3D {
877
937
  if (
878
938
  initialMetadata?.tileset?.root &&
879
939
  typeof initialMetadata.tileset.root.then !== 'function' &&
880
- !this.source.prepareTileset
940
+ (!this.source.prepareTileset || this.spatialReference?.status === 'native')
881
941
  ) {
882
942
  this.root = this._initializeTileHeaders(initialMetadata.tileset, null);
883
943
  this._applyViewState();
@@ -0,0 +1,100 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) vis.gl contributors
3
+
4
+ import type {Tileset3D} from './tileset-3d';
5
+ import type {Tile3D} from './tile-3d';
6
+
7
+ /**
8
+ * A deterministic, renderer-neutral snapshot of one {@link Tileset3D} traversal.
9
+ *
10
+ * Tile IDs are sorted so snapshots can be compared across runs even when child requests complete
11
+ * in a different order. Counts are reported for the completed traversal frame and cache values are
12
+ * expressed in bytes. This is an inspection contract; it does not alter traversal or request policy.
13
+ */
14
+ export type Tileset3DTraversalSnapshot = {
15
+ /** Monotonically increasing traversal frame number. */
16
+ frameNumber: number;
17
+ /** IDs selected for rendering, sorted for stable comparisons. */
18
+ selectedTileIds: string[];
19
+ /** IDs whose content was requested, sorted for stable comparisons. */
20
+ requestedTileIds: string[];
21
+ /** IDs visited as hierarchy-only or empty tiles, sorted for stable comparisons. */
22
+ emptyTileIds: string[];
23
+ /** Number of selected tiles in the completed frame. */
24
+ visibleTileCount: number;
25
+ /** Number of selected tiles with renderable content. */
26
+ renderableTileCount: number;
27
+ /** Number of tile or subtree loads currently in flight. */
28
+ loadingTileCount: number;
29
+ /** Cumulative number of tiles loaded into the runtime cache. */
30
+ loadedTileCount: number;
31
+ /** Cumulative number of failed tile loads. */
32
+ failedTileCount: number;
33
+ /** Number of tiles currently retained in the runtime cache. */
34
+ cachedTileCount: number;
35
+ /** Estimated cached content bytes. */
36
+ cacheBytes: number;
37
+ /** Active memory-adjusted maximum SSE in logical/CSS pixels. */
38
+ maximumScreenSpaceError: number;
39
+ /** Source-specific implicit subtree counters, when the source exposes them. */
40
+ implicitTiling?: {
41
+ /** Number of subtree resources requested from the source. */
42
+ requestedSubtrees: number;
43
+ /** Number of subtree resources successfully materialized. */
44
+ loadedSubtrees: number;
45
+ /** Number of requests served by the parsed-subtree cache. */
46
+ cacheHits: number;
47
+ /** Number of parsed subtrees currently retained for reuse. */
48
+ cachedSubtrees: number;
49
+ /** Number of subtree requests currently in flight. */
50
+ pendingSubtrees: number;
51
+ /** Number of runtime tile headers created from materialized subtrees. */
52
+ materializedTiles: number;
53
+ };
54
+ };
55
+
56
+ /**
57
+ * Creates a stable observability snapshot from public {@link Tileset3D} state.
58
+ *
59
+ * The helper intentionally reads only public runtime fields and source diagnostics. It is safe to
60
+ * call from an instrumentation loop and does not retain tile or content references.
61
+ *
62
+ * @param tileset - Runtime whose most recent traversal should be inspected.
63
+ * @returns A serializable snapshot suitable for logs, regression fixtures, and benchmark output.
64
+ */
65
+ export function getTileset3DTraversalSnapshot(tileset: Tileset3D): Tileset3DTraversalSnapshot {
66
+ const selectedTiles = tileset.selectedTiles.slice();
67
+ const requestedTiles = tileset.requestedTiles.slice();
68
+ const emptyTiles = tileset.emptyTiles.slice();
69
+
70
+ const getTileIds = (tiles: readonly Tile3D[]): string[] =>
71
+ tiles.map(tile => String(tile.id)).sort();
72
+
73
+ const getStatCount = (name: string): number => {
74
+ const count = tileset.stats.get(name).count;
75
+ return typeof count === 'number' && Number.isFinite(count) ? count : 0;
76
+ };
77
+
78
+ const implicitTilingSource = tileset.source as Tileset3D['source'] & {
79
+ getImplicitTilingStats?: () => Tileset3DTraversalSnapshot['implicitTiling'];
80
+ };
81
+ const implicitTiling = implicitTilingSource.getImplicitTilingStats?.();
82
+
83
+ return {
84
+ frameNumber: tileset.frameNumber,
85
+ selectedTileIds: getTileIds(selectedTiles),
86
+ requestedTileIds: getTileIds(requestedTiles),
87
+ emptyTileIds: getTileIds(emptyTiles),
88
+ visibleTileCount: selectedTiles.length,
89
+ renderableTileCount: selectedTiles.filter(
90
+ tile => tile.contentAvailable && Boolean(tile.content)
91
+ ).length,
92
+ loadingTileCount: getStatCount('Tiles Loading'),
93
+ loadedTileCount: getStatCount('Tiles Loaded'),
94
+ failedTileCount: getStatCount('Failed Tile Loads'),
95
+ cachedTileCount: getStatCount('Tiles In Memory'),
96
+ cacheBytes: tileset.gpuMemoryUsageInBytes,
97
+ maximumScreenSpaceError: tileset.memoryAdjustedScreenSpaceError,
98
+ ...(implicitTiling ? {implicitTiling} : {})
99
+ };
100
+ }
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import type {CoreAPI, LoaderOptions, LoaderWithParser} from '@loaders.gl/loader-utils';
5
+ import type {CoreAPI, Loader, LoaderOptions} from '@loaders.gl/loader-utils';
6
6
  import type {Vector3} from '@math.gl/core';
7
7
  import type {TilesetTraverser, TilesetTraverserProps} from './tileset-traverser';
8
8
  import type {Tile3D} from './tile-3d';
@@ -10,6 +10,7 @@ import type {Tileset3D} from './tileset-3d';
10
10
  import type {FrameState} from '../helpers/frame-state';
11
11
  import type {TILESET_TYPE} from '../../constants';
12
12
  import type {TilesetSpatialReference} from '../../spatial/spatial-types';
13
+ import type {Tile3DContent} from './tile-3d-contracts';
13
14
 
14
15
  /**
15
16
  * Parsed top-level tileset payload consumed by {@link Tileset3D} or a {@link Tileset3DSource}.
@@ -24,18 +25,14 @@ export type TilesetSourceResolver = {
24
25
  /**
25
26
  * Loads and parses the root metadata resource for a source.
26
27
  */
27
- loadRoot<DataT>(
28
- url: string,
29
- loader: LoaderWithParser<DataT>,
30
- loadOptions: LoaderOptions
31
- ): Promise<DataT>;
28
+ loadRoot<DataT>(url: string, loader: Loader<DataT>, loadOptions: LoaderOptions): Promise<DataT>;
32
29
 
33
30
  /**
34
31
  * Loads and parses an arbitrary source-relative resource.
35
32
  */
36
33
  loadResource<DataT>(
37
34
  url: string,
38
- loader: LoaderWithParser<DataT>,
35
+ loader: Loader<DataT>,
39
36
  loadOptions: LoaderOptions
40
37
  ): Promise<DataT>;
41
38
  };
@@ -47,7 +44,7 @@ export type TilesetSourceRequest = {
47
44
  /** Absolute root metadata URL. */
48
45
  url: string;
49
46
  /** Loader used to fetch and parse the root metadata. */
50
- loader: LoaderWithParser;
47
+ loader: Loader;
51
48
  /** Optional base path override for relative resources. */
52
49
  basePath?: string;
53
50
  /** Optional resource-loading strategy used for archive-backed datasets. */
@@ -72,6 +69,8 @@ export type TileContentLoadResult = {
72
69
  loaded: boolean;
73
70
  /** All payloads loaded for a tile, in source order. */
74
71
  contents?: any[];
72
+ /** Optional normalized renderer-neutral descriptors for each content entry. */
73
+ contentEntries?: Tile3DContent[];
75
74
  /** Nested tileset metadata to install below the loaded tile, if any. */
76
75
  nestedTileset?: TilesetJSON;
77
76
  /** Nested tilesets found among multiple content payloads. */
@@ -119,8 +118,8 @@ export type TilesetSourceViewState = {
119
118
  export type TilesetSourceMetadata = {
120
119
  /** Format discriminator. */
121
120
  type: TILESET_TYPE;
122
- /** Loader used for metadata and content requests. */
123
- loader: LoaderWithParser;
121
+ /** Primary format loader used for metadata and default content requests. */
122
+ loader: Loader;
124
123
  /** Absolute root metadata URL. */
125
124
  url: string;
126
125
  /** Base path used for relative resource resolution. */
@@ -143,8 +142,8 @@ export type TilesetSourceMetadata = {
143
142
  export interface Tileset3DSource {
144
143
  /** Tileset format discriminator. */
145
144
  readonly type: TILESET_TYPE;
146
- /** Loader used for content and metadata requests. */
147
- readonly loader: LoaderWithParser;
145
+ /** Primary format loader used for metadata and default content requests. */
146
+ readonly loader: Loader;
148
147
  /** Absolute URL of the root tileset resource. */
149
148
  readonly url: string;
150
149
  /** Base directory used for relative resource resolution. */