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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +26 -0
  2. package/dist/dist.dev.js +10015 -8172
  3. package/dist/dist.min.js +3 -4
  4. package/dist/index.cjs +1676 -477
  5. package/dist/index.cjs.map +3 -3
  6. package/dist/index.d.ts +10 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +4 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/spatial/format-spatial-reference.d.ts +1 -0
  11. package/dist/spatial/format-spatial-reference.d.ts.map +1 -1
  12. package/dist/spatial/format-spatial-reference.js +104 -46
  13. package/dist/spatial/format-spatial-reference.js.map +1 -1
  14. package/dist/spatial/get-spatial-coordinate-frame.d.ts +12 -1
  15. package/dist/spatial/get-spatial-coordinate-frame.d.ts.map +1 -1
  16. package/dist/spatial/get-spatial-coordinate-frame.js +105 -24
  17. package/dist/spatial/get-spatial-coordinate-frame.js.map +1 -1
  18. package/dist/spatial/normalize-crs-identifier.d.ts +15 -0
  19. package/dist/spatial/normalize-crs-identifier.d.ts.map +1 -0
  20. package/dist/spatial/normalize-crs-identifier.js +22 -0
  21. package/dist/spatial/normalize-crs-identifier.js.map +1 -0
  22. package/dist/spatial/spatial-coordinate-transformer.d.ts +4 -0
  23. package/dist/spatial/spatial-coordinate-transformer.d.ts.map +1 -1
  24. package/dist/spatial/spatial-coordinate-transformer.js +102 -18
  25. package/dist/spatial/spatial-coordinate-transformer.js.map +1 -1
  26. package/dist/spatial/spatial-resource-registry.js +3 -3
  27. package/dist/spatial/spatial-resource-registry.js.map +1 -1
  28. package/dist/spatial/tiles-3d-spatial-transformer.d.ts +32 -0
  29. package/dist/spatial/tiles-3d-spatial-transformer.d.ts.map +1 -0
  30. package/dist/spatial/tiles-3d-spatial-transformer.js +175 -0
  31. package/dist/spatial/tiles-3d-spatial-transformer.js.map +1 -0
  32. package/dist/tileset-3d/common/tile-3d-contracts.d.ts +63 -0
  33. package/dist/tileset-3d/common/tile-3d-contracts.d.ts.map +1 -0
  34. package/dist/tileset-3d/common/tile-3d-contracts.js +51 -0
  35. package/dist/tileset-3d/common/tile-3d-contracts.js.map +1 -0
  36. package/dist/tileset-3d/common/tile-3d.d.ts +33 -5
  37. package/dist/tileset-3d/common/tile-3d.d.ts.map +1 -1
  38. package/dist/tileset-3d/common/tile-3d.js +179 -29
  39. package/dist/tileset-3d/common/tile-3d.js.map +1 -1
  40. package/dist/tileset-3d/common/tileset-3d.d.ts +15 -25
  41. package/dist/tileset-3d/common/tileset-3d.d.ts.map +1 -1
  42. package/dist/tileset-3d/common/tileset-3d.js +35 -42
  43. package/dist/tileset-3d/common/tileset-3d.js.map +1 -1
  44. package/dist/tileset-3d/common/tileset-content-traversal.d.ts +37 -0
  45. package/dist/tileset-3d/common/tileset-content-traversal.d.ts.map +1 -0
  46. package/dist/tileset-3d/common/tileset-content-traversal.js +80 -0
  47. package/dist/tileset-3d/common/tileset-content-traversal.js.map +1 -0
  48. package/dist/tileset-3d/common/tileset-observability.d.ts +60 -0
  49. package/dist/tileset-3d/common/tileset-observability.d.ts.map +1 -0
  50. package/dist/tileset-3d/common/tileset-observability.js +39 -0
  51. package/dist/tileset-3d/common/tileset-observability.js.map +1 -0
  52. package/dist/tileset-3d/common/tileset-source.d.ts +11 -1
  53. package/dist/tileset-3d/common/tileset-source.d.ts.map +1 -1
  54. package/dist/tileset-3d/common/tileset-source.js.map +1 -1
  55. package/dist/tileset-3d/common/tileset-traverser.d.ts.map +1 -1
  56. package/dist/tileset-3d/common/tileset-traverser.js +4 -3
  57. package/dist/tileset-3d/common/tileset-traverser.js.map +1 -1
  58. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts +47 -1
  59. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts.map +1 -1
  60. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js +327 -30
  61. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js.map +1 -1
  62. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts +34 -0
  63. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts.map +1 -1
  64. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js +278 -14
  65. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js.map +1 -1
  66. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts +5 -3
  67. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts.map +1 -1
  68. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js +7 -4
  69. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js.map +1 -1
  70. package/dist/tileset-3d/format-i3s/i3s-source.d.ts +4 -2
  71. package/dist/tileset-3d/format-i3s/i3s-source.d.ts.map +1 -1
  72. package/dist/tileset-3d/format-i3s/i3s-source.js +60 -9
  73. package/dist/tileset-3d/format-i3s/i3s-source.js.map +1 -1
  74. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.d.ts.map +1 -1
  75. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js +1 -1
  76. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js.map +1 -1
  77. package/dist/tileset-3d/helpers/bounding-volume.d.ts +7 -3
  78. package/dist/tileset-3d/helpers/bounding-volume.d.ts.map +1 -1
  79. package/dist/tileset-3d/helpers/bounding-volume.js +19 -61
  80. package/dist/tileset-3d/helpers/bounding-volume.js.map +1 -1
  81. package/dist/tileset-3d/helpers/frame-state.d.ts +5 -0
  82. package/dist/tileset-3d/helpers/frame-state.d.ts.map +1 -1
  83. package/dist/tileset-3d/helpers/frame-state.js +11 -5
  84. package/dist/tileset-3d/helpers/frame-state.js.map +1 -1
  85. package/dist/tileset-3d/helpers/s2-bounding-volume.d.ts +17 -0
  86. package/dist/tileset-3d/helpers/s2-bounding-volume.d.ts.map +1 -0
  87. package/dist/tileset-3d/helpers/s2-bounding-volume.js +20 -0
  88. package/dist/tileset-3d/helpers/s2-bounding-volume.js.map +1 -0
  89. package/dist/types.d.ts +2 -0
  90. package/dist/types.d.ts.map +1 -1
  91. package/package.json +11 -12
  92. package/src/index.ts +18 -1
  93. package/src/spatial/format-spatial-reference.ts +127 -53
  94. package/src/spatial/get-spatial-coordinate-frame.ts +126 -31
  95. package/src/spatial/normalize-crs-identifier.ts +26 -0
  96. package/src/spatial/spatial-coordinate-transformer.ts +124 -24
  97. package/src/spatial/spatial-resource-registry.ts +3 -3
  98. package/src/spatial/tiles-3d-spatial-transformer.ts +206 -0
  99. package/src/tileset-3d/common/tile-3d-contracts.ts +115 -0
  100. package/src/tileset-3d/common/tile-3d.ts +208 -32
  101. package/src/tileset-3d/common/tileset-3d.ts +37 -61
  102. package/src/tileset-3d/common/tileset-content-traversal.ts +114 -0
  103. package/src/tileset-3d/common/tileset-observability.ts +100 -0
  104. package/src/tileset-3d/common/tileset-source.ts +19 -1
  105. package/src/tileset-3d/common/tileset-traverser.ts +4 -3
  106. package/src/tileset-3d/format-3d-tiles/implicit-tiling.ts +481 -34
  107. package/src/tileset-3d/format-3d-tiles/tiles-3d-source.ts +405 -14
  108. package/src/tileset-3d/format-3d-tiles/tileset-3d-traverser.ts +7 -4
  109. package/src/tileset-3d/format-i3s/i3s-source.ts +84 -13
  110. package/src/tileset-3d/format-i3s/i3s-tileset-traverser.ts +2 -1
  111. package/src/tileset-3d/helpers/bounding-volume.ts +29 -62
  112. package/src/tileset-3d/helpers/frame-state.ts +15 -7
  113. package/src/tileset-3d/helpers/s2-bounding-volume.ts +35 -0
  114. package/src/types.ts +2 -0
