@loaders.gl/tiles 5.0.0-alpha.6 → 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 (96) hide show
  1. package/dist/dist.dev.js +5160 -2896
  2. package/dist/dist.min.js +4 -2
  3. package/dist/index.cjs +1509 -454
  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/tile-3d-contracts.d.ts +63 -0
  30. package/dist/tileset-3d/common/tile-3d-contracts.d.ts.map +1 -0
  31. package/dist/tileset-3d/common/tile-3d-contracts.js +51 -0
  32. package/dist/tileset-3d/common/tile-3d-contracts.js.map +1 -0
  33. package/dist/tileset-3d/common/tile-3d.d.ts +29 -5
  34. package/dist/tileset-3d/common/tile-3d.d.ts.map +1 -1
  35. package/dist/tileset-3d/common/tile-3d.js +147 -18
  36. package/dist/tileset-3d/common/tile-3d.js.map +1 -1
  37. package/dist/tileset-3d/common/tileset-3d.d.ts +14 -0
  38. package/dist/tileset-3d/common/tileset-3d.d.ts.map +1 -1
  39. package/dist/tileset-3d/common/tileset-3d.js +27 -1
  40. package/dist/tileset-3d/common/tileset-3d.js.map +1 -1
  41. package/dist/tileset-3d/common/tileset-observability.d.ts +60 -0
  42. package/dist/tileset-3d/common/tileset-observability.d.ts.map +1 -0
  43. package/dist/tileset-3d/common/tileset-observability.js +39 -0
  44. package/dist/tileset-3d/common/tileset-observability.js.map +1 -0
  45. package/dist/tileset-3d/common/tileset-source.d.ts +3 -0
  46. package/dist/tileset-3d/common/tileset-source.d.ts.map +1 -1
  47. package/dist/tileset-3d/common/tileset-source.js.map +1 -1
  48. package/dist/tileset-3d/common/tileset-traverser.d.ts.map +1 -1
  49. package/dist/tileset-3d/common/tileset-traverser.js +4 -3
  50. package/dist/tileset-3d/common/tileset-traverser.js.map +1 -1
  51. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts +47 -1
  52. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts.map +1 -1
  53. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js +327 -30
  54. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js.map +1 -1
  55. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts +32 -0
  56. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts.map +1 -1
  57. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js +265 -14
  58. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js.map +1 -1
  59. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts +5 -3
  60. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts.map +1 -1
  61. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js +7 -4
  62. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js.map +1 -1
  63. package/dist/tileset-3d/helpers/bounding-volume.d.ts +7 -3
  64. package/dist/tileset-3d/helpers/bounding-volume.d.ts.map +1 -1
  65. package/dist/tileset-3d/helpers/bounding-volume.js +19 -61
  66. package/dist/tileset-3d/helpers/bounding-volume.js.map +1 -1
  67. package/dist/tileset-3d/helpers/frame-state.d.ts +4 -0
  68. package/dist/tileset-3d/helpers/frame-state.d.ts.map +1 -1
  69. package/dist/tileset-3d/helpers/frame-state.js +6 -1
  70. package/dist/tileset-3d/helpers/frame-state.js.map +1 -1
  71. package/dist/tileset-3d/helpers/s2-bounding-volume.d.ts +17 -0
  72. package/dist/tileset-3d/helpers/s2-bounding-volume.d.ts.map +1 -0
  73. package/dist/tileset-3d/helpers/s2-bounding-volume.js +20 -0
  74. package/dist/tileset-3d/helpers/s2-bounding-volume.js.map +1 -0
  75. package/dist/types.d.ts +2 -0
  76. package/dist/types.d.ts.map +1 -1
  77. package/package.json +13 -12
  78. package/src/index.ts +13 -1
  79. package/src/spatial/format-spatial-reference.ts +127 -53
  80. package/src/spatial/get-spatial-coordinate-frame.ts +126 -31
  81. package/src/spatial/normalize-crs-identifier.ts +26 -0
  82. package/src/spatial/spatial-coordinate-transformer.ts +106 -15
  83. package/src/spatial/tiles-3d-spatial-transformer.ts +206 -0
  84. package/src/tileset-3d/common/tile-3d-contracts.ts +115 -0
  85. package/src/tileset-3d/common/tile-3d.ts +172 -18
  86. package/src/tileset-3d/common/tileset-3d.ts +29 -1
  87. package/src/tileset-3d/common/tileset-observability.ts +100 -0
  88. package/src/tileset-3d/common/tileset-source.ts +3 -0
  89. package/src/tileset-3d/common/tileset-traverser.ts +4 -3
  90. package/src/tileset-3d/format-3d-tiles/implicit-tiling.ts +481 -34
  91. package/src/tileset-3d/format-3d-tiles/tiles-3d-source.ts +387 -14
  92. package/src/tileset-3d/format-3d-tiles/tileset-3d-traverser.ts +7 -4
  93. package/src/tileset-3d/helpers/bounding-volume.ts +29 -62
  94. package/src/tileset-3d/helpers/frame-state.ts +6 -1
  95. package/src/tileset-3d/helpers/s2-bounding-volume.ts +35 -0
  96. package/src/types.ts +2 -0
