@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
@@ -2,10 +2,10 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import {path, RequestCache} from '@loaders.gl/loader-utils';
5
+ import {path, RequestCache, sliceArrayBuffer} from '@loaders.gl/loader-utils';
6
6
  import {Ellipsoid} from '@math.gl/geospatial';
7
- import {Vector3} from '@math.gl/core';
8
- import type {CoreAPI, LoaderOptions, LoaderWithParser} from '@loaders.gl/loader-utils';
7
+ import {Matrix4, Vector3} from '@math.gl/core';
8
+ import type {CoreAPI, Loader, LoaderContext, LoaderOptions} from '@loaders.gl/loader-utils';
9
9
  import type {Tile3D} from '../common/tile-3d';
10
10
  import {Tileset3DTraverser} from './tileset-3d-traverser';
11
11
  import type {Tileset3D} from '../common/tileset-3d';
@@ -27,6 +27,12 @@ import {TILESET_TYPE} from '../../constants';
27
27
  import type {TilesetTraverser, TilesetTraverserProps} from '../common/tileset-traverser';
28
28
  import type {FrameState} from '../helpers/frame-state';
29
29
  import {get3DTilesSpatialReference} from '../../spatial/format-spatial-reference';
30
+ import {Tiles3DSpatialTransformer} from '../../spatial/tiles-3d-spatial-transformer';
31
+ import {
32
+ applyTilesetSpatialOptions,
33
+ markTilesetSpatialReferenceTransformed
34
+ } from '../../spatial/spatial-types';
35
+ import type {TilesetSpatialReference} from '../../spatial/spatial-types';
30
36
  import {
31
37
  materializeImplicitSubtree,
32
38
  type ImplicitSubtreeReference,
@@ -42,6 +48,24 @@ const EMPTY_CONTENT_FORMATS: TilesetContentFormats = {
42
48
 
43
49
  const DEFAULT_MAXIMUM_CACHED_SUBTREES = 32;
44
50
 
51
+ type PackageFile = {
52
+ name?: string;
53
+ mimeType: string;
54
+ uri?: string;
55
+ originalUri?: string;
56
+ data?: ArrayBuffer;
57
+ byteOffset: number;
58
+ byteLength: number;
59
+ bufferUri?: string;
60
+ };
61
+
62
+ type PackageResource = {fileIndex: number; files: PackageFile[]};
63
+
64
+ type Tiles3DPackageLoaderContext = LoaderContext & {
65
+ /** Parent package records available while parsing an embedded tileset or subtree. */
66
+ _tiles3dPackageFiles: PackageFile[];
67
+ };
68
+
45
69
  /** Diagnostics for source-managed implicit subtree loading. */
46
70
  export type ImplicitTilingStats = {
47
71
  /** Subtree resources requested from the resolver or core API. */
@@ -65,7 +89,7 @@ export class Tiles3DSource implements Tileset3DSource {
65
89
  /** 3D Tiles format discriminator. */
66
90
  readonly type = TILESET_TYPE.TILES3D;
67
91
  /** Loader used for tile content requests. */
68
- readonly loader: LoaderWithParser;
92
+ readonly loader: Loader;
69
93
  /** Root tileset URL. */
70
94
  readonly url: string;
71
95
  /** Base path used for relative tile resource resolution. */
@@ -93,6 +117,12 @@ export class Tiles3DSource implements Tileset3DSource {
93
117
  private readonly queryParams: Record<string, string> = {};
94
118
  /** Final request URLs cached by unmodified tile content URL. */
95
119
  private readonly tileUrlCache: Map<string, string> = new Map();
120
+ /** Original content descriptors retained across {@link Tile3D.unloadContent} calls. */
121
+ private readonly tileContentHeaders = new WeakMap<Tile3D, Record<string, any>[]>();
122
+ /** Source-local namespaces assigned to distinct embedded glTF package file collections. */
123
+ private readonly packageNamespaces = new WeakMap<PackageFile[], number>();
124
+ /** Next source-local package namespace. */
125
+ private nextPackageNamespace = 0;
96
126
  /** Parsed subtree requests keyed by final source URL for deduplication and LRU reuse. */
97
127
  private readonly implicitSubtreeCache: RequestCache<ParsedImplicitSubtree>;
98
128
  /** Mutable counters exposed as a defensive snapshot through {@link getImplicitTilingStats}. */
@@ -110,6 +140,10 @@ export class Tiles3DSource implements Tileset3DSource {
110
140
  private readonly extensionsUsed: string[] = [];
111
141
  private readonly resolver?: TilesetSourceResolver;
112
142
  private rootTileset: TilesetJSON;
143
+ /** CRS transformer shared by this source's headers and decoded glTF content. */
144
+ private spatialTransformer?: Tiles3DSpatialTransformer;
145
+ /** Root header after composing source-frame transforms and rebuilding target bounds. */
146
+ private preparedRootHeader?: Record<string, any>;
113
147
 
114
148
  /**
115
149
  * Creates a 3D Tiles source.
@@ -175,11 +209,12 @@ export class Tiles3DSource implements Tileset3DSource {
175
209
  throw new Error('Tileset must have an asset property.');
176
210
  }
177
211
  if (
212
+ this.rootTileset.formatVersion !== '2.0-draft' &&
178
213
  this.asset.version !== '0.0' &&
179
214
  this.asset.version !== '1.0' &&
180
215
  this.asset.version !== '1.1'
181
216
  ) {
182
- throw new Error('The tileset must be 3D Tiles version either 0.0 or 1.0 or 1.1.');
217
+ throw new Error('The tileset must be 3D Tiles version 0.0, 1.0, 1.1, or 2.0-draft.');
183
218
  }
184
219
 
185
220
  if ('tilesetVersion' in this.asset) {
@@ -224,6 +259,29 @@ export class Tiles3DSource implements Tileset3DSource {
224
259
  return this.getMetadata().tileset;
225
260
  }
226
261
 
262
+ /** Prepare nonlinear CRS state before runtime headers are materialized. */
263
+ async prepareTileset(tileset: Tileset3D): Promise<void> {
264
+ const spatialReference = tileset.spatialReference;
265
+ if (spatialReference.status === 'unresolved') {
266
+ throw new Error(
267
+ spatialReference.warnings[0] ||
268
+ '3D Tiles spatial operations cannot be resolved from the supplied metadata and options'
269
+ );
270
+ }
271
+ if (spatialReference.status !== 'transformable' && spatialReference.status !== 'transformed') {
272
+ return;
273
+ }
274
+ this.spatialTransformer ||= new Tiles3DSpatialTransformer(
275
+ spatialReference,
276
+ tileset.options.spatial
277
+ );
278
+ tileset.spatialReference = markTilesetSpatialReferenceTransformed(spatialReference);
279
+ this.preparedRootHeader = this.transformTileHeader(
280
+ this.getMetadata().tileset.root,
281
+ new Matrix4()
282
+ );
283
+ }
284
+
227
285
  /**
228
286
  * Builds explicit runtime headers while leaving implicit subtree references lazy.
229
287
  */
@@ -232,7 +290,8 @@ export class Tiles3DSource implements Tileset3DSource {
232
290
  tilesetJson: TilesetJSON,
233
291
  parentTile?: Tile3D | null
234
292
  ): Tile3D {
235
- const rootTile = new Tile3DNode(tileset, tilesetJson.root, parentTile || undefined);
293
+ const rootHeader = parentTile ? tilesetJson.root : this.preparedRootHeader || tilesetJson.root;
294
+ const rootTile = new Tile3DNode(tileset, rootHeader, parentTile || undefined);
236
295
 
237
296
  if (parentTile) {
238
297
  parentTile.children.push(rootTile);
@@ -274,6 +333,17 @@ export class Tiles3DSource implements Tileset3DSource {
274
333
  */
275
334
  async loadTileContent(tile: Tile3D): Promise<TileContentLoadResult> {
276
335
  const contentUrls = (tile.contentUrls || [tile.contentUrl]).filter(Boolean);
336
+ const cachedContentHeaders = this.tileContentHeaders.get(tile);
337
+ const contentHeaders: Record<string, any>[] =
338
+ cachedContentHeaders ||
339
+ (Array.isArray(tile.header?.content)
340
+ ? tile.header.content
341
+ : tile.header?.content
342
+ ? [tile.header.content]
343
+ : []);
344
+ if (!cachedContentHeaders) {
345
+ this.tileContentHeaders.set(tile, contentHeaders);
346
+ }
277
347
  const tilesetLoaderOptions =
278
348
  (this.loadOptions[this.loader.id] as Record<string, unknown>) || {};
279
349
  const options = {
@@ -283,12 +353,37 @@ export class Tiles3DSource implements Tileset3DSource {
283
353
  // Content bytes, rather than URL suffixes, distinguish external tilesets from renderable
284
354
  // payloads. This is required for signed and extensionless resources.
285
355
  isTileset: 'auto',
286
- assetGltfUpAxis: (this.asset && this.asset.gltfUpAxis) || 'Y'
356
+ assetGltfUpAxis: (this.asset && this.asset.gltfUpAxis) || 'Y',
357
+ _tilesetOptions: {
358
+ spatialReference: tile.header?._spatialReference || tile.tileset?.spatialReference,
359
+ spatialOptions: tile.tileset?.options.spatial,
360
+ spatialTransform: tile.header?._spatialTransform
361
+ }
287
362
  }
288
363
  };
289
364
 
290
365
  const contents = await Promise.all(
291
- contentUrls.map(contentUrl => this.loadResourceData(this.getTileUrl(contentUrl), options))
366
+ contentUrls.map((contentUrl, contentIndex) => {
367
+ const contentHeader = contentHeaders[contentIndex] || {};
368
+ const vectorExtension = contentHeader.extensions?.['3DTILES_content_gltf_vector'];
369
+ const vectorContent =
370
+ contentHeader._vectorContent ||
371
+ (vectorExtension?.vector === true ? {clip: vectorExtension.clip === true} : undefined);
372
+ const contentOptions = vectorContent
373
+ ? {
374
+ ...options,
375
+ [this.loader.id]: {
376
+ ...(options[this.loader.id] as Record<string, unknown>),
377
+ vectorContent
378
+ }
379
+ }
380
+ : options;
381
+ return this.loadContentResource(
382
+ this.getTileUrl(contentUrl),
383
+ contentHeader._resource,
384
+ contentOptions
385
+ );
386
+ })
292
387
  );
293
388
  tile.contents = contents;
294
389
  tile.content = contents[0] || null;
@@ -302,6 +397,72 @@ export class Tiles3DSource implements Tileset3DSource {
302
397
  };
303
398
  }
304
399
 
400
+ /** Transform a tile header and its eagerly declared descendants. */
401
+ private transformTileHeader(
402
+ header: Record<string, any>,
403
+ parentTransform: Matrix4 = new Matrix4()
404
+ ): Record<string, any> {
405
+ if (!this.spatialTransformer) {
406
+ return header;
407
+ }
408
+ return this.transformTileHeaderWithTransformer(
409
+ header,
410
+ this.spatialTransformer,
411
+ parentTransform
412
+ );
413
+ }
414
+
415
+ /** Rebuilds one header subtree using an explicitly selected CRS transformer. */
416
+ private transformTileHeaderWithTransformer(
417
+ header: Record<string, any>,
418
+ transformer: Tiles3DSpatialTransformer,
419
+ parentTransform: Matrix4,
420
+ spatialReference?: TilesetSpatialReference
421
+ ): Record<string, any> {
422
+ const localTransform = header.transform ? new Matrix4(header.transform) : new Matrix4();
423
+ const composedTransform = new Matrix4(parentTransform).multiplyRight(localTransform);
424
+ const content = Array.isArray(header.content)
425
+ ? header.content.map((entry: Record<string, any>) =>
426
+ this.transformContentHeader(entry, composedTransform, transformer)
427
+ )
428
+ : header.content
429
+ ? this.transformContentHeader(header.content, composedTransform, transformer)
430
+ : header.content;
431
+ return {
432
+ ...header,
433
+ transform: undefined,
434
+ boundingVolume: header.boundingVolume
435
+ ? transformer.transformBoundingVolume(header.boundingVolume, composedTransform)
436
+ : header.boundingVolume,
437
+ _spatialReference: spatialReference,
438
+ _spatialTransform: composedTransform.toArray(),
439
+ content,
440
+ children: Array.isArray(header.children)
441
+ ? header.children.map((child: Record<string, any>) =>
442
+ this.transformTileHeaderWithTransformer(
443
+ child,
444
+ transformer,
445
+ composedTransform,
446
+ spatialReference
447
+ )
448
+ )
449
+ : header.children
450
+ };
451
+ }
452
+
453
+ private transformContentHeader(
454
+ content: Record<string, any>,
455
+ composedTransform: Matrix4,
456
+ transformer: Tiles3DSpatialTransformer
457
+ ): Record<string, any> {
458
+ return {
459
+ ...content,
460
+ boundingVolume: content.boundingVolume
461
+ ? transformer.transformBoundingVolume(content.boundingVolume, composedTransform)
462
+ : content.boundingVolume
463
+ };
464
+ }
465
+
305
466
  /**
306
467
  * Loads, materializes, and installs exactly one implicit subtree.
307
468
  *
@@ -321,7 +482,7 @@ export class Tiles3DSource implements Tileset3DSource {
321
482
  }
322
483
 
323
484
  const subtreeUrl = this.getTileUrl(reference.subtreeUrl);
324
- const subtree = await this.loadImplicitSubtreeResource(subtreeUrl);
485
+ const subtree = await this.loadImplicitSubtreeResource(subtreeUrl, reference.resource);
325
486
  if (this.destroyed || tile.isDestroyed()) {
326
487
  return {loaded: false, tileCount: 0, childSubtreeCount: 0};
327
488
  }
@@ -330,11 +491,25 @@ export class Tiles3DSource implements Tileset3DSource {
330
491
  subtreeUrl
331
492
  });
332
493
 
333
- tile.applyImplicitSubtreeHeader(materializedSubtree.root);
494
+ const nestedSpatialReference = tile.header._spatialReference as
495
+ | TilesetSpatialReference
496
+ | undefined;
497
+ const subtreeTransformer = nestedSpatialReference
498
+ ? new Tiles3DSpatialTransformer(nestedSpatialReference, tile.tileset.options.spatial)
499
+ : this.spatialTransformer;
500
+ const materializedRoot = subtreeTransformer
501
+ ? this.transformTileHeaderWithTransformer(
502
+ materializedSubtree.root,
503
+ subtreeTransformer,
504
+ new Matrix4(),
505
+ nestedSpatialReference
506
+ )
507
+ : materializedSubtree.root;
508
+ tile.applyImplicitSubtreeHeader(materializedRoot);
334
509
  const materializedTileCount = this.initializeMaterializedChildren(
335
510
  tile.tileset,
336
511
  tile,
337
- materializedSubtree.root.children
512
+ materializedRoot.children
338
513
  );
339
514
  this.implicitTilingStats.loadedSubtrees++;
340
515
  this.implicitTilingStats.materializedTiles += materializedTileCount;
@@ -477,17 +652,106 @@ export class Tiles3DSource implements Tileset3DSource {
477
652
  const nestedTilesets =
478
653
  loadResult.nestedTilesets || (loadResult.nestedTileset ? [loadResult.nestedTileset] : []);
479
654
  for (const nestedTileset of nestedTilesets) {
480
- tileset._initializeTileHeaders(nestedTileset, tile);
655
+ tileset._initializeTileHeaders(this.prepareNestedTileset(tileset, nestedTileset), tile);
481
656
  }
482
657
  }
483
658
 
659
+ /**
660
+ * Resolves a nested tileset's CRS independently and adapts it to the owning tileset output frame.
661
+ *
662
+ * Nested resources may use a different source CRS from their parent. Their headers and glTF
663
+ * payloads must therefore use a transformer built from the nested descriptor, while the parent
664
+ * runtime continues to expose one requested output frame to traversal and rendering.
665
+ */
666
+ private prepareNestedTileset(tileset: Tileset3D, nestedTileset: TilesetJSON): TilesetJSON {
667
+ const nestedSpatialReference = nestedTileset.spatialMetadata;
668
+ const parentSpatialReference = tileset.spatialReference;
669
+
670
+ if (
671
+ nestedSpatialReference?.status === 'unresolved' ||
672
+ nestedSpatialReference?.crs.state === 'unknown'
673
+ ) {
674
+ throw new Error(
675
+ nestedSpatialReference.warnings[0] ||
676
+ 'Nested 3D Tiles CRS is explicitly unresolved and cannot be placed in the parent frame'
677
+ );
678
+ }
679
+
680
+ if (!nestedSpatialReference || !nestedSpatialReference.sourceCrs) {
681
+ if (
682
+ this.spatialTransformer &&
683
+ (parentSpatialReference.status === 'transformable' ||
684
+ parentSpatialReference.status === 'transformed')
685
+ ) {
686
+ return {
687
+ ...nestedTileset,
688
+ spatialMetadata: parentSpatialReference,
689
+ root: this.transformTileHeaderWithTransformer(
690
+ nestedTileset.root,
691
+ this.spatialTransformer,
692
+ new Matrix4(),
693
+ parentSpatialReference
694
+ )
695
+ };
696
+ }
697
+ return addNestedSpatialReference(nestedTileset, parentSpatialReference);
698
+ }
699
+
700
+ const parentCoordinateEpoch = parentSpatialReference.coordinateEpoch;
701
+ const nestedCoordinateEpoch = nestedSpatialReference.coordinateEpoch;
702
+ if (
703
+ parentCoordinateEpoch !== undefined &&
704
+ nestedCoordinateEpoch !== undefined &&
705
+ parentCoordinateEpoch !== nestedCoordinateEpoch
706
+ ) {
707
+ throw new Error(
708
+ `Nested 3D Tiles coordinate epoch ${nestedCoordinateEpoch} differs from parent epoch ${parentCoordinateEpoch}; epoch-aware transformation is unavailable`
709
+ );
710
+ }
711
+
712
+ const outputCrs = parentSpatialReference.targetCrs || parentSpatialReference.sourceCrs;
713
+ if (!outputCrs) {
714
+ return addNestedSpatialReference(nestedTileset, nestedSpatialReference);
715
+ }
716
+
717
+ const nestedOutputReference = applyTilesetSpatialOptions(nestedSpatialReference, {
718
+ ...tileset.options.spatial,
719
+ outputCoordinates: 'target-crs',
720
+ targetCrs: outputCrs
721
+ });
722
+ if (nestedOutputReference.status === 'unresolved') {
723
+ throw new Error(
724
+ nestedOutputReference.warnings[0] ||
725
+ 'Nested 3D Tiles CRS cannot be resolved in the parent output frame'
726
+ );
727
+ }
728
+ if (nestedOutputReference.status === 'native') {
729
+ return addNestedSpatialReference(nestedTileset, nestedOutputReference);
730
+ }
731
+
732
+ const nestedTransformer = new Tiles3DSpatialTransformer(
733
+ nestedOutputReference,
734
+ tileset.options.spatial
735
+ );
736
+ return {
737
+ ...nestedTileset,
738
+ spatialMetadata: nestedOutputReference,
739
+ root: this.transformTileHeaderWithTransformer(
740
+ nestedTileset.root,
741
+ nestedTransformer,
742
+ new Matrix4(),
743
+ nestedOutputReference
744
+ )
745
+ };
746
+ }
747
+
484
748
  /**
485
749
  * Loads data through injected core APIs so this module stays independent from `@loaders.gl/core`.
486
750
  */
487
751
  private async loadWithCoreApi(
488
752
  url: string,
489
753
  options: LoaderOptions,
490
- loader: LoaderWithParser = this.loader
754
+ loader: Loader = this.loader
491
755
  ): Promise<any> {
492
756
  if (!this.coreApi) {
493
757
  throw new Error('Tiles3DSource requires an injected coreApi to load tileset data');
@@ -513,7 +777,7 @@ export class Tiles3DSource implements Tileset3DSource {
513
777
  private async loadResourceData(
514
778
  url: string,
515
779
  options: LoaderOptions,
516
- loader: LoaderWithParser = this.loader
780
+ loader: Loader = this.loader
517
781
  ): Promise<any> {
518
782
  if (this.resolver) {
519
783
  return await this.resolver.loadResource(url, loader, options);
@@ -522,13 +786,102 @@ export class Tiles3DSource implements Tileset3DSource {
522
786
  return await this.loadWithCoreApi(url, options, loader);
523
787
  }
524
788
 
789
+ /** Loads a URL content reference or parses a buffer-view file retained from a glTF package. */
790
+ private async loadContentResource(
791
+ url: string,
792
+ packageResource: PackageResource | undefined,
793
+ options: LoaderOptions
794
+ ): Promise<any> {
795
+ if (!packageResource) {
796
+ return await this.loadResourceData(url, options);
797
+ }
798
+ const file = packageResource.files[packageResource.fileIndex];
799
+ if (!file) {
800
+ throw new Error(`3D Tiles package references missing file ${packageResource.fileIndex}`);
801
+ }
802
+ if (file.uri) {
803
+ return await this.loadResourceData(this.getTileUrl(file.uri), options);
804
+ }
805
+ if (!this.coreApi) {
806
+ throw new Error('Tiles3DSource requires an injected coreApi to parse embedded package data');
807
+ }
808
+
809
+ const data = await this.getPackageFileData(file);
810
+ const packageNamespace = this.getPackageNamespace(packageResource.files);
811
+ const packageRootUrl = `gltf-package://${packageNamespace}/${packageResource.fileIndex}`;
812
+ const fileName = file.name || 'content';
813
+ const directorySeparatorIndex = fileName.lastIndexOf('/');
814
+ const packageBaseUrl =
815
+ directorySeparatorIndex >= 0
816
+ ? `${packageRootUrl}/${fileName.slice(0, directorySeparatorIndex)}`
817
+ : packageRootUrl;
818
+ return await this.coreApi.parse(data, this.loader, options, {
819
+ url: `${packageRootUrl}/${fileName}`,
820
+ filename: fileName,
821
+ baseUrl: packageBaseUrl,
822
+ loaders: [this.loader],
823
+ coreApi: this.coreApi,
824
+ _parse: this.coreApi.parse,
825
+ _tiles3dPackageFiles: packageResource.files,
826
+ fetch: async (resource: string, init?: RequestInit) => {
827
+ const reference = resource.startsWith(`${packageRootUrl}/`)
828
+ ? resource.slice(packageRootUrl.length + 1)
829
+ : resource;
830
+ const referencedFile = packageResource.files.find(
831
+ packageFile => packageFile.name === reference || packageFile.originalUri === reference
832
+ );
833
+ if (!referencedFile) {
834
+ throw new Error(`3D Tiles glTF package does not contain file ${reference}`);
835
+ }
836
+ if (referencedFile.uri) {
837
+ return await this.coreApi!.fetchFile(this.getTileUrl(referencedFile.uri), init);
838
+ }
839
+ const referencedData = await this.getPackageFileData(referencedFile);
840
+ return new Response(referencedData, {
841
+ headers: {'content-type': referencedFile.mimeType}
842
+ });
843
+ }
844
+ } as Tiles3DPackageLoaderContext);
845
+ }
846
+
847
+ /** Returns a stable source-local namespace for one embedded glTF package file collection. */
848
+ private getPackageNamespace(files: PackageFile[]): number {
849
+ const existingNamespace = this.packageNamespaces.get(files);
850
+ if (existingNamespace !== undefined) {
851
+ return existingNamespace;
852
+ }
853
+ const packageNamespace = this.nextPackageNamespace++;
854
+ this.packageNamespaces.set(files, packageNamespace);
855
+ return packageNamespace;
856
+ }
857
+
858
+ /** Resolves a retained package-file byte range without eagerly copying other package files. */
859
+ private async getPackageFileData(file: PackageFile): Promise<ArrayBuffer> {
860
+ if (file.data) {
861
+ return sliceArrayBuffer(file.data, file.byteOffset, file.byteLength);
862
+ }
863
+ if (file.bufferUri && this.coreApi) {
864
+ const response = await this.coreApi.fetchFile(this.getTileUrl(file.bufferUri));
865
+ if (!response.ok) {
866
+ throw new Error(`Failed to fetch 3D Tiles package buffer: HTTP ${response.status}`);
867
+ }
868
+ const buffer = await response.arrayBuffer();
869
+ return sliceArrayBuffer(buffer, file.byteOffset, file.byteLength);
870
+ }
871
+ throw new Error('3D Tiles package file has no available byte source');
872
+ }
873
+
525
874
  /**
526
875
  * Returns a parsed subtree from the LRU cache or starts one source-managed request.
527
876
  *
528
877
  * @param subtreeUrl - Final subtree URL after query inheritance.
878
+ * @param packageResource - Optional glTF package file satisfying the subtree reference.
529
879
  * @returns Parsed subtree availability data.
530
880
  */
531
- private async loadImplicitSubtreeResource(subtreeUrl: string): Promise<ParsedImplicitSubtree> {
881
+ private async loadImplicitSubtreeResource(
882
+ subtreeUrl: string,
883
+ packageResource?: PackageResource
884
+ ): Promise<ParsedImplicitSubtree> {
532
885
  const cachedSubtree = this.implicitSubtreeCache.get(subtreeUrl);
533
886
  if (cachedSubtree) {
534
887
  this.implicitTilingStats.cacheHits++;
@@ -538,14 +891,17 @@ export class Tiles3DSource implements Tileset3DSource {
538
891
  this.implicitTilingStats.requestedSubtrees++;
539
892
  const loaderOptions = (this.loadOptions[this.loader.id] as Record<string, unknown>) || {};
540
893
  return await this.implicitSubtreeCache.getOrLoad(subtreeUrl, async () => {
541
- return (await this.loadResourceData(subtreeUrl, {
894
+ const options = {
542
895
  ...this.loadOptions,
543
896
  [this.loader.id]: {
544
897
  ...loaderOptions,
545
898
  isTileset: false,
546
899
  isSubtree: true
547
900
  }
548
- })) as ParsedImplicitSubtree;
901
+ };
902
+ return (await (packageResource
903
+ ? this.loadContentResource(subtreeUrl, packageResource, options)
904
+ : this.loadResourceData(subtreeUrl, options))) as ParsedImplicitSubtree;
549
905
  });
550
906
  }
551
907
 
@@ -610,3 +966,20 @@ function normalizeTiles3DRequest(input: TilesetSourceInput): TilesetSourceReques
610
966
  coreApi: (input as TilesetSourceRequest).coreApi
611
967
  };
612
968
  }
969
+
970
+ /** Adds a nested CRS descriptor to each header without changing its native placement. */
971
+ function addNestedSpatialReference(
972
+ nestedTileset: TilesetJSON,
973
+ spatialReference: TilesetSpatialReference
974
+ ): TilesetJSON {
975
+ const addToHeader = (header: Record<string, any>): Record<string, any> => ({
976
+ ...header,
977
+ _spatialReference: spatialReference,
978
+ children: Array.isArray(header.children) ? header.children.map(addToHeader) : header.children
979
+ });
980
+ return {
981
+ ...nestedTileset,
982
+ spatialMetadata: spatialReference,
983
+ root: addToHeader(nestedTileset.root)
984
+ };
985
+ }