@@ -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, Loader, LoaderOptions} 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. */
@@ -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;
@@ -346,6 +521,17 @@ export class Tiles3DSource implements Tileset3DSource {
346
521
  };
347
522
  }
348
523
 
524
+ /** Loads an implicit subtree for dataset traversal without camera state. */
525
+ async loadTileChildrenForTraversal(
526
+ tile: Tile3D,
527
+ signal?: AbortSignal
528
+ ): Promise<TileChildrenLoadResult> {
529
+ throwIfTraversalAborted(signal);
530
+ const result = await this.loadTileChildren(tile, {} as FrameState);
531
+ throwIfTraversalAborted(signal);
532
+ return result;
533
+ }
534
+
349
535
  /**
350
536
  * Returns a snapshot of implicit-subtree request, cache, and materialization counters.
351
537
  *
@@ -477,8 +663,97 @@ export class Tiles3DSource implements Tileset3DSource {
477
663
  const nestedTilesets =
478
664
  loadResult.nestedTilesets || (loadResult.nestedTileset ? [loadResult.nestedTileset] : []);
479
665
  for (const nestedTileset of nestedTilesets) {
480
- tileset._initializeTileHeaders(nestedTileset, tile);
666
+ tileset._initializeTileHeaders(this.prepareNestedTileset(tileset, nestedTileset), tile);
667
+ }
668
+ }
669
+
670
+ /**
671
+ * Resolves a nested tileset's CRS independently and adapts it to the owning tileset output frame.
672
+ *
673
+ * Nested resources may use a different source CRS from their parent. Their headers and glTF
674
+ * payloads must therefore use a transformer built from the nested descriptor, while the parent
675
+ * runtime continues to expose one requested output frame to traversal and rendering.
676
+ */
677
+ private prepareNestedTileset(tileset: Tileset3D, nestedTileset: TilesetJSON): TilesetJSON {
678
+ const nestedSpatialReference = nestedTileset.spatialMetadata;
679
+ const parentSpatialReference = tileset.spatialReference;
680
+
681
+ if (
682
+ nestedSpatialReference?.status === 'unresolved' ||
683
+ nestedSpatialReference?.crs.state === 'unknown'
684
+ ) {
685
+ throw new Error(
686
+ nestedSpatialReference.warnings[0] ||
687
+ 'Nested 3D Tiles CRS is explicitly unresolved and cannot be placed in the parent frame'
688
+ );
689
+ }
690
+
691
+ if (!nestedSpatialReference || !nestedSpatialReference.sourceCrs) {
692
+ if (
693
+ this.spatialTransformer &&
694
+ (parentSpatialReference.status === 'transformable' ||
695
+ parentSpatialReference.status === 'transformed')
696
+ ) {
697
+ return {
698
+ ...nestedTileset,
699
+ spatialMetadata: parentSpatialReference,
700
+ root: this.transformTileHeaderWithTransformer(
701
+ nestedTileset.root,
702
+ this.spatialTransformer,
703
+ new Matrix4(),
704
+ parentSpatialReference
705
+ )
706
+ };
707
+ }
708
+ return addNestedSpatialReference(nestedTileset, parentSpatialReference);
709
+ }
710
+
711
+ const parentCoordinateEpoch = parentSpatialReference.coordinateEpoch;
712
+ const nestedCoordinateEpoch = nestedSpatialReference.coordinateEpoch;
713
+ if (
714
+ parentCoordinateEpoch !== undefined &&
715
+ nestedCoordinateEpoch !== undefined &&
716
+ parentCoordinateEpoch !== nestedCoordinateEpoch
717
+ ) {
718
+ throw new Error(
719
+ `Nested 3D Tiles coordinate epoch ${nestedCoordinateEpoch} differs from parent epoch ${parentCoordinateEpoch}; epoch-aware transformation is unavailable`
720
+ );
481
721
  }
722
+
723
+ const outputCrs = parentSpatialReference.targetCrs || parentSpatialReference.sourceCrs;
724
+ if (!outputCrs) {
725
+ return addNestedSpatialReference(nestedTileset, nestedSpatialReference);
726
+ }
727
+
728
+ const nestedOutputReference = applyTilesetSpatialOptions(nestedSpatialReference, {
729
+ ...tileset.options.spatial,
730
+ outputCoordinates: 'target-crs',
731
+ targetCrs: outputCrs
732
+ });
733
+ if (nestedOutputReference.status === 'unresolved') {
734
+ throw new Error(
735
+ nestedOutputReference.warnings[0] ||
736
+ 'Nested 3D Tiles CRS cannot be resolved in the parent output frame'
737
+ );
738
+ }
739
+ if (nestedOutputReference.status === 'native') {
740
+ return addNestedSpatialReference(nestedTileset, nestedOutputReference);
741
+ }
742
+
743
+ const nestedTransformer = new Tiles3DSpatialTransformer(
744
+ nestedOutputReference,
745
+ tileset.options.spatial
746
+ );
747
+ return {
748
+ ...nestedTileset,
749
+ spatialMetadata: nestedOutputReference,
750
+ root: this.transformTileHeaderWithTransformer(
751
+ nestedTileset.root,
752
+ nestedTransformer,
753
+ new Matrix4(),
754
+ nestedOutputReference
755
+ )
756
+ };
482
757
  }