@@ -0,0 +1,115 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ /** Structural subset implemented by math.gl bounding volumes used by tile renderers. */
6
+ export type Tile3DBoundingVolume = {
7
+ distanceSquaredTo?(point: unknown): number;
8
+ intersectPlane?(plane: unknown): number;
9
+ };
10
+
11
+ /** Renderer-neutral description of one ordered tile content entry. */
12
+ export type Tile3DContent = {
13
+ /** Zero-based source order, stable across loading and unloading. */
14
+ index: number;
15
+ /** Resolved content URI when declared by the tileset. */
16
+ uri?: string;
17
+ /** Tileset group index declared by this content entry. */
18
+ group?: number;
19
+ /** Normalized tile content kind, when detected. */
20
+ type?: string;
21
+ /** Decoded payload, or null while unloaded/failed. */
22
+ payload: unknown;
23
+ /** Raw content metadata reference. */
24
+ metadata: Record<string, unknown> | null;
25
+ /** Transformed content volume, with tile-volume fallback. */
26
+ boundingVolume: Tile3DBoundingVolume | null;
27
+ /** Normalized feature-id declarations for this content. */
28
+ featureIds: Tile3DFeatureIdSet[];
29
+ /** Whether the payload can currently be rendered. */
30
+ renderable: boolean;
31
+ };
32
+
33
+ /** Renderer-neutral feature-id declaration from a tile content payload. */
34
+ export type Tile3DFeatureIdSet = {
35
+ /** Source of the feature identifiers. */
36
+ source: 'attribute' | 'property-table' | 'constant' | 'texture' | 'implicit';
37
+ /** glTF attribute name for attribute-backed identifiers. */
38
+ attribute?: string;
39
+ /** Structural-metadata property-table index. */
40
+ propertyTable?: number;
41
+ /** Constant feature identifier. */
42
+ constant?: number;
43
+ /** Texture-backed identifier declaration, when present. */
44
+ texture?: Record<string, unknown>;
45
+ /** Implicit identifier declaration, when present. */
46
+ implicit?: Record<string, unknown>;
47
+ /** Decoded identifier values, when supplied by a content loader. */
48
+ values?: Uint32Array | Uint16Array | Uint8Array;
49
+ /** Unrecognized source fields preserved for forward compatibility. */
50
+ details?: Record<string, unknown>;
51
+ };
52
+
53
+ /** Raw metadata references inherited by a tile and its contents. */
54
+ export type Tile3DMetadataContext = {
55
+ /** Tileset-level metadata entity. */
56
+ tileset?: Record<string, unknown> | null;
57
+ /** Group metadata entity selected for the tile's primary content. */
58
+ group?: Record<string, unknown> | null;
59
+ /** All tileset groups, used to resolve a selected content entry's group. */
60
+ groups?: readonly (Record<string, unknown> | null)[];
61
+ /** Tile metadata entity. */
62
+ tile?: Record<string, unknown> | null;
63
+ /** Content metadata entity. */
64
+ content?: Record<string, unknown> | null;
65
+ /** Implicit-subtree metadata entity. */
66
+ subtree?: Record<string, unknown> | null;
67
+ };
68
+
69
+ /** Extracts mesh-feature declarations from a decoded glTF-like payload. */
70
+ export function getTile3DFeatureIdSets(payload: unknown): Tile3DFeatureIdSet[] {
71
+ const featureIdSets: Tile3DFeatureIdSet[] = [];
72
+ const visited = new Set<object>();
73
+ const visit = (value: unknown): void => {
74
+ if (!value || typeof value !== 'object') {
75
+ return;
76
+ }
77
+ if (visited.has(value as object)) {
78
+ return;
79
+ }
80
+ visited.add(value as object);
81
+ const record = value as Record<string, any>;
82
+ const declarations = record.extensions?.EXT_mesh_features?.featureIds;
83
+ if (Array.isArray(declarations)) {
84
+ for (const declaration of declarations) {
85
+ if (!declaration || typeof declaration !== 'object') {
86
+ continue;
87
+ }
88
+ const source = declaration.texture
89
+ ? 'texture'
90
+ : declaration.implicit
91
+ ? 'implicit'
92
+ : declaration.attribute !== undefined
93
+ ? 'attribute'
94
+ : declaration.propertyTable !== undefined
95
+ ? 'property-table'
96
+ : 'constant';
97
+ featureIdSets.push({
98
+ source,
99
+ attribute: declaration.attribute,
100
+ propertyTable: declaration.propertyTable,
101
+ constant: declaration.constant,
102
+ texture: declaration.texture,
103
+ implicit: declaration.implicit,
104
+ values: declaration.values,
105
+ details: declaration
106
+ });
107
+ }
108
+ }
109
+ for (const child of Object.values(record)) {
110
+ visit(child);
111
+ }
112
+ };
113
+ visit(payload);
114
+ return featureIdSets;
115
+ }
@@ -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
  }