483
758
 
484
759
  /**
@@ -522,13 +797,102 @@ export class Tiles3DSource implements Tileset3DSource {
522
797
  return await this.loadWithCoreApi(url, options, loader);
523
798
  }
524
799
 
800
+ /** Loads a URL content reference or parses a buffer-view file retained from a glTF package. */
801
+ private async loadContentResource(
802
+ url: string,
803
+ packageResource: PackageResource | undefined,
804
+ options: LoaderOptions
805
+ ): Promise<any> {
806
+ if (!packageResource) {
807
+ return await this.loadResourceData(url, options);
808
+ }
809
+ const file = packageResource.files[packageResource.fileIndex];
810
+ if (!file) {
811
+ throw new Error(`3D Tiles package references missing file ${packageResource.fileIndex}`);
812
+ }
813
+ if (file.uri) {
814
+ return await this.loadResourceData(this.getTileUrl(file.uri), options);
815
+ }
816
+ if (!this.coreApi) {
817
+ throw new Error('Tiles3DSource requires an injected coreApi to parse embedded package data');
818
+ }
819
+
820
+ const data = await this.getPackageFileData(file);
821
+ const packageNamespace = this.getPackageNamespace(packageResource.files);
822
+ const packageRootUrl = `gltf-package://${packageNamespace}/${packageResource.fileIndex}`;
823
+ const fileName = file.name || 'content';
824
+ const directorySeparatorIndex = fileName.lastIndexOf('/');
825
+ const packageBaseUrl =
826
+ directorySeparatorIndex >= 0
827
+ ? `${packageRootUrl}/${fileName.slice(0, directorySeparatorIndex)}`
828
+ : packageRootUrl;
829
+ return await this.coreApi.parse(data, this.loader, options, {
830
+ url: `${packageRootUrl}/${fileName}`,
831
+ filename: fileName,
832
+ baseUrl: packageBaseUrl,
833
+ loaders: [this.loader],
834
+ coreApi: this.coreApi,
835
+ _parse: this.coreApi.parse,
836
+ _tiles3dPackageFiles: packageResource.files,
837
+ fetch: async (resource: string, init?: RequestInit) => {
838
+ const reference = resource.startsWith(`${packageRootUrl}/`)
839
+ ? resource.slice(packageRootUrl.length + 1)
840
+ : resource;
841
+ const referencedFile = packageResource.files.find(
842
+ packageFile => packageFile.name === reference || packageFile.originalUri === reference
843
+ );
844
+ if (!referencedFile) {
845
+ throw new Error(`3D Tiles glTF package does not contain file ${reference}`);
846
+ }
847
+ if (referencedFile.uri) {
848
+ return await this.coreApi!.fetchFile(this.getTileUrl(referencedFile.uri), init);
849
+ }
850
+ const referencedData = await this.getPackageFileData(referencedFile);
851
+ return new Response(referencedData, {
852
+ headers: {'content-type': referencedFile.mimeType}
853
+ });
854
+ }
855
+ } as Tiles3DPackageLoaderContext);
856
+ }
857
+
858
+ /** Returns a stable source-local namespace for one embedded glTF package file collection. */
859
+ private getPackageNamespace(files: PackageFile[]): number {
860
+ const existingNamespace = this.packageNamespaces.get(files);
861
+ if (existingNamespace !== undefined) {
862
+ return existingNamespace;
863
+ }
864
+ const packageNamespace = this.nextPackageNamespace++;
865
+ this.packageNamespaces.set(files, packageNamespace);
866
+ return packageNamespace;
867
+ }
868
+
869
+ /** Resolves a retained package-file byte range without eagerly copying other package files. */
870
+ private async getPackageFileData(file: PackageFile): Promise<ArrayBuffer> {
871
+ if (file.data) {
872
+ return sliceArrayBuffer(file.data, file.byteOffset, file.byteLength);
873
+ }
874
+ if (file.bufferUri && this.coreApi) {
875
+ const response = await this.coreApi.fetchFile(this.getTileUrl(file.bufferUri));
876
+ if (!response.ok) {
877
+ throw new Error(`Failed to fetch 3D Tiles package buffer: HTTP ${response.status}`);
878
+ }
879
+ const buffer = await response.arrayBuffer();
880
+ return sliceArrayBuffer(buffer, file.byteOffset, file.byteLength);
881
+ }
882
+ throw new Error('3D Tiles package file has no available byte source');
883
+ }
884
+
525
885
  /**
526
886
  * Returns a parsed subtree from the LRU cache or starts one source-managed request.
527
887
  *
528
888
  * @param subtreeUrl - Final subtree URL after query inheritance.
889
+ * @param packageResource - Optional glTF package file satisfying the subtree reference.
529
890
  * @returns Parsed subtree availability data.
530
891
  */
531
- private async loadImplicitSubtreeResource(subtreeUrl: string): Promise<ParsedImplicitSubtree> {
892
+ private async loadImplicitSubtreeResource(
893
+ subtreeUrl: string,
894
+ packageResource?: PackageResource
895
+ ): Promise<ParsedImplicitSubtree> {
532
896
  const cachedSubtree = this.implicitSubtreeCache.get(subtreeUrl);
533
897
  if (cachedSubtree) {
534
898
  this.implicitTilingStats.cacheHits++;
@@ -538,14 +902,17 @@ export class Tiles3DSource implements Tileset3DSource {
538
902
  this.implicitTilingStats.requestedSubtrees++;
539
903
  const loaderOptions = (this.loadOptions[this.loader.id] as Record<string, unknown>) || {};
540
904
  return await this.implicitSubtreeCache.getOrLoad(subtreeUrl, async () => {
541
- return (await this.loadResourceData(subtreeUrl, {
905
+ const options = {
542
906
  ...this.loadOptions,
543
907
  [this.loader.id]: {
544
908
  ...loaderOptions,
545
909
  isTileset: false,
546
910
  isSubtree: true
547
911
  }
548
- })) as ParsedImplicitSubtree;
912
+ };
913
+ return (await (packageResource
914
+ ? this.loadContentResource(subtreeUrl, packageResource, options)
915
+ : this.loadResourceData(subtreeUrl, options))) as ParsedImplicitSubtree;
549
916
  });
550
917
  }
551
918
 
@@ -583,6 +950,13 @@ export class Tiles3DSource implements Tileset3DSource {
583
950
  }
584
951
  }
585
952
 
953
+ /** Throws a cancellation reason before or after source-managed traversal work. */
954
+ function throwIfTraversalAborted(signal?: AbortSignal): void {
955
+ if (signal?.aborted) {
956
+ throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');
957
+ }
958
+ }
959
+
586
960
  function isTilesetRequest(input: TilesetSourceInput): input is TilesetSourceRequest {
587
961
  return Boolean(
588
962
  input && typeof input === 'object' && 'url' in input && 'loader' in input && !('type' in input)
@@ -610,3 +984,20 @@ function normalizeTiles3DRequest(input: TilesetSourceInput): TilesetSourceReques
610
984
  coreApi: (input as TilesetSourceRequest).coreApi
611
985
  };
612
986
  }
987
+
988
+ /** Adds a nested CRS descriptor to each header without changing its native placement. */
989
+ function addNestedSpatialReference(
990
+ nestedTileset: TilesetJSON,
991
+ spatialReference: TilesetSpatialReference
992
+ ): TilesetJSON {
993
+ const addToHeader = (header: Record<string, any>): Record<string, any> => ({
994
+ ...header,
995
+ _spatialReference: spatialReference,
996
+ children: Array.isArray(header.children) ? header.children.map(addToHeader) : header.children
997
+ });
998
+ return {
999
+ ...nestedTileset,
1000
+ spatialMetadata: spatialReference,
1001
+ root: addToHeader(nestedTileset.root)
1002
+ };
1003
+ }
@@ -274,10 +274,12 @@ export class Tileset3DTraverser extends TilesetTraverser {
274
274
  }
275
275
 
276
276
  /**
277
- * Starts an implicit subtree request only after culling, request-volume, and SSE eligibility.
277
+ * Starts an implicit subtree request after culling and request-volume eligibility.
278
278
  *
279
- * The current parent remains the traversal boundary while metadata is loading, which preserves
280
- * REPLACE coverage and ADD accumulation. Request scheduling reads the tile's existing
279
+ * The first subtree of an implicit hierarchy materializes independently of SSE because its
280
+ * placeholder has no render content. Deeper subtree boundaries remain SSE-gated. The current
281
+ * parent remains the traversal boundary while metadata is loading, which preserves REPLACE
282
+ * coverage and ADD accumulation. Request scheduling reads the tile's existing
281
283
  * progressive-resolution and foveated metrics, so subtree metadata follows the same priority
282
284
  * policy as render content. A completion notification lets the owning tileset publish the newly
283
285
  * materialized traversal result on the next update.
@@ -286,10 +288,11 @@ export class Tileset3DTraverser extends TilesetTraverser {
286
288
  * @param frameState - Current culling and LOD state.
287
289
  */
288
290
  updateChildTiles(tile: Tile3D, frameState: FrameState): void {
291
+ const isImplicitHierarchyRoot = tile.header.implicitSubtree?.coordinates.level === 0;
289
292
  if (
290
293
  tile.hasUnloadedChildren &&
291
294
  tile.isVisibleAndInRequestVolume &&
292
- this.shouldRefine(tile, frameState)
295
+ (isImplicitHierarchyRoot || this.shouldRefine(tile, frameState))
293
296
  ) {
294
297
  const requestKey = `${frameState.viewport.id}:${tile.id}`;
295
298
  if (!this.pendingImplicitSubtrees.has(requestKey)) {