@@ -585,6 +608,8 @@ export class Tile3D {
585
608
  this.contents = loadResult.contents;
586
609
  this.content = this.contents[0] || null;
587
610
  }
611
+ this.vectorContent =
612
+ this.contents.find(content => Boolean(content?.vectorContent))?.vectorContent || null;
588
613
 
589
614
  if (this.tileset.options.contentLoader) {
590
615
  await this.tileset.options.contentLoader(this);
@@ -592,6 +617,7 @@ export class Tile3D {
592
617
 
593
618
  this.contentState = TILE_CONTENT_STATE.READY;
594
619
  this._onContentLoaded();
620
+ this._updateContentEntriesAfterLoad(loadResult);
595
621
  return loadResult;
596
622
  } catch (error) {
597
623
  // Tile is unloaded before the content finishes loading
@@ -665,34 +691,137 @@ export class Tile3D {
665
691
  /**
666
692
  * Replaces a contentless implicit placeholder with its materialized subtree-root header.
667
693
  *
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.
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.
671
697
  *
672
698
  * @param materializedHeader - Available root header produced from one subtree resource.
673
699
  */
674
700
  applyImplicitSubtreeHeader(materializedHeader: Record<string, any>): void {
675
701
  const existingTransform = this.header.transform;
676
702
  const existingTransformMatrix = this.header.transformMatrix;
703
+ const hasMaterializedTransform = materializedHeader.transform !== undefined;
677
704
  this.header = {
678
705
  ...this.header,
679
706
  ...materializedHeader,
680
- transform: existingTransform,
681
- transformMatrix: existingTransformMatrix,
707
+ transform: hasMaterializedTransform ? materializedHeader.transform : existingTransform,
708
+ transformMatrix: hasMaterializedTransform
709
+ ? materializedHeader.transformMatrix
710
+ : existingTransformMatrix,
682
711
  implicitSubtree: undefined
683
712
  };
713
+ if (hasMaterializedTransform) {
714
+ this._initializeTransforms(this.header);
715
+ }
684
716
  this.refine = this._getRefine(materializedHeader.refine);
685
717
  this.type = materializedHeader.type;
686
718
  this.contentUrl = materializedHeader.contentUrl;
687
719
  this.contentUrls =
688
720
  materializedHeader.contentUrls ||
689
721
  (materializedHeader.contentUrl ? [materializedHeader.contentUrl] : []);
722
+ this._initializeMetadata(this.header);
690
723
  this._initializeLodMetric(materializedHeader);
691
724
  this._updateLodMetricScale();
692
725
  this._initializeBoundingVolumes(this.header);
726
+ this._boundingBox = undefined;
693
727
  // Use the merged header so inherited viewer-request-volume metadata remains available when a
694
728
  // materialized implicit tile has no render content of its own.
695
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);
696
825
  }
697
826
 
698
827
  // Unloads the tile's content.
@@ -704,6 +833,13 @@ export class Tile3D {
704
833
  }
705
834
  this.contents = [];
706
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);
707
843
  if (this.header.content && this.header.content.destroy) {
708
844
  this.header.content.destroy();
709
845
  }
@@ -848,9 +984,15 @@ export class Tile3D {
848
984
  * @param frameState Current camera, culling, and optional clipping-plane state.
849
985
  * @returns The content visibility classification.
850
986
  */
851
- 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
+ : [];
852
994
  return getContentVisibility(
853
- this._contentBoundingVolumes,
995
+ contentVolumes,
854
996
  this.boundingVolume,
855
997
  frameState.cullingVolume,
856
998
  this._visibilityPlaneMask,
@@ -913,6 +1055,10 @@ export class Tile3D {
913
1055
 
914
1056
  /** Initializes the tile's LOD metric from its header or its nearest available ancestor. */
915
1057
  _initializeLodMetric(header: {[key: string]: any}): void {
1058
+ this._scaleGeometricError =
1059
+ typeof header._scaleGeometricError === 'boolean'
1060
+ ? header._scaleGeometricError
1061
+ : (this.parent?._scaleGeometricError ?? true);
916
1062
  if ('lodMetricType' in header) {
917
1063
  this.lodMetricType = header.lodMetricType;
918
1064
  } else {
@@ -969,7 +1115,7 @@ export class Tile3D {
969
1115
  * I3S `maxScreenThreshold` is already a screen-space metric and must not be transform-scaled.
970
1116
  */
971
1117
  _updateLodMetricScale(): void {
972
- if (this.lodMetricType !== LOD_METRIC_TYPE.GEOMETRIC_ERROR) {
1118
+ if (this.lodMetricType !== LOD_METRIC_TYPE.GEOMETRIC_ERROR || !this._scaleGeometricError) {
973
1119
  this.lodMetricValue = this._unscaledLodMetricValue;
974
1120
  return;
975
1121
  }
@@ -1074,11 +1220,13 @@ export class Tile3D {
1074
1220
  }
1075
1221
 
1076
1222
  _updateBoundingVolume(header) {
1223
+ this._boundingBox = undefined;
1077
1224
  // Update the bounding volumes
1078
1225
  this.boundingVolume = createBoundingVolume(
1079
1226
  header.boundingVolume,
1080
1227
  this.computedTransform,
1081
- this.boundingVolume
1228
+ this.boundingVolume,
1229
+ this._initialTransform
1082
1230
  );
1083
1231
 
1084
1232
  // Viewer request volumes constrain traversal, not just render content. A contentless implicit
@@ -1088,7 +1236,8 @@ export class Tile3D {
1088
1236
  this._viewerRequestVolume = createBoundingVolume(
1089
1237
  header.viewerRequestVolume,
1090
1238
  this.computedTransform,
1091
- this._viewerRequestVolume
1239
+ this._viewerRequestVolume,
1240
+ this._initialTransform
1092
1241
  );
1093
1242
  }
1094
1243
 
@@ -1106,7 +1255,12 @@ export class Tile3D {
1106
1255
  this._contentBoundingVolumes = contentHeaders.length
1107
1256
  ? contentHeaders.map(headerEntry =>
1108
1257
  headerEntry?.boundingVolume
1109
- ? createBoundingVolume(headerEntry.boundingVolume, this.computedTransform)
1258
+ ? createBoundingVolume(
1259
+ headerEntry.boundingVolume,
1260
+ this.computedTransform,
1261
+ undefined,
1262
+ this._initialTransform
1263
+ )
1110
1264
  : this.boundingVolume
1111
1265
  )
1112
1266
  : [this.boundingVolume];
@@ -517,6 +517,26 @@ export class Tileset3D {
517
517
  return this._frameNumber;
518
518
  }
519
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
+
520
540
  /**
521
541
  * Gets or sets the soft target, in bytes, for cached tile content not needed this frame.
522
542
  *
@@ -662,6 +682,14 @@ export class Tileset3D {
662
682
  return;
663
683
  }
664
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
+ }
665
693
 
666
694
  this._cache.reset();
667
695
  this._frameNumber++;
@@ -909,7 +937,7 @@ export class Tileset3D {
909
937
  if (
910
938
  initialMetadata?.tileset?.root &&
911
939
  typeof initialMetadata.tileset.root.then !== 'function' &&
912
- !this.source.prepareTileset
940
+ (!this.source.prepareTileset || this.spatialReference?.status === 'native')
913
941
  ) {
914
942
  this.root = this._initializeTileHeaders(initialMetadata.tileset, null);
915
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
+ }
@@ -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}.
@@ -68,6 +69,8 @@ export type TileContentLoadResult = {
68
69
  loaded: boolean;
69
70
  /** All payloads loaded for a tile, in source order. */
70
71
  contents?: any[];
72
+ /** Optional normalized renderer-neutral descriptors for each content entry. */
73
+ contentEntries?: Tile3DContent[];
71
74
  /** Nested tileset metadata to install below the loaded tile, if any. */
72
75
  nestedTileset?: TilesetJSON;
73
76
  /** Nested tilesets found among multiple content payloads. */
@@ -219,9 +219,10 @@ export class TilesetTraverser {
219
219
  }
220
220
  stack.push(child);
221
221
  hasVisibleChild = true;
222
- } else if (checkRefines || loadSiblings) {
223
- // Keep non-visible children loaded since they are still needed before the parent can refine.
224
- // Or loadSiblings is true so always load tiles regardless of visibility.
222
+ } else if ((checkRefines || loadSiblings) && child._inRequestVolume) {
223
+ // Keep non-visible children loaded when they are inside their viewer request volume;
224
+ // they may still be needed before a replacement parent can refine. The request-volume
225
+ // check intentionally applies to sibling loading as well, preventing off-volume work.
225
226
  this.loadTile(child, frameState);
226
227
  this.touchTile(child, frameState);
227
228
  }