@loaders.gl/tiles 5.0.0-alpha.1 → 5.0.0-alpha.2

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 (115) hide show
  1. package/LICENSE +14 -3
  2. package/dist/dist.dev.js +2396 -274
  3. package/dist/dist.min.js +1 -1
  4. package/dist/index.cjs +2763 -337
  5. package/dist/index.cjs.map +3 -4
  6. package/dist/index.d.ts +7 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/point-cloud/point-cloud-tile.d.ts.map +1 -1
  11. package/dist/point-cloud/point-cloud-tile.js +3 -0
  12. package/dist/point-cloud/point-cloud-tile.js.map +1 -1
  13. package/dist/point-cloud/point-cloud-tileset.d.ts.map +1 -1
  14. package/dist/point-cloud/point-cloud-tileset.js +3 -0
  15. package/dist/point-cloud/point-cloud-tileset.js.map +1 -1
  16. package/dist/point-cloud/types.d.ts.map +1 -1
  17. package/dist/point-cloud/types.js +3 -0
  18. package/dist/point-cloud/types.js.map +1 -1
  19. package/dist/raster-set.d.ts +35 -29
  20. package/dist/raster-set.d.ts.map +1 -1
  21. package/dist/raster-set.js +7 -1
  22. package/dist/raster-set.js.map +1 -1
  23. package/dist/tileset-3d/common/tile-3d.d.ts +98 -3
  24. package/dist/tileset-3d/common/tile-3d.d.ts.map +1 -1
  25. package/dist/tileset-3d/common/tile-3d.js +229 -15
  26. package/dist/tileset-3d/common/tile-3d.js.map +1 -1
  27. package/dist/tileset-3d/common/tileset-3d.d.ts +143 -3
  28. package/dist/tileset-3d/common/tileset-3d.d.ts.map +1 -1
  29. package/dist/tileset-3d/common/tileset-3d.js +257 -16
  30. package/dist/tileset-3d/common/tileset-3d.js.map +1 -1
  31. package/dist/tileset-3d/common/tileset-cache.d.ts +11 -1
  32. package/dist/tileset-3d/common/tileset-cache.d.ts.map +1 -1
  33. package/dist/tileset-3d/common/tileset-cache.js +11 -3
  34. package/dist/tileset-3d/common/tileset-cache.js.map +1 -1
  35. package/dist/tileset-3d/common/tileset-source.d.ts +18 -0
  36. package/dist/tileset-3d/common/tileset-source.d.ts.map +1 -1
  37. package/dist/tileset-3d/common/tileset-source.js.map +1 -1
  38. package/dist/tileset-3d/common/tileset-traverser.d.ts +13 -0
  39. package/dist/tileset-3d/common/tileset-traverser.d.ts.map +1 -1
  40. package/dist/tileset-3d/common/tileset-traverser.js +28 -1
  41. package/dist/tileset-3d/common/tileset-traverser.js.map +1 -1
  42. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts +111 -0
  43. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts.map +1 -0
  44. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js +340 -0
  45. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js.map +1 -0
  46. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts +92 -3
  47. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts.map +1 -1
  48. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js +211 -13
  49. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js.map +1 -1
  50. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts +36 -0
  51. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts.map +1 -1
  52. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js +66 -0
  53. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js.map +1 -1
  54. package/dist/tileset-3d/format-i3s/i3s-pending-tiles-register.d.ts.map +1 -1
  55. package/dist/tileset-3d/format-i3s/i3s-pending-tiles-register.js +3 -0
  56. package/dist/tileset-3d/format-i3s/i3s-pending-tiles-register.js.map +1 -1
  57. package/dist/tileset-3d/format-i3s/i3s-tile-manager.d.ts.map +1 -1
  58. package/dist/tileset-3d/format-i3s/i3s-tile-manager.js +3 -0
  59. package/dist/tileset-3d/format-i3s/i3s-tile-manager.js.map +1 -1
  60. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.d.ts.map +1 -1
  61. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js +3 -0
  62. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js.map +1 -1
  63. package/dist/tileset-3d/helpers/3d-tiles-options.d.ts.map +1 -1
  64. package/dist/tileset-3d/helpers/3d-tiles-options.js +3 -0
  65. package/dist/tileset-3d/helpers/3d-tiles-options.js.map +1 -1
  66. package/dist/tileset-3d/helpers/bounding-volume.d.ts.map +1 -1
  67. package/dist/tileset-3d/helpers/bounding-volume.js +1 -0
  68. package/dist/tileset-3d/helpers/bounding-volume.js.map +1 -1
  69. package/dist/tileset-3d/helpers/frame-state.d.ts +59 -1
  70. package/dist/tileset-3d/helpers/frame-state.d.ts.map +1 -1
  71. package/dist/tileset-3d/helpers/frame-state.js +84 -9
  72. package/dist/tileset-3d/helpers/frame-state.js.map +1 -1
  73. package/dist/tileset-3d/helpers/tiles-3d-lod.d.ts +61 -9
  74. package/dist/tileset-3d/helpers/tiles-3d-lod.d.ts.map +1 -1
  75. package/dist/tileset-3d/helpers/tiles-3d-lod.js +198 -93
  76. package/dist/tileset-3d/helpers/tiles-3d-lod.js.map +1 -1
  77. package/dist/tileset-3d/helpers/tiles-3d-request-priority.d.ts +134 -0
  78. package/dist/tileset-3d/helpers/tiles-3d-request-priority.d.ts.map +1 -0
  79. package/dist/tileset-3d/helpers/tiles-3d-request-priority.js +179 -0
  80. package/dist/tileset-3d/helpers/tiles-3d-request-priority.js.map +1 -0
  81. package/dist/types.d.ts +2 -0
  82. package/dist/types.d.ts.map +1 -1
  83. package/dist/types.js +3 -0
  84. package/dist/types.js.map +1 -1
  85. package/dist/utils/doubly-linked-list.d.ts.map +1 -1
  86. package/dist/utils/doubly-linked-list.js +1 -0
  87. package/dist/utils/doubly-linked-list.js.map +1 -1
  88. package/dist/utils/managed-array.d.ts.map +1 -1
  89. package/dist/utils/managed-array.js +1 -0
  90. package/dist/utils/managed-array.js.map +1 -1
  91. package/package.json +5 -5
  92. package/src/index.ts +19 -0
  93. package/src/point-cloud/point-cloud-tile.ts +4 -0
  94. package/src/point-cloud/point-cloud-tileset.ts +4 -0
  95. package/src/point-cloud/types.ts +4 -0
  96. package/src/raster-set.ts +81 -43
  97. package/src/tileset-3d/common/tile-3d.ts +285 -21
  98. package/src/tileset-3d/common/tileset-3d.ts +368 -17
  99. package/src/tileset-3d/common/tileset-cache.ts +15 -7
  100. package/src/tileset-3d/common/tileset-source.ts +21 -0
  101. package/src/tileset-3d/common/tileset-traverser.ts +29 -1
  102. package/src/tileset-3d/format-3d-tiles/implicit-tiling.ts +534 -0
  103. package/src/tileset-3d/format-3d-tiles/tiles-3d-source.ts +266 -13
  104. package/src/tileset-3d/format-3d-tiles/tileset-3d-traverser.ts +77 -0
  105. package/src/tileset-3d/format-i3s/i3s-pending-tiles-register.ts +4 -0
  106. package/src/tileset-3d/format-i3s/i3s-tile-manager.ts +4 -0
  107. package/src/tileset-3d/format-i3s/i3s-tileset-traverser.ts +4 -0
  108. package/src/tileset-3d/helpers/3d-tiles-options.ts +4 -0
  109. package/src/tileset-3d/helpers/bounding-volume.ts +2 -0
  110. package/src/tileset-3d/helpers/frame-state.ts +139 -11
  111. package/src/tileset-3d/helpers/tiles-3d-lod.ts +291 -112
  112. package/src/tileset-3d/helpers/tiles-3d-request-priority.ts +301 -0
  113. package/src/types.ts +6 -0
  114. package/src/utils/doubly-linked-list.ts +2 -0
  115. package/src/utils/managed-array.ts +2 -0
package/dist/dist.dev.js CHANGED
@@ -13,7 +13,11 @@ var __exports__ = (() => {
13
13
  var __getProtoOf = Object.getPrototypeOf;
14
14
  var __hasOwnProp = Object.prototype.hasOwnProperty;
15
15
  var __commonJS = (cb, mod) => function __require() {
16
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
16
+ try {
17
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
18
+ } catch (e) {
19
+ throw mod = 0, e;
20
+ }
17
21
  };
18
22
  var __export = (target, all) => {
19
23
  for (var name in all)
@@ -71,9 +75,12 @@ var __exports__ = (() => {
71
75
  TilesetTraverser: () => TilesetTraverser,
72
76
  calculateTransformProps: () => calculateTransformProps,
73
77
  createBoundingVolume: () => createBoundingVolume,
78
+ createImplicitSubtreeReference: () => createImplicitSubtreeReference,
74
79
  getFrameState: () => getFrameState,
75
80
  getLodStatus: () => getLodStatus,
76
- isTileset3DSource: () => isTileset3DSource
81
+ isTileset3DSource: () => isTileset3DSource,
82
+ materializeImplicitSubtree: () => materializeImplicitSubtree,
83
+ replaceImplicitUrlTemplate: () => replaceImplicitUrlTemplate
77
84
  });
78
85
  __reExport(bundle_exports, __toESM(require_core(), 1));
79
86
 
@@ -110,6 +117,9 @@ var __exports__ = (() => {
110
117
  function degrees(radians2, result) {
111
118
  return map(radians2, (radians3) => radians3 * RADIANS_TO_DEGREES, result);
112
119
  }
120
+ function clamp(value, min2, max2) {
121
+ return map(value, (value2) => Math.max(min2, Math.min(max2, value2)));
122
+ }
113
123
  function equals(a, b, epsilon) {
114
124
  const oldEpsilon = config.EPSILON;
115
125
  if (epsilon) {
@@ -569,7 +579,7 @@ var __exports__ = (() => {
569
579
  out[1] = m[1] * x + m[5] * y + m[13];
570
580
  return out;
571
581
  }
572
- var forEach = function() {
582
+ var forEach = (function() {
573
583
  const vec = create();
574
584
  return function(a, stride, offset, count, fn, arg) {
575
585
  let i;
@@ -594,7 +604,7 @@ var __exports__ = (() => {
594
604
  }
595
605
  return a;
596
606
  };
597
- }();
607
+ })();
598
608
 
599
609
  // ../../node_modules/@math.gl/core/dist/lib/gl-matrix-extras.js
600
610
  function vec2_transformMat4AsVector(out, a, m) {
@@ -1041,7 +1051,7 @@ var __exports__ = (() => {
1041
1051
  var sqrDist = squaredDistance;
1042
1052
  var len = length;
1043
1053
  var sqrLen = squaredLength;
1044
- var forEach2 = function() {
1054
+ var forEach2 = (function() {
1045
1055
  const vec = create2();
1046
1056
  return function(a, stride, offset, count, fn, arg) {
1047
1057
  let i;
@@ -1068,15 +1078,15 @@ var __exports__ = (() => {
1068
1078
  }
1069
1079
  return a;
1070
1080
  };
1071
- }();
1081
+ })();
1072
1082
 
1073
1083
  // ../../node_modules/@math.gl/core/dist/classes/vector3.js
1074
1084
  var ORIGIN = [0, 0, 0];
1075
1085
  var ZERO;
1076
- var Vector3 = class extends Vector {
1086
+ var Vector3 = class _Vector3 extends Vector {
1077
1087
  static get ZERO() {
1078
1088
  if (!ZERO) {
1079
- ZERO = new Vector3(0, 0, 0);
1089
+ ZERO = new _Vector3(0, 0, 0);
1080
1090
  Object.freeze(ZERO);
1081
1091
  }
1082
1092
  return ZERO;
@@ -1193,10 +1203,10 @@ var __exports__ = (() => {
1193
1203
 
1194
1204
  // ../../node_modules/@math.gl/core/dist/classes/vector4.js
1195
1205
  var ZERO2;
1196
- var Vector4 = class extends Vector {
1206
+ var Vector4 = class _Vector4 extends Vector {
1197
1207
  static get ZERO() {
1198
1208
  if (!ZERO2) {
1199
- ZERO2 = new Vector4(0, 0, 0, 0);
1209
+ ZERO2 = new _Vector4(0, 0, 0, 0);
1200
1210
  Object.freeze(ZERO2);
1201
1211
  }
1202
1212
  return ZERO2;
@@ -3310,7 +3320,7 @@ var __exports__ = (() => {
3310
3320
  out[3] = a[3];
3311
3321
  return out;
3312
3322
  }
3313
- var forEach3 = function() {
3323
+ var forEach3 = (function() {
3314
3324
  const vec = create5();
3315
3325
  return function(a, stride, offset, count, fn, arg) {
3316
3326
  let i;
@@ -3339,7 +3349,7 @@ var __exports__ = (() => {
3339
3349
  }
3340
3350
  return a;
3341
3351
  };
3342
- }();
3352
+ })();
3343
3353
 
3344
3354
  // ../../node_modules/@math.gl/core/dist/classes/matrix4.js
3345
3355
  var INDICES2;
@@ -4045,7 +4055,7 @@ var __exports__ = (() => {
4045
4055
  var length3 = length2;
4046
4056
  var squaredLength3 = squaredLength2;
4047
4057
  var normalize3 = normalize2;
4048
- var rotationTo = function() {
4058
+ var rotationTo = (function() {
4049
4059
  const tmpvec3 = create2();
4050
4060
  const xUnitVec3 = fromValues(1, 0, 0);
4051
4061
  const yUnitVec3 = fromValues(0, 1, 0);
@@ -4072,8 +4082,8 @@ var __exports__ = (() => {
4072
4082
  out[3] = 1 + dot4;
4073
4083
  return normalize3(out, out);
4074
4084
  };
4075
- }();
4076
- var sqlerp = function() {
4085
+ })();
4086
+ var sqlerp = (function() {
4077
4087
  const temp1 = create6();
4078
4088
  const temp2 = create6();
4079
4089
  return function(out, a, b, c, d, t) {
@@ -4082,8 +4092,8 @@ var __exports__ = (() => {
4082
4092
  slerp2(out, temp1, temp2, 2 * t * (1 - t));
4083
4093
  return out;
4084
4094
  };
4085
- }();
4086
- var setAxes = function() {
4095
+ })();
4096
+ var setAxes = (function() {
4087
4097
  const matr = create3();
4088
4098
  return function(out, view, right, up) {
4089
4099
  matr[0] = right[0];
@@ -4097,7 +4107,7 @@ var __exports__ = (() => {
4097
4107
  matr[8] = -view[2];
4098
4108
  return normalize3(out, fromMat3(out, matr));
4099
4109
  };
4100
- }();
4110
+ })();
4101
4111
 
4102
4112
  // ../../node_modules/@math.gl/core/dist/classes/quaternion.js
4103
4113
  var IDENTITY_QUATERNION = [0, 0, 0, 1];
@@ -5106,6 +5116,506 @@ var __exports__ = (() => {
5106
5116
  }
5107
5117
  };
5108
5118
 
5119
+ // ../loader-utils/src/lib/request-utils/range-request-scheduler.ts
5120
+ var RANGE_STATS_KEYS = {
5121
+ logicalRanges: "Logical Range Requests",
5122
+ rangeBatches: "Range Request Batches",
5123
+ transportRanges: "Transport Ranges Created",
5124
+ completedTransportRanges: "Range Transport Requests Completed",
5125
+ coalescedRanges: "Coalesced Logical Ranges",
5126
+ requestedBytes: "Logical Range Request Bytes",
5127
+ transportBytes: "Range Transport Bytes Requested",
5128
+ responseBytes: "Range Response Bytes",
5129
+ networkTimeMs: "Range Network Time",
5130
+ overfetchBytes: "Range Overfetch Bytes",
5131
+ failedTransportRanges: "Range Transport Requests Failed",
5132
+ abortedLogicalRanges: "Aborted Logical Range Requests",
5133
+ fullResponseFallbacks: "Full Response Fallbacks"
5134
+ };
5135
+ var DEFAULT_PROPS2 = {
5136
+ batchDelayMs: 50,
5137
+ maxGapBytes: 65536,
5138
+ rangeExpansionBytes: 65536,
5139
+ maxMergedBytes: 8388608,
5140
+ stats: createRangeStats("range-request-scheduler-default"),
5141
+ onEvent: () => {
5142
+ }
5143
+ };
5144
+ var RangeRequestScheduler = class {
5145
+ /** Runtime scheduler configuration with defaults filled in. */
5146
+ props;
5147
+ /** Runtime counters for range batching and transport behavior. */
5148
+ stats;
5149
+ /** Optional event callback for range batching diagnostics. */
5150
+ onEvent;
5151
+ pendingRequests = [];
5152
+ flushTimer = null;
5153
+ /** Creates a scheduler for one group of byte-range-addressable resources. */
5154
+ constructor(props = {}) {
5155
+ const rangeExpansionBytes = props.rangeExpansionBytes ?? props.maxGapBytes ?? DEFAULT_PROPS2.rangeExpansionBytes;
5156
+ this.props = {
5157
+ batchDelayMs: props.batchDelayMs ?? DEFAULT_PROPS2.batchDelayMs,
5158
+ maxGapBytes: props.maxGapBytes ?? rangeExpansionBytes,
5159
+ rangeExpansionBytes,
5160
+ maxMergedBytes: props.maxMergedBytes ?? DEFAULT_PROPS2.maxMergedBytes
5161
+ };
5162
+ this.stats = props.stats || createRangeStats("range-request-scheduler");
5163
+ this.onEvent = props.onEvent;
5164
+ initializeStats(this.stats);
5165
+ }
5166
+ /**
5167
+ * Enqueues one byte range request and resolves with the exact requested byte slice.
5168
+ */
5169
+ scheduleRequest(request) {
5170
+ if (request.length < 0) {
5171
+ return Promise.reject(new Error("Byte range length cannot be negative"));
5172
+ }
5173
+ if (request.signal?.aborted) {
5174
+ return Promise.reject(createAbortError());
5175
+ }
5176
+ if (request.length === 0) {
5177
+ return Promise.resolve(new ArrayBuffer(0));
5178
+ }
5179
+ const promise = new Promise((resolve2, reject) => {
5180
+ const pendingRequest = { ...request, resolve: resolve2, reject, settled: false };
5181
+ if (request.signal) {
5182
+ pendingRequest.abortListener = () => {
5183
+ this.abortRequest(pendingRequest);
5184
+ pendingRequest.onAbort?.();
5185
+ };
5186
+ request.signal.addEventListener("abort", pendingRequest.abortListener, { once: true });
5187
+ }
5188
+ this.pendingRequests.push(pendingRequest);
5189
+ });
5190
+ this.trackEvent({
5191
+ type: "queued",
5192
+ sourceId: request.sourceId,
5193
+ offset: request.offset,
5194
+ length: request.length,
5195
+ logicalRequestCount: 1,
5196
+ logicalBytes: request.length
5197
+ });
5198
+ this.scheduleFlush();
5199
+ return promise;
5200
+ }
5201
+ /**
5202
+ * Enqueues one HTTP byte-range request and resolves with the exact requested byte slice.
5203
+ */
5204
+ fetch(request) {
5205
+ const fetchFunction = request.fetch || globalThis.fetch;
5206
+ return this.scheduleRequest({
5207
+ sourceId: request.sourceId || request.url,
5208
+ offset: request.offset,
5209
+ length: request.length,
5210
+ signal: request.signal,
5211
+ // Different fetch calls may carry different credentials or transport implementations.
5212
+ // Sharing is therefore opt-in through an explicit common isolation key.
5213
+ isolationKey: request.isolationKey ?? {},
5214
+ fetchRange: (offset, length4, signal) => fetchHttpRange({
5215
+ url: request.url,
5216
+ offset,
5217
+ length: length4,
5218
+ signal,
5219
+ fetch: fetchFunction,
5220
+ fetchOptions: request.fetchOptions
5221
+ })
5222
+ });
5223
+ }
5224
+ /** Immediately starts loading the currently queued requests. */
5225
+ flush() {
5226
+ if (this.flushTimer) {
5227
+ clearTimeout(this.flushTimer);
5228
+ this.flushTimer = null;
5229
+ }
5230
+ const pendingRequests = this.pendingRequests;
5231
+ this.pendingRequests = [];
5232
+ const activeRequests = pendingRequests.filter((request) => {
5233
+ if (request.settled || request.signal?.aborted) {
5234
+ this.abortRequest(request);
5235
+ return false;
5236
+ }
5237
+ return true;
5238
+ });
5239
+ const mergedRequests = this.mergeRequests(activeRequests);
5240
+ if (activeRequests.length > 0) {
5241
+ this.trackBatch(activeRequests, mergedRequests);
5242
+ }
5243
+ for (const mergedRequest of mergedRequests) {
5244
+ this.fetchMergedRequest(mergedRequest);
5245
+ }
5246
+ }
5247
+ /** Schedules the next pending-request flush. */
5248
+ scheduleFlush() {
5249
+ if (this.flushTimer) {
5250
+ return;
5251
+ }
5252
+ this.flushTimer = setTimeout(() => this.flush(), this.props.batchDelayMs);
5253
+ }
5254
+ /** Sorts active requests and combines requests for the same source when thresholds allow. */
5255
+ mergeRequests(requests) {
5256
+ requests.sort((left, right) => {
5257
+ if (left.sourceId !== right.sourceId) {
5258
+ return left.sourceId < right.sourceId ? -1 : 1;
5259
+ }
5260
+ return left.offset - right.offset;
5261
+ });
5262
+ const mergedRequests = [];
5263
+ for (const request of requests) {
5264
+ const requestEndOffset = request.offset + request.length;
5265
+ const lastMergedRequest = mergedRequests[mergedRequests.length - 1];
5266
+ if (lastMergedRequest && canMergeRequests(lastMergedRequest, request, this.props)) {
5267
+ lastMergedRequest.endOffset = Math.max(lastMergedRequest.endOffset, requestEndOffset);
5268
+ lastMergedRequest.requests.push(request);
5269
+ } else {
5270
+ mergedRequests.push({
5271
+ sourceId: request.sourceId,
5272
+ isolationKey: request.isolationKey ?? request.sourceId,
5273
+ offset: request.offset,
5274
+ endOffset: requestEndOffset,
5275
+ fetchRange: request.fetchRange,
5276
+ requests: [request]
5277
+ });
5278
+ }
5279
+ }
5280
+ return mergedRequests;
5281
+ }
5282
+ /** Loads one merged range and resolves each child request with its sliced bytes. */
5283
+ async fetchMergedRequest(mergedRequest) {
5284
+ const { offset, endOffset, fetchRange, requests } = mergedRequest;
5285
+ const abortController = new AbortController();
5286
+ const requestStartTime = getTimestamp();
5287
+ const abortTransportIfUnused = () => {
5288
+ if (requests.every((request) => request.signal?.aborted)) {
5289
+ abortController.abort();
5290
+ }
5291
+ };
5292
+ try {
5293
+ for (const request of requests) {
5294
+ request.onAbort = abortTransportIfUnused;
5295
+ }
5296
+ abortTransportIfUnused();
5297
+ if (requests.every((request) => request.settled)) {
5298
+ return;
5299
+ }
5300
+ const logicalBytes = getLogicalRequestBytes(requests);
5301
+ const length4 = endOffset - offset;
5302
+ this.trackEvent({
5303
+ type: "request",
5304
+ sourceId: mergedRequest.sourceId,
5305
+ offset,
5306
+ length: length4,
5307
+ logicalRequestCount: requests.length,
5308
+ logicalBytes,
5309
+ transportBytes: length4,
5310
+ overfetchBytes: Math.max(length4 - logicalBytes, 0)
5311
+ });
5312
+ const transportResult = normalizeTransportResult(
5313
+ await fetchRange(offset, length4, abortController.signal)
5314
+ );
5315
+ const { arrayBuffer } = transportResult;
5316
+ const isClampedFullResponse = transportResult.fullResponse && offset === 0 && arrayBuffer.byteLength < length4;
5317
+ const isClampedEndOfFile = arrayBuffer.byteLength < length4 && Number.isSafeInteger(transportResult.sourceByteLength) && offset + arrayBuffer.byteLength === transportResult.sourceByteLength;
5318
+ if (arrayBuffer.byteLength !== length4 && !isClampedFullResponse && !isClampedEndOfFile) {
5319
+ throw new Error(
5320
+ `Byte-range transport returned ${arrayBuffer.byteLength} bytes; expected ${length4}`
5321
+ );
5322
+ }
5323
+ const networkTimeMs = transportResult.networkTimeMs ?? getTimestamp() - requestStartTime;
5324
+ this.trackEvent({
5325
+ type: "response",
5326
+ sourceId: mergedRequest.sourceId,
5327
+ offset,
5328
+ length: length4,
5329
+ logicalRequestCount: requests.length,
5330
+ logicalBytes,
5331
+ transportBytes: length4,
5332
+ responseBytes: transportResult.transportBytes ?? arrayBuffer.byteLength,
5333
+ networkTimeMs,
5334
+ overfetchBytes: Math.max(length4 - logicalBytes, 0),
5335
+ status: transportResult.status,
5336
+ fullResponse: transportResult.fullResponse
5337
+ });
5338
+ for (const request of requests) {
5339
+ if (request.settled) {
5340
+ continue;
5341
+ }
5342
+ const start = request.offset - offset;
5343
+ request.settled = true;
5344
+ request.resolve(arrayBuffer.slice(start, start + request.length));
5345
+ }
5346
+ } catch (error) {
5347
+ this.trackEvent({
5348
+ type: "error",
5349
+ sourceId: mergedRequest.sourceId,
5350
+ offset,
5351
+ length: endOffset - offset,
5352
+ logicalRequestCount: requests.length,
5353
+ logicalBytes: getLogicalRequestBytes(requests),
5354
+ networkTimeMs: getTimestamp() - requestStartTime,
5355
+ error
5356
+ });
5357
+ for (const request of requests) {
5358
+ if (request.settled) {
5359
+ continue;
5360
+ }
5361
+ if (request.signal?.aborted) {
5362
+ this.abortRequest(request);
5363
+ } else {
5364
+ request.settled = true;
5365
+ request.reject(error);
5366
+ }
5367
+ }
5368
+ } finally {
5369
+ for (const request of requests) {
5370
+ if (request.abortListener) {
5371
+ request.signal?.removeEventListener("abort", request.abortListener);
5372
+ }
5373
+ request.onAbort = void 0;
5374
+ }
5375
+ }
5376
+ }
5377
+ /** Rejects and tracks one logical request exactly once when its caller aborts. */
5378
+ abortRequest(request) {
5379
+ if (request.settled) {
5380
+ return;
5381
+ }
5382
+ request.settled = true;
5383
+ request.reject(createAbortError());
5384
+ this.trackEvent({
5385
+ type: "abort",
5386
+ sourceId: request.sourceId,
5387
+ offset: request.offset,
5388
+ length: request.length,
5389
+ logicalRequestCount: 1,
5390
+ logicalBytes: request.length
5391
+ });
5392
+ }
5393
+ /** Emits one event to Stats and to the optional callback. */
5394
+ trackEvent(event) {
5395
+ trackStatsEvent(this.stats, event);
5396
+ try {
5397
+ this.onEvent?.(event);
5398
+ } catch {
5399
+ }
5400
+ }
5401
+ /** Emits one batch event after logical requests have been merged. */
5402
+ trackBatch(activeRequests, mergedRequests) {
5403
+ const logicalBytes = getLogicalRequestBytes(activeRequests);
5404
+ const transportBytes = mergedRequests.reduce(
5405
+ (sum, mergedRequest) => sum + mergedRequest.endOffset - mergedRequest.offset,
5406
+ 0
5407
+ );
5408
+ this.trackEvent({
5409
+ type: "batch",
5410
+ logicalRequestCount: activeRequests.length,
5411
+ transportRequestCount: mergedRequests.length,
5412
+ logicalBytes,
5413
+ transportBytes,
5414
+ overfetchBytes: Math.max(transportBytes - logicalBytes, 0)
5415
+ });
5416
+ }
5417
+ };
5418
+ function trackStatsEvent(stats, event) {
5419
+ if ((event.type === "response" || event.type === "error") && event.networkTimeMs !== void 0) {
5420
+ stats.get(RANGE_STATS_KEYS.networkTimeMs, "time").addTime(event.networkTimeMs);
5421
+ }
5422
+ switch (event.type) {
5423
+ case "queued":
5424
+ stats.get(RANGE_STATS_KEYS.logicalRanges, "count").incrementCount();
5425
+ stats.get(RANGE_STATS_KEYS.requestedBytes, "count").addCount(event.logicalBytes || 0);
5426
+ break;
5427
+ case "batch":
5428
+ stats.get(RANGE_STATS_KEYS.rangeBatches, "count").incrementCount();
5429
+ stats.get("Logical Ranges Batched", "count").addCount(event.logicalRequestCount || 0);
5430
+ stats.get(RANGE_STATS_KEYS.transportRanges, "count").addCount(event.transportRequestCount || 0);
5431
+ stats.get(RANGE_STATS_KEYS.coalescedRanges, "count").addCount(
5432
+ Math.max((event.logicalRequestCount || 0) - (event.transportRequestCount || 0), 0)
5433
+ );
5434
+ stats.get(RANGE_STATS_KEYS.transportBytes, "count").addCount(event.transportBytes || 0);
5435
+ stats.get(RANGE_STATS_KEYS.overfetchBytes, "count").addCount(event.overfetchBytes || 0);
5436
+ break;
5437
+ case "response":
5438
+ stats.get(RANGE_STATS_KEYS.completedTransportRanges, "count").incrementCount();
5439
+ stats.get(RANGE_STATS_KEYS.responseBytes, "count").addCount(event.responseBytes || 0);
5440
+ if (event.fullResponse) {
5441
+ stats.get(RANGE_STATS_KEYS.fullResponseFallbacks, "count").incrementCount();
5442
+ }
5443
+ break;
5444
+ case "error":
5445
+ stats.get(RANGE_STATS_KEYS.failedTransportRanges, "count").incrementCount();
5446
+ break;
5447
+ case "abort":
5448
+ stats.get(RANGE_STATS_KEYS.abortedLogicalRanges, "count").incrementCount();
5449
+ break;
5450
+ default:
5451
+ }
5452
+ }
5453
+ function createRangeStats(id = "range-request-scheduler") {
5454
+ const stats = new Stats({ id });
5455
+ initializeStats(stats);
5456
+ return stats;
5457
+ }
5458
+ async function fetchHttpRange(request) {
5459
+ const abortContext = createAbortableFetchContext(request.signal);
5460
+ const requestStartTime = getTimestamp();
5461
+ try {
5462
+ let fullResponse = false;
5463
+ let response = await request.fetch(
5464
+ request.url,
5465
+ createRangeFetchOptions(
5466
+ request.fetchOptions,
5467
+ request.offset,
5468
+ request.length,
5469
+ abortContext.signal
5470
+ )
5471
+ );
5472
+ if (response.status === 416 && request.offset === 0) {
5473
+ const actualLength = parseUnsatisfiedContentRange(response.headers.get("Content-Range"));
5474
+ if (!actualLength) {
5475
+ throw new Error("Missing content-length on 416 response");
5476
+ }
5477
+ response = await request.fetch(
5478
+ request.url,
5479
+ createRangeFetchOptions(request.fetchOptions, 0, actualLength, abortContext.signal)
5480
+ );
5481
+ fullResponse = true;
5482
+ }
5483
+ if (response.status === 200) {
5484
+ abortContext.abort();
5485
+ await cancelIgnoredRangeResponse(response);
5486
+ throw new Error("Byte-range request failed: server returned 200 instead of 206");
5487
+ }
5488
+ if (response.status !== 206 && !response.ok) {
5489
+ throw new Error(`Bad response code: ${response.status}`);
5490
+ }
5491
+ const arrayBuffer = await response.arrayBuffer();
5492
+ const contentRangeHeader = response.headers.get("Content-Range");
5493
+ const contentRange = parseSatisfiedContentRange(contentRangeHeader);
5494
+ if (contentRangeHeader && !contentRange) {
5495
+ throw new Error(`Invalid Content-Range header: ${contentRangeHeader}`);
5496
+ }
5497
+ if (contentRange) {
5498
+ const responseLength = contentRange.endOffset - contentRange.offset + 1;
5499
+ const requestedEndOffset = request.offset + request.length - 1;
5500
+ if (contentRange.offset !== request.offset || contentRange.endOffset > requestedEndOffset || responseLength !== arrayBuffer.byteLength || contentRange.sourceByteLength !== void 0 && contentRange.endOffset >= contentRange.sourceByteLength) {
5501
+ throw new Error(`Content-Range does not match requested range: ${contentRangeHeader}`);
5502
+ }
5503
+ }
5504
+ return {
5505
+ arrayBuffer,
5506
+ status: response.status,
5507
+ sourceByteLength: contentRange?.sourceByteLength,
5508
+ transportBytes: arrayBuffer.byteLength,
5509
+ networkTimeMs: getTimestamp() - requestStartTime,
5510
+ fullResponse
5511
+ };
5512
+ } finally {
5513
+ abortContext.removeAbortListener();
5514
+ }
5515
+ }
5516
+ function canMergeRequests(mergedRequest, request, props) {
5517
+ if (mergedRequest.sourceId !== request.sourceId) {
5518
+ return false;
5519
+ }
5520
+ if (mergedRequest.isolationKey !== (request.isolationKey ?? request.sourceId)) {
5521
+ return false;
5522
+ }
5523
+ const requestEndOffset = request.offset + request.length;
5524
+ const gapBytes = request.offset - mergedRequest.endOffset;
5525
+ const mergedBytes = Math.max(mergedRequest.endOffset, requestEndOffset) - mergedRequest.offset;
5526
+ return gapBytes <= props.rangeExpansionBytes && mergedBytes <= props.maxMergedBytes;
5527
+ }
5528
+ function initializeStats(stats) {
5529
+ stats.get(RANGE_STATS_KEYS.logicalRanges, "count");
5530
+ stats.get(RANGE_STATS_KEYS.requestedBytes, "count");
5531
+ stats.get(RANGE_STATS_KEYS.rangeBatches, "count");
5532
+ stats.get("Logical Ranges Batched", "count");
5533
+ stats.get(RANGE_STATS_KEYS.transportRanges, "count");
5534
+ stats.get(RANGE_STATS_KEYS.coalescedRanges, "count");
5535
+ stats.get(RANGE_STATS_KEYS.transportBytes, "count");
5536
+ stats.get(RANGE_STATS_KEYS.completedTransportRanges, "count");
5537
+ stats.get(RANGE_STATS_KEYS.responseBytes, "count");
5538
+ stats.get(RANGE_STATS_KEYS.networkTimeMs, "time");
5539
+ stats.get(RANGE_STATS_KEYS.overfetchBytes, "count");
5540
+ stats.get(RANGE_STATS_KEYS.fullResponseFallbacks, "count");
5541
+ stats.get(RANGE_STATS_KEYS.failedTransportRanges, "count");
5542
+ stats.get(RANGE_STATS_KEYS.abortedLogicalRanges, "count");
5543
+ }
5544
+ function getLogicalRequestBytes(requests) {
5545
+ return requests.reduce((sum, request) => sum + request.length, 0);
5546
+ }
5547
+ function normalizeTransportResult(result) {
5548
+ return result instanceof ArrayBuffer ? { arrayBuffer: result } : result;
5549
+ }
5550
+ function createRangeFetchOptions(fetchOptions, offset, length4, signal) {
5551
+ return {
5552
+ ...fetchOptions,
5553
+ signal,
5554
+ headers: {
5555
+ ...getHeadersObject(fetchOptions?.headers),
5556
+ Range: `bytes=${offset}-${offset + length4 - 1}`
5557
+ }
5558
+ };
5559
+ }
5560
+ function getHeadersObject(headers) {
5561
+ if (!headers) {
5562
+ return {};
5563
+ }
5564
+ if (headers instanceof Headers) {
5565
+ return Object.fromEntries(headers.entries());
5566
+ }
5567
+ if (Array.isArray(headers)) {
5568
+ return Object.fromEntries(headers);
5569
+ }
5570
+ return headers;
5571
+ }
5572
+ function parseUnsatisfiedContentRange(contentRange) {
5573
+ const match = contentRange?.match(/^bytes \*\/(\d+)$/);
5574
+ return match ? Number(match[1]) : null;
5575
+ }
5576
+ function parseSatisfiedContentRange(contentRange) {
5577
+ const match = contentRange?.match(/^bytes (\d+)-(\d+)\/(\d+|\*)$/);
5578
+ if (!match) {
5579
+ return null;
5580
+ }
5581
+ const offset = Number(match[1]);
5582
+ const endOffset = Number(match[2]);
5583
+ const sourceByteLength = match[3] === "*" ? void 0 : Number(match[3]);
5584
+ if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(endOffset) || offset > endOffset || sourceByteLength !== void 0 && !Number.isSafeInteger(sourceByteLength)) {
5585
+ return null;
5586
+ }
5587
+ return { offset, endOffset, sourceByteLength };
5588
+ }
5589
+ function createAbortableFetchContext(parentSignal) {
5590
+ const abortController = new AbortController();
5591
+ const abortListener = () => abortController.abort();
5592
+ if (parentSignal?.aborted) {
5593
+ abortController.abort();
5594
+ } else {
5595
+ parentSignal?.addEventListener("abort", abortListener, { once: true });
5596
+ }
5597
+ return {
5598
+ signal: abortController.signal,
5599
+ abort: () => abortController.abort(),
5600
+ removeAbortListener: () => parentSignal?.removeEventListener("abort", abortListener)
5601
+ };
5602
+ }
5603
+ async function cancelIgnoredRangeResponse(response) {
5604
+ await response.body?.cancel().catch(() => {
5605
+ });
5606
+ }
5607
+ function getTimestamp() {
5608
+ return globalThis.performance?.now() ?? Date.now();
5609
+ }
5610
+ function createAbortError() {
5611
+ if (typeof DOMException !== "undefined") {
5612
+ return new DOMException("Request aborted", "AbortError");
5613
+ }
5614
+ const error = new Error("Request aborted");
5615
+ error.name = "AbortError";
5616
+ return error;
5617
+ }
5618
+
5109
5619
  // ../loader-utils/src/lib/path-utils/path.ts
5110
5620
  var path_exports = {};
5111
5621
  __export(path_exports, {
@@ -5270,97 +5780,513 @@ var __exports__ = (() => {
5270
5780
  isDirectory: false
5271
5781
  };
5272
5782
  }
5273
- async read(start, length4) {
5783
+ async read(start, length4, signal) {
5784
+ if (signal?.aborted) {
5785
+ throw new Error("Request aborted");
5786
+ }
5274
5787
  const arrayBuffer = await this.handle.slice(Number(start), Number(start) + Number(length4)).arrayBuffer();
5788
+ if (signal?.aborted) {
5789
+ throw new Error("Request aborted");
5790
+ }
5275
5791
  return arrayBuffer;
5276
5792
  }
5277
5793
  };
5278
5794
 
5795
+ // ../loader-utils/src/lib/files/http-file-transport.ts
5796
+ var HttpFileTransport = class {
5797
+ /** Remote object URL. */
5798
+ url;
5799
+ /** Fetch implementation used for range requests. */
5800
+ fetchFunction;
5801
+ /** Persistent fetch options copied from the caller. */
5802
+ fetchOptions;
5803
+ /** Validator enforcement mode. */
5804
+ consistency;
5805
+ /** Caller-supplied identity information. */
5806
+ identityHints;
5807
+ /** Failure objects already included in telemetry. */
5808
+ countedFailures = /* @__PURE__ */ new WeakSet();
5809
+ /** Mutable counters backing immutable telemetry snapshots. */
5810
+ telemetry = {
5811
+ requestedBytes: 0,
5812
+ downloadedBytes: 0,
5813
+ requestCount: 0,
5814
+ networkTimeMs: 0,
5815
+ abortCount: 0,
5816
+ errorCount: 0
5817
+ };
5818
+ /** Creates the transport for one remote object. */
5819
+ constructor(url, options) {
5820
+ validateKnownByteLength(options.byteLength);
5821
+ this.url = url;
5822
+ this.fetchFunction = options.fetch || ((input, init) => globalThis.fetch(input, init));
5823
+ this.fetchOptions = options.fetchOptions ? { ...options.fetchOptions, headers: new Headers(options.fetchOptions.headers) } : void 0;
5824
+ this.consistency = options.consistency || "best-effort";
5825
+ this.identityHints = {
5826
+ byteLength: options.byteLength,
5827
+ etag: normalizeHeaderValue(options.etag),
5828
+ lastModified: normalizeHeaderValue(options.lastModified)
5829
+ };
5830
+ }
5831
+ /** Returns an identity that is complete enough to avoid an opening request. */
5832
+ getInitialIdentity() {
5833
+ const { byteLength, etag, lastModified } = this.identityHints;
5834
+ if (byteLength !== void 0 && (etag !== void 0 || lastModified !== void 0)) {
5835
+ return createIdentity(byteLength, etag, lastModified);
5836
+ }
5837
+ if (byteLength === 0 && this.consistency === "best-effort") {
5838
+ return createIdentity(0);
5839
+ }
5840
+ return null;
5841
+ }
5842
+ /** Returns an immutable point-in-time copy of the transport counters. */
5843
+ getTelemetry() {
5844
+ return Object.freeze({ ...this.telemetry });
5845
+ }
5846
+ /** Counts each failure object once even when it passes through several async layers. */
5847
+ trackFailure(error) {
5848
+ if (typeof error === "object" && error !== null) {
5849
+ if (this.countedFailures.has(error)) {
5850
+ return;
5851
+ }
5852
+ this.countedFailures.add(error);
5853
+ }
5854
+ if (isAbortError(error)) {
5855
+ this.telemetry.abortCount++;
5856
+ } else {
5857
+ this.telemetry.errorCount++;
5858
+ }
5859
+ }
5860
+ /** Performs and validates one exact HTTP range request. */
5861
+ async requestRange(offset, length4, signal, expectedIdentity) {
5862
+ const requestStartTime = getTimestamp2();
5863
+ const abortContext = createCombinedAbortContext(signal, this.fetchOptions?.signal);
5864
+ this.telemetry.requestCount++;
5865
+ this.telemetry.requestedBytes += length4;
5866
+ try {
5867
+ const response = await this.fetchFunction(
5868
+ this.url,
5869
+ createFetchOptions(this.fetchOptions, offset, length4, abortContext.signal)
5870
+ );
5871
+ if (isEmptyIdentityProbeResponse(response, offset, length4, expectedIdentity)) {
5872
+ return await this.readEmptyIdentityProbe(response, requestStartTime);
5873
+ }
5874
+ if (response.status !== 206) {
5875
+ await cancelResponse(response);
5876
+ throw new Error(`HTTP byte-range request expected 206, received ${response.status}`);
5877
+ }
5878
+ const contentRange = await readContentRange(response);
5879
+ const responseIdentity = getResponseIdentity(response, contentRange.byteLength);
5880
+ const responseError = getIdentityError(
5881
+ expectedIdentity,
5882
+ responseIdentity,
5883
+ this.identityHints,
5884
+ this.consistency
5885
+ ) || getRangeError(contentRange, offset, length4);
5886
+ if (responseError) {
5887
+ await cancelResponse(response);
5888
+ throw new Error(responseError);
5889
+ }
5890
+ const arrayBuffer = await response.arrayBuffer();
5891
+ this.telemetry.downloadedBytes += arrayBuffer.byteLength;
5892
+ if (arrayBuffer.byteLength !== length4) {
5893
+ throw new Error(
5894
+ `HTTP byte-range response contained ${arrayBuffer.byteLength} bytes; expected ${length4}`
5895
+ );
5896
+ }
5897
+ return {
5898
+ arrayBuffer,
5899
+ identity: resolveIdentity(expectedIdentity, responseIdentity, this.identityHints),
5900
+ status: response.status,
5901
+ transportBytes: arrayBuffer.byteLength,
5902
+ networkTimeMs: getTimestamp2() - requestStartTime
5903
+ };
5904
+ } catch (error) {
5905
+ if (expectedIdentity === null || !isAbortError(error)) {
5906
+ this.trackFailure(error);
5907
+ }
5908
+ throw error;
5909
+ } finally {
5910
+ abortContext.removeAbortListeners();
5911
+ this.telemetry.networkTimeMs += getTimestamp2() - requestStartTime;
5912
+ }
5913
+ }
5914
+ /** Validates and consumes the special empty-object identity response. */
5915
+ async readEmptyIdentityProbe(response, requestStartTime) {
5916
+ const responseIdentity = getResponseIdentity(response, 0);
5917
+ const identityError = getIdentityError(
5918
+ null,
5919
+ responseIdentity,
5920
+ this.identityHints,
5921
+ this.consistency
5922
+ );
5923
+ await cancelResponse(response);
5924
+ if (identityError) {
5925
+ throw new Error(identityError);
5926
+ }
5927
+ return {
5928
+ arrayBuffer: new ArrayBuffer(0),
5929
+ identity: resolveIdentity(null, responseIdentity, this.identityHints),
5930
+ status: response.status,
5931
+ transportBytes: 0,
5932
+ networkTimeMs: getTimestamp2() - requestStartTime
5933
+ };
5934
+ }
5935
+ };
5936
+ function createFetchOptions(fetchOptions, offset, length4, signal) {
5937
+ const headers = new Headers(fetchOptions?.headers);
5938
+ headers.set("Range", `bytes=${offset}-${offset + length4 - 1}`);
5939
+ return {
5940
+ ...fetchOptions,
5941
+ method: "GET",
5942
+ body: void 0,
5943
+ headers,
5944
+ signal
5945
+ };
5946
+ }
5947
+ function isEmptyIdentityProbeResponse(response, offset, length4, expectedIdentity) {
5948
+ return response.status === 416 && expectedIdentity === null && offset === 0 && length4 === 1 && /^bytes \*\/0$/i.test(response.headers.get("Content-Range") || "");
5949
+ }
5950
+ async function readContentRange(response) {
5951
+ try {
5952
+ return parseContentRange(response.headers.get("Content-Range"));
5953
+ } catch (error) {
5954
+ await cancelResponse(response);
5955
+ throw error;
5956
+ }
5957
+ }
5958
+ function parseContentRange(contentRange) {
5959
+ const match = contentRange?.match(/^bytes (\d+)-(\d+)\/(\d+)$/);
5960
+ if (!match) {
5961
+ throw new Error("HTTP byte-range response has an invalid Content-Range header");
5962
+ }
5963
+ return {
5964
+ offset: Number(match[1]),
5965
+ endOffset: Number(match[2]),
5966
+ byteLength: Number(match[3])
5967
+ };
5968
+ }
5969
+ function getResponseIdentity(response, byteLength) {
5970
+ return createIdentity(
5971
+ byteLength,
5972
+ normalizeHeaderValue(response.headers.get("ETag")),
5973
+ normalizeHeaderValue(response.headers.get("Last-Modified"))
5974
+ );
5975
+ }
5976
+ function resolveIdentity(expectedIdentity, responseIdentity, identityHints) {
5977
+ return expectedIdentity || createIdentity(
5978
+ responseIdentity.byteLength,
5979
+ responseIdentity.etag ?? identityHints.etag,
5980
+ responseIdentity.lastModified ?? identityHints.lastModified
5981
+ );
5982
+ }
5983
+ function getRangeError(contentRange, offset, length4) {
5984
+ if (!Number.isSafeInteger(contentRange.offset) || !Number.isSafeInteger(contentRange.endOffset) || !Number.isSafeInteger(contentRange.byteLength) || contentRange.offset < 0 || contentRange.endOffset < contentRange.offset || contentRange.endOffset >= contentRange.byteLength) {
5985
+ return "HTTP byte-range response has invalid range bounds";
5986
+ }
5987
+ if (contentRange.offset !== offset || contentRange.endOffset !== offset + length4 - 1) {
5988
+ return "HTTP byte-range response does not match the requested range";
5989
+ }
5990
+ return null;
5991
+ }
5992
+ function getIdentityError(expectedIdentity, responseIdentity, identityHints, consistency) {
5993
+ const expectedByteLength = expectedIdentity?.byteLength ?? identityHints.byteLength;
5994
+ if (expectedByteLength !== void 0 && responseIdentity.byteLength !== expectedByteLength) {
5995
+ return "HTTP file length changed while the file was open";
5996
+ }
5997
+ const expectedEtag = expectedIdentity?.etag ?? identityHints.etag;
5998
+ const expectedLastModified = expectedIdentity?.lastModified ?? identityHints.lastModified;
5999
+ if (expectedEtag) {
6000
+ if (responseIdentity.etag) {
6001
+ return expectedEtag === responseIdentity.etag ? null : "HTTP file ETag changed while the file was open";
6002
+ }
6003
+ if (consistency === "strict") {
6004
+ return "HTTP byte-range response is missing the pinned ETag";
6005
+ }
6006
+ if (expectedLastModified && responseIdentity.lastModified && expectedLastModified !== responseIdentity.lastModified) {
6007
+ return "HTTP file Last-Modified value changed while the file was open";
6008
+ }
6009
+ return null;
6010
+ }
6011
+ if (expectedLastModified) {
6012
+ if (responseIdentity.lastModified && expectedLastModified !== responseIdentity.lastModified) {
6013
+ return "HTTP file Last-Modified value changed while the file was open";
6014
+ }
6015
+ if (consistency === "strict" && !responseIdentity.lastModified) {
6016
+ return "HTTP byte-range response is missing the pinned Last-Modified value";
6017
+ }
6018
+ return null;
6019
+ }
6020
+ if (consistency === "strict" && !responseIdentity.etag && !responseIdentity.lastModified) {
6021
+ return "HTTP byte-range response does not provide an ETag or Last-Modified validator";
6022
+ }
6023
+ return null;
6024
+ }
6025
+ function createIdentity(byteLength, etag, lastModified) {
6026
+ return Object.freeze({ byteLength, etag, lastModified });
6027
+ }
6028
+ function validateKnownByteLength(byteLength) {
6029
+ if (byteLength !== void 0 && (!Number.isSafeInteger(byteLength) || byteLength < 0)) {
6030
+ throw new Error("HttpFile byteLength must be a non-negative safe integer");
6031
+ }
6032
+ }
6033
+ async function cancelResponse(response) {
6034
+ await response.body?.cancel().catch(() => {
6035
+ });
6036
+ }
6037
+ function normalizeHeaderValue(value) {
6038
+ return value || void 0;
6039
+ }
6040
+ function getTimestamp2() {
6041
+ return globalThis.performance?.now() ?? Date.now();
6042
+ }
6043
+ function isAbortError(error) {
6044
+ return error instanceof Error && error.name === "AbortError" || error instanceof Error && /aborted/i.test(error.message);
6045
+ }
6046
+ function createCombinedAbortContext(...signals) {
6047
+ const abortController = new AbortController();
6048
+ const uniqueSignals = [
6049
+ ...new Set(signals.filter((signal) => Boolean(signal)))
6050
+ ];
6051
+ const abortListener = () => abortController.abort();
6052
+ for (const signal of uniqueSignals) {
6053
+ if (signal.aborted) {
6054
+ abortController.abort();
6055
+ } else {
6056
+ signal.addEventListener("abort", abortListener, { once: true });
6057
+ }
6058
+ }
6059
+ return {
6060
+ signal: abortController.signal,
6061
+ removeAbortListeners: () => {
6062
+ for (const signal of uniqueSignals) {
6063
+ signal.removeEventListener("abort", abortListener);
6064
+ }
6065
+ }
6066
+ };
6067
+ }
6068
+
5279
6069
  // ../loader-utils/src/lib/files/http-file.ts
5280
- var HttpFile = class {
6070
+ var HttpFile = class _HttpFile {
6071
+ /** URL used as the ReadableFile handle. */
5281
6072
  handle;
5282
- size = 0;
5283
- bigsize = 0n;
6073
+ /** Remote object URL. */
5284
6074
  url;
5285
- constructor(url) {
6075
+ /** HTTP transport and response validator for this file. */
6076
+ transport;
6077
+ /** Scheduler used to coalesce compatible range reads. */
6078
+ rangeScheduler;
6079
+ /** Unique key that prevents coalescing across transport or credential contexts. */
6080
+ schedulerIsolationKey = {};
6081
+ /** Caller-supplied length available before identity discovery. */
6082
+ suppliedByteLength;
6083
+ /** Pinned remote object identity. */
6084
+ identity;
6085
+ /** Shared opening probe, while identity discovery is in progress. */
6086
+ openPromise = null;
6087
+ /** Whether the file has stopped accepting operations. */
6088
+ closed = false;
6089
+ /** Creates a lazy HTTP file. Use {@link HttpFile.open} when identity must be pinned immediately. */
6090
+ constructor(url, options = {}) {
5286
6091
  this.handle = url;
5287
6092
  this.url = url;
6093
+ this.transport = new HttpFileTransport(url, options);
6094
+ this.rangeScheduler = options.rangeScheduler || new RangeRequestScheduler({ batchDelayMs: 0, ...options.rangeSchedulerProps });
6095
+ this.suppliedByteLength = options.byteLength;
6096
+ this.identity = this.transport.getInitialIdentity();
6097
+ }
6098
+ /** Opens a file and pins its byte length and available validators. */
6099
+ static async open(url, options = {}, signal) {
6100
+ const file = new _HttpFile(url, options);
6101
+ await file.open(signal);
6102
+ return file;
6103
+ }
6104
+ /** Pins this file's byte length and available validators. */
6105
+ async open(signal) {
6106
+ this.assertOpen();
6107
+ if (signal?.aborted) {
6108
+ const error = createAbortError2();
6109
+ this.transport.trackFailure(error);
6110
+ throw error;
6111
+ }
6112
+ await this.getIdentity(signal);
6113
+ return this;
5288
6114
  }
6115
+ /** Pinned file length, or zero before a lazy file has opened. */
6116
+ get size() {
6117
+ return this.identity?.byteLength ?? this.suppliedByteLength ?? 0;
6118
+ }
6119
+ /** Pinned file length as a bigint, or zero before a lazy file has opened. */
6120
+ get bigsize() {
6121
+ return BigInt(this.size);
6122
+ }
6123
+ /** Returns the pinned identity, or null before a lazy file has opened. */
6124
+ getIdentitySnapshot() {
6125
+ return this.identity;
6126
+ }
6127
+ /** Returns an immutable point-in-time copy of this file's transport counters. */
6128
+ getTelemetry() {
6129
+ return this.transport.getTelemetry();
6130
+ }
6131
+ /** Prevents new operations. Active fetches remain controlled by their read signals. */
5289
6132
  async close() {
6133
+ this.closed = true;
5290
6134
  }
6135
+ /** Returns pinned file information without issuing a HEAD request. */
5291
6136
  async stat() {
5292
- const response = await fetch(this.handle, { method: "HEAD" });
5293
- if (!response.ok) {
5294
- throw new Error(`Failed to fetch HEAD ${this.handle}`);
5295
- }
5296
- const size = parseInt(response.headers.get("Content-Length") || "0");
6137
+ this.assertOpen();
6138
+ const identity4 = await this.getIdentity();
5297
6139
  return {
5298
- size,
5299
- bigsize: BigInt(size),
6140
+ size: identity4.byteLength,
6141
+ bigsize: BigInt(identity4.byteLength),
5300
6142
  isDirectory: false
5301
6143
  };
5302
6144
  }
5303
- async read(offset = 0, length4 = 0) {
5304
- const response = await this.fetchRange(offset, length4);
5305
- const arrayBuffer = await response.arrayBuffer();
5306
- return arrayBuffer;
6145
+ /** Reads exactly one byte range, with optional cancellation. */
6146
+ async read(offset = 0, length4 = 0, signal) {
6147
+ this.assertOpen();
6148
+ if (signal?.aborted) {
6149
+ const error = createAbortError2();
6150
+ this.transport.trackFailure(error);
6151
+ throw error;
6152
+ }
6153
+ const identity4 = await this.getIdentity(signal);
6154
+ const numericOffset = normalizeOffset(offset);
6155
+ validateRange(numericOffset, length4, identity4.byteLength);
6156
+ if (length4 === 0) {
6157
+ return new ArrayBuffer(0);
6158
+ }
6159
+ try {
6160
+ return await this.rangeScheduler.scheduleRequest({
6161
+ sourceId: this.url,
6162
+ isolationKey: this.schedulerIsolationKey,
6163
+ offset: numericOffset,
6164
+ length: length4,
6165
+ signal,
6166
+ fetchRange: this.fetchScheduledRange
6167
+ });
6168
+ } catch (error) {
6169
+ this.transport.trackFailure(error);
6170
+ throw error;
6171
+ }
5307
6172
  }
5308
- /**
5309
- *
5310
- * @param offset
5311
- * @param length
5312
- * @param signal
5313
- * @returns
5314
- * @see https://github.com/protomaps/PMTiles
5315
- */
5316
- // eslint-disable-next-line complexity
6173
+ /** Reads a range and returns a Response for legacy ReadableFile consumers. */
5317
6174
  async fetchRange(offset, length4, signal) {
5318
- const nOffset = Number(offset);
5319
- const nLength = Number(length4);
5320
- let controller;
5321
- if (!signal) {
5322
- controller = new AbortController();
5323
- signal = controller.signal;
5324
- }
5325
- const url = this.handle;
5326
- let response = await fetch(url, {
5327
- signal,
5328
- headers: { Range: `bytes=${nOffset}-${nOffset + nLength - 1}` }
5329
- });
5330
- switch (response.status) {
5331
- case 206:
5332
- break;
5333
- case 200:
5334
- const contentLength = response.headers.get("Content-Length");
5335
- if (!contentLength || Number(contentLength) > length4) {
5336
- if (controller) {
5337
- controller.abort();
5338
- }
5339
- throw Error(
5340
- "content-length header missing or exceeding request. Server must support HTTP Byte Serving."
5341
- );
5342
- }
5343
- case 416:
5344
- if (offset === 0) {
5345
- const contentRange = response.headers.get("Content-Range");
5346
- if (!contentRange || !contentRange.startsWith("bytes *")) {
5347
- throw Error("Missing content-length on 416 response");
5348
- }
5349
- const actualLength = Number(contentRange.substr(8));
5350
- response = await fetch(this.url, {
5351
- signal,
5352
- headers: { Range: `bytes=0-${actualLength - 1}` }
5353
- });
5354
- }
5355
- break;
5356
- default:
5357
- if (response.status >= 300) {
5358
- throw Error(`Bad response code: ${response.status}`);
6175
+ const numericOffset = normalizeOffset(offset);
6176
+ const arrayBuffer = await this.read(numericOffset, length4, signal);
6177
+ const headers = createIdentityHeaders(this.identity, numericOffset, length4);
6178
+ return new Response(arrayBuffer, { status: 206, headers });
6179
+ }
6180
+ /** Stable scheduler callback used by every read from this file. */
6181
+ fetchScheduledRange = async (offset, length4, signal) => {
6182
+ const identity4 = await this.getIdentity(signal);
6183
+ const result = await this.transport.requestRange(offset, length4, signal, identity4);
6184
+ return {
6185
+ arrayBuffer: result.arrayBuffer,
6186
+ status: result.status,
6187
+ transportBytes: result.transportBytes,
6188
+ networkTimeMs: result.networkTimeMs
6189
+ };
6190
+ };
6191
+ /** Returns the shared lazy identity promise, retrying after a failed probe. */
6192
+ getIdentity(signal) {
6193
+ if (this.identity) {
6194
+ return Promise.resolve(this.identity);
6195
+ }
6196
+ if (!this.openPromise) {
6197
+ const nextOpenPromise = this.probeIdentity();
6198
+ const cachedOpenPromise = nextOpenPromise.catch((error) => {
6199
+ if (this.openPromise === cachedOpenPromise) {
6200
+ this.openPromise = null;
5359
6201
  }
6202
+ throw error;
6203
+ });
6204
+ this.openPromise = cachedOpenPromise;
6205
+ }
6206
+ return waitForPromiseWithSignal(this.openPromise, signal).catch((error) => {
6207
+ this.transport.trackFailure(error);
6208
+ throw error;
6209
+ });
6210
+ }
6211
+ /** Uses a one-byte GET range to discover object length and validators. */
6212
+ async probeIdentity() {
6213
+ try {
6214
+ const result = await this.transport.requestRange(0, 1, void 0, null);
6215
+ this.identity = result.identity;
6216
+ return result.identity;
6217
+ } catch (error) {
6218
+ this.transport.trackFailure(error);
6219
+ throw error;
6220
+ }
6221
+ }
6222
+ /** Throws when the file has been closed. */
6223
+ assertOpen() {
6224
+ if (this.closed) {
6225
+ throw new Error("HttpFile is closed");
5360
6226
  }
5361
- return response;
5362
6227
  }
5363
6228
  };
6229
+ function createIdentityHeaders(identity4, offset, length4) {
6230
+ const headers = new Headers();
6231
+ if (length4 > 0 && identity4) {
6232
+ headers.set("Content-Range", `bytes ${offset}-${offset + length4 - 1}/${identity4.byteLength}`);
6233
+ }
6234
+ if (identity4?.etag) {
6235
+ headers.set("ETag", identity4.etag);
6236
+ }
6237
+ if (identity4?.lastModified) {
6238
+ headers.set("Last-Modified", identity4.lastModified);
6239
+ }
6240
+ return headers;
6241
+ }
6242
+ function normalizeOffset(offset) {
6243
+ const numericOffset = Number(offset);
6244
+ if (!Number.isSafeInteger(numericOffset) || numericOffset < 0) {
6245
+ throw new Error("HTTP byte-range offset must be a non-negative safe integer");
6246
+ }
6247
+ return numericOffset;
6248
+ }
6249
+ function validateRange(offset, length4, byteLength) {
6250
+ if (!Number.isSafeInteger(length4) || length4 < 0) {
6251
+ throw new Error("HTTP byte-range length must be a non-negative safe integer");
6252
+ }
6253
+ if (offset + length4 > byteLength) {
6254
+ throw new Error("HTTP byte range extends beyond the end of the file");
6255
+ }
6256
+ }
6257
+ function createAbortError2() {
6258
+ if (typeof DOMException !== "undefined") {
6259
+ return new DOMException("Request aborted", "AbortError");
6260
+ }
6261
+ const error = new Error("Request aborted");
6262
+ error.name = "AbortError";
6263
+ return error;
6264
+ }
6265
+ function waitForPromiseWithSignal(promise, signal) {
6266
+ if (!signal) {
6267
+ return promise;
6268
+ }
6269
+ if (signal.aborted) {
6270
+ return Promise.reject(createAbortError2());
6271
+ }
6272
+ return new Promise((resolve2, reject) => {
6273
+ const abortListener = () => {
6274
+ signal.removeEventListener("abort", abortListener);
6275
+ reject(createAbortError2());
6276
+ };
6277
+ signal.addEventListener("abort", abortListener, { once: true });
6278
+ promise.then(
6279
+ (value) => {
6280
+ signal.removeEventListener("abort", abortListener);
6281
+ resolve2(value);
6282
+ },
6283
+ (error) => {
6284
+ signal.removeEventListener("abort", abortListener);
6285
+ reject(error);
6286
+ }
6287
+ );
6288
+ });
6289
+ }
5364
6290
 
5365
6291
  // src/utils/doubly-linked-list-node.ts
5366
6292
  var DoublyLinkedListNode = class {
@@ -5488,14 +6414,23 @@ var __exports__ = (() => {
5488
6414
  unloadCallback(tileset, tile);
5489
6415
  }
5490
6416
  }
6417
+ /**
6418
+ * Unloads least-recently-used tiles until the byte-native cache target is met.
6419
+ *
6420
+ * Tiles touched after the frame sentinel remain protected, so {@link Tileset3D.cacheBytes} is a
6421
+ * soft target rather than a hard cap on the visible working set. Overflow headroom affects
6422
+ * memory-adjusted SSE, not eviction of unused tiles.
6423
+ *
6424
+ * @param tileset - Tileset that owns the cache and reports estimated content memory.
6425
+ * @param unloadCallback - Callback that releases content and decrements the memory estimate.
6426
+ */
5491
6427
  unloadTiles(tileset, unloadCallback) {
5492
6428
  const trimTiles = this._trimTiles;
5493
6429
  this._trimTiles = false;
5494
6430
  const list = this._list;
5495
- const maximumMemoryUsageInBytes = tileset.maximumMemoryUsage * 1024 * 1024;
5496
6431
  const sentinel = this._sentinel;
5497
6432
  let node = list.head;
5498
- while (node !== sentinel && (tileset.gpuMemoryUsageInBytes > maximumMemoryUsageInBytes || trimTiles)) {
6433
+ while (node !== sentinel && (tileset.gpuMemoryUsageInBytes > tileset.cacheBytes || trimTiles)) {
5499
6434
  const tile = node.item;
5500
6435
  node = node.next;
5501
6436
  this.unloadTile(tileset, tile, unloadCallback);
@@ -5566,12 +6501,10 @@ var __exports__ = (() => {
5566
6501
  const sceneIndex = typeof gltf.scene === "number" ? gltf.scene : 0;
5567
6502
  const scene = gltf.scenes?.[sceneIndex];
5568
6503
  const rootNode = scene?.nodes?.[0];
5569
- if (!rootNode?.matrix)
5570
- return null;
6504
+ if (!rootNode?.matrix) return null;
5571
6505
  const m = rootNode.matrix;
5572
6506
  const translationMagnitude = m[12] * m[12] + m[13] * m[13] + m[14] * m[14];
5573
- if (translationMagnitude <= TRANSLATION_LIMIT_SQUARED)
5574
- return null;
6507
+ if (translationMagnitude <= TRANSLATION_LIMIT_SQUARED) return null;
5575
6508
  return rootNode;
5576
6509
  }
5577
6510
 
@@ -5592,7 +6525,7 @@ var __exports__ = (() => {
5592
6525
  // ../../node_modules/@math.gl/culling/dist/lib/bounding-volumes/bounding-sphere.js
5593
6526
  var scratchVector7 = new Vector3();
5594
6527
  var scratchVector22 = new Vector3();
5595
- var BoundingSphere = class {
6528
+ var BoundingSphere = class _BoundingSphere {
5596
6529
  /** Creates a bounding sphere */
5597
6530
  constructor(center = [0, 0, 0], radius = 0) {
5598
6531
  this.radius = -0;
@@ -5621,7 +6554,7 @@ var __exports__ = (() => {
5621
6554
  }
5622
6555
  /** Duplicates a BoundingSphere instance. */
5623
6556
  clone() {
5624
- return new BoundingSphere(this.center, this.radius);
6557
+ return new _BoundingSphere(this.center, this.radius);
5625
6558
  }
5626
6559
  /** Computes a bounding sphere that contains both the left and right bounding spheres. */
5627
6560
  union(boundingSphere) {
@@ -5711,7 +6644,7 @@ var __exports__ = (() => {
5711
6644
  COLUMN2ROW1: 7,
5712
6645
  COLUMN2ROW2: 8
5713
6646
  };
5714
- var OrientedBoundingBox = class {
6647
+ var OrientedBoundingBox = class _OrientedBoundingBox {
5715
6648
  constructor(center = [0, 0, 0], halfAxes = [0, 0, 0, 0, 0, 0, 0, 0, 0]) {
5716
6649
  this.center = new Vector3().from(center);
5717
6650
  this.halfAxes = new Matrix3(halfAxes);
@@ -5754,7 +6687,7 @@ var __exports__ = (() => {
5754
6687
  }
5755
6688
  /** Duplicates a OrientedBoundingBox instance. */
5756
6689
  clone() {
5757
- return new OrientedBoundingBox(this.center, this.halfAxes);
6690
+ return new _OrientedBoundingBox(this.center, this.halfAxes);
5758
6691
  }
5759
6692
  /** Compares the provided OrientedBoundingBox component wise and returns */
5760
6693
  equals(right) {
@@ -5916,7 +6849,7 @@ var __exports__ = (() => {
5916
6849
  // ../../node_modules/@math.gl/culling/dist/lib/plane.js
5917
6850
  var scratchPosition2 = new Vector3();
5918
6851
  var scratchNormal3 = new Vector3();
5919
- var Plane = class {
6852
+ var Plane = class _Plane {
5920
6853
  constructor(normal = [0, 0, 1], distance2 = 0) {
5921
6854
  this.normal = new Vector3();
5922
6855
  this.distance = -0;
@@ -5946,7 +6879,7 @@ var __exports__ = (() => {
5946
6879
  }
5947
6880
  /** Duplicates a Plane instance. */
5948
6881
  clone() {
5949
- return new Plane(this.normal, this.distance);
6882
+ return new _Plane(this.normal, this.distance);
5950
6883
  }
5951
6884
  /** Compares the provided Planes by normal and distance */
5952
6885
  equals(right) {
@@ -5976,7 +6909,7 @@ var __exports__ = (() => {
5976
6909
  var faces = [new Vector3([1, 0, 0]), new Vector3([0, 1, 0]), new Vector3([0, 0, 1])];
5977
6910
  var scratchPlaneCenter = new Vector3();
5978
6911
  var scratchPlaneNormal = new Vector3();
5979
- var CullingVolume = class {
6912
+ var CullingVolume = class _CullingVolume {
5980
6913
  /**
5981
6914
  * Create a new `CullingVolume` bounded by an array of clipping planed
5982
6915
  * @param planes Array of clipping planes.
@@ -6037,10 +6970,10 @@ var __exports__ = (() => {
6037
6970
  */
6038
6971
  computeVisibilityWithPlaneMask(boundingVolume, parentPlaneMask) {
6039
6972
  assert(Number.isFinite(parentPlaneMask), "parentPlaneMask is required.");
6040
- if (parentPlaneMask === CullingVolume.MASK_OUTSIDE || parentPlaneMask === CullingVolume.MASK_INSIDE) {
6973
+ if (parentPlaneMask === _CullingVolume.MASK_OUTSIDE || parentPlaneMask === _CullingVolume.MASK_INSIDE) {
6041
6974
  return parentPlaneMask;
6042
6975
  }
6043
- let mask = CullingVolume.MASK_INSIDE;
6976
+ let mask = _CullingVolume.MASK_INSIDE;
6044
6977
  const planes = this.planes;
6045
6978
  for (let k = 0; k < this.planes.length; ++k) {
6046
6979
  const flag = k < 31 ? 1 << k : 0;
@@ -6050,7 +6983,7 @@ var __exports__ = (() => {
6050
6983
  const plane = planes[k];
6051
6984
  const result = boundingVolume.intersectPlane(plane);
6052
6985
  if (result === INTERSECTION.OUTSIDE) {
6053
- return CullingVolume.MASK_OUTSIDE;
6986
+ return _CullingVolume.MASK_OUTSIDE;
6054
6987
  } else if (result === INTERSECTION.INTERSECTING) {
6055
6988
  mask |= flag;
6056
6989
  }
@@ -6105,6 +7038,8 @@ var __exports__ = (() => {
6105
7038
  // src/tileset-3d/helpers/frame-state.ts
6106
7039
  var scratchVector8 = new Vector3();
6107
7040
  var scratchPosition3 = new Vector3();
7041
+ var CAMERA_POSITION_EPSILON = 1e-5;
7042
+ var CAMERA_DIRECTION_EPSILON = 1e-7;
6108
7043
  var cullingVolume = new CullingVolume([
6109
7044
  new Plane(),
6110
7045
  new Plane(),
@@ -6113,8 +7048,9 @@ var __exports__ = (() => {
6113
7048
  new Plane(),
6114
7049
  new Plane()
6115
7050
  ]);
6116
- function getFrameState(viewport, frameNumber) {
6117
- const { cameraDirection, cameraUp, height } = viewport;
7051
+ function getFrameState(viewport, frameNumber, options = {}) {
7052
+ const { cameraUp, height } = viewport;
7053
+ const cameraDirection = viewport.cameraDirection;
6118
7054
  const { metersPerUnit } = viewport.distanceScales;
6119
7055
  const viewportCenterCartesian = worldToCartesian(viewport, viewport.center);
6120
7056
  const enuToFixedTransform = Ellipsoid.WGS84.eastNorthUpToFixedFrame(viewportCenterCartesian);
@@ -6123,10 +7059,18 @@ var __exports__ = (() => {
6123
7059
  cameraPositionCartographic,
6124
7060
  new Vector3()
6125
7061
  );
6126
- const cameraDirectionCartesian = new Vector3(
6127
- // @ts-ignore
6128
- enuToFixedTransform.transformAsVector(new Vector3(cameraDirection).scale(metersPerUnit))
6129
- ).normalize();
7062
+ let cameraDirectionCartesian;
7063
+ if (cameraDirection) {
7064
+ cameraDirectionCartesian = new Vector3(
7065
+ // @ts-ignore
7066
+ enuToFixedTransform.transformAsVector(new Vector3(cameraDirection).scale(metersPerUnit))
7067
+ ).normalize();
7068
+ } else {
7069
+ const nearPlane = viewport.getFrustumPlanes().near;
7070
+ const nearCenterCommon = closestPointOnPlane(nearPlane, viewport.cameraPosition);
7071
+ const nearCenterCartesian = worldToCartesian(viewport, nearCenterCommon);
7072
+ cameraDirectionCartesian = new Vector3(nearCenterCartesian).subtract(cameraPositionCartesian2).normalize();
7073
+ }
6130
7074
  const cameraUpCartesian = new Vector3(
6131
7075
  // @ts-ignore
6132
7076
  enuToFixedTransform.transformAsVector(new Vector3(cameraUp).scale(metersPerUnit))
@@ -6147,7 +7091,10 @@ var __exports__ = (() => {
6147
7091
  camera: {
6148
7092
  position: cameraPositionCartesian2,
6149
7093
  direction: cameraDirectionCartesian,
6150
- up: cameraUpCartesian
7094
+ up: cameraUpCartesian,
7095
+ cartographicPosition: cameraPositionCartographic,
7096
+ verticalFieldOfView: Number.isFinite(viewport.fovy) && Number(viewport.fovy) > 0 ? Number(viewport.fovy) * Math.PI / 180 : Math.PI / 3,
7097
+ timeSinceMovement: options.timeSinceCameraMovement ?? Number.POSITIVE_INFINITY
6151
7098
  },
6152
7099
  viewport,
6153
7100
  topDownViewport,
@@ -6155,8 +7102,29 @@ var __exports__ = (() => {
6155
7102
  cullingVolume,
6156
7103
  frameNumber,
6157
7104
  // TODO: This can be the same between updates, what number is unique for between updates?
6158
- sseDenominator: 1.15
7105
+ sseDenominator: 1.15,
6159
7106
  // Assumes fovy = 60 degrees
7107
+ // Tileset3D fills this immediately before traversal because it depends on the root volume.
7108
+ dynamicScreenSpaceErrorDensity: 0
7109
+ };
7110
+ }
7111
+ function updateCameraMotionState(previousState, position, direction, currentTime) {
7112
+ const positionArray = Array.from(position);
7113
+ const directionArray = Array.from(direction);
7114
+ const positionChanged = previousState ? positionArray.length !== previousState.position.length || positionArray.some(
7115
+ (value, index) => Math.abs(value - previousState.position[index]) > CAMERA_POSITION_EPSILON
7116
+ ) : false;
7117
+ const directionChanged = previousState ? directionArray.length !== previousState.direction.length || directionArray.some(
7118
+ (value, index) => Math.abs(value - previousState.direction[index]) > CAMERA_DIRECTION_EPSILON
7119
+ ) : false;
7120
+ const lastMovementTime = positionChanged || directionChanged ? currentTime : previousState?.lastMovementTime ?? Number.NEGATIVE_INFINITY;
7121
+ return {
7122
+ state: {
7123
+ position: positionArray,
7124
+ direction: directionArray,
7125
+ lastMovementTime
7126
+ },
7127
+ timeSinceMovement: Math.max((currentTime - lastMovementTime) / 1e3, 0)
6160
7128
  };
6161
7129
  }
6162
7130
  function limitSelectedTiles(tiles, frameState, maximumTilesSelected) {
@@ -6217,6 +7185,290 @@ var __exports__ = (() => {
6217
7185
  return Ellipsoid.WGS84.cartographicToCartesian(cartographicPos, out);
6218
7186
  }
6219
7187
 
7188
+ // src/tileset-3d/helpers/tiles-3d-lod.ts
7189
+ var scratchPositionNormal = new Vector3();
7190
+ var scratchCartographic = new Vector3();
7191
+ var scratchCenter = new Vector3();
7192
+ var scratchPosition4 = new Vector3();
7193
+ var scratchDirection = new Vector3();
7194
+ var scratchBoxXAxis = new Vector3();
7195
+ var scratchBoxYAxis = new Vector3();
7196
+ var scratchBoxZAxis = new Vector3();
7197
+ var scratchBoxQuaternion = new Quaternion();
7198
+ var unitZDirection = new Vector3(0, 0, 1);
7199
+ function updateRootTransformForDynamicScreenSpaceError(root, modelMatrix) {
7200
+ root._updateTransform(modelMatrix);
7201
+ }
7202
+ function calculateDynamicScreenSpaceErrorDensity(root, frameState, options) {
7203
+ if (!Number.isFinite(options.dynamicScreenSpaceErrorDensity) || options.dynamicScreenSpaceErrorDensity <= 0) {
7204
+ return 0;
7205
+ }
7206
+ const boundingVolumeHeader = getContentBoundingVolumeHeader(root);
7207
+ const heightRange = boundingVolumeHeader.region ? getRegionHeightRange(boundingVolumeHeader.region, frameState) : getCartesianHeightRange(root, boundingVolumeHeader, frameState);
7208
+ if (!heightRange) {
7209
+ return 0;
7210
+ }
7211
+ const heightFalloff = clamp(options.dynamicScreenSpaceErrorHeightFalloff, 0, 1);
7212
+ const heightClose = heightRange.minimumHeight + (heightRange.maximumHeight - heightRange.minimumHeight) * heightFalloff;
7213
+ const heightSpan = heightRange.maximumHeight - heightClose;
7214
+ const heightPercentage = heightSpan > 0 ? clamp((heightRange.cameraHeight - heightClose) / heightSpan, 0, 1) : heightRange.cameraHeight >= heightRange.maximumHeight ? 1 : 0;
7215
+ const verticalAlignment = clamp(
7216
+ Math.abs(heightRange.cameraDirection.dot(heightRange.upDirection)),
7217
+ 0,
7218
+ 1
7219
+ );
7220
+ const horizonFactor = (1 - verticalAlignment) * (1 - heightPercentage);
7221
+ return options.dynamicScreenSpaceErrorDensity * horizonFactor;
7222
+ }
7223
+ function getDynamicScreenSpaceErrorFog(distanceToCamera, density) {
7224
+ const scalar = distanceToCamera * density;
7225
+ return 1 - Math.exp(-(scalar * scalar));
7226
+ }
7227
+ function getDynamicScreenSpaceError(distanceToCamera, density, factor) {
7228
+ if (density <= 0 || factor <= 0 || !Number.isFinite(density) || !Number.isFinite(factor)) {
7229
+ return 0;
7230
+ }
7231
+ return getDynamicScreenSpaceErrorFog(distanceToCamera, density) * factor;
7232
+ }
7233
+ function getTiles3DScreenSpaceError(tile, frameState, useParentLodMetric, viewportHeightFraction = 1) {
7234
+ const tileset = tile.tileset;
7235
+ const parentLodMetricValue = tile.parent && tile.parent.lodMetricValue || tile.lodMetricValue;
7236
+ const lodMetricValue = useParentLodMetric ? parentLodMetricValue : tile.lodMetricValue;
7237
+ if (lodMetricValue === 0) {
7238
+ return 0;
7239
+ }
7240
+ const { viewDistanceScale } = tileset.options;
7241
+ const lodScale = viewDistanceScale || 1;
7242
+ const heightFraction = Number.isFinite(viewportHeightFraction) ? Math.max(viewportHeightFraction, 0) : 1;
7243
+ const orthographicError = getOrthographicScreenSpaceError(
7244
+ lodMetricValue,
7245
+ frameState,
7246
+ lodScale,
7247
+ heightFraction
7248
+ );
7249
+ if (orthographicError !== null) {
7250
+ return orthographicError;
7251
+ }
7252
+ const distanceToCamera = Math.max(tile._distanceToCamera, 1e-7);
7253
+ const { height, sseDenominator } = frameState;
7254
+ let screenSpaceError = lodMetricValue * height * heightFraction * lodScale / (distanceToCamera * sseDenominator);
7255
+ if (tileset.options.dynamicScreenSpaceError && !frameState.viewport.orthographic) {
7256
+ screenSpaceError -= getDynamicScreenSpaceError(
7257
+ distanceToCamera,
7258
+ frameState.dynamicScreenSpaceErrorDensity,
7259
+ tileset.options.dynamicScreenSpaceErrorFactor
7260
+ );
7261
+ }
7262
+ return screenSpaceError;
7263
+ }
7264
+ function getOrthographicScreenSpaceError(lodMetricValue, frameState, lodScale, viewportHeightFraction) {
7265
+ const viewport = frameState.viewport;
7266
+ if (!viewport?.orthographic) {
7267
+ return null;
7268
+ }
7269
+ const metersPerPixel = viewport.metersPerPixel;
7270
+ if (!Number.isFinite(metersPerPixel) || !metersPerPixel || metersPerPixel <= 0) {
7271
+ return null;
7272
+ }
7273
+ return lodMetricValue * lodScale * viewportHeightFraction / metersPerPixel;
7274
+ }
7275
+ function getContentBoundingVolumeHeader(root) {
7276
+ return root.header.content?.boundingVolume || root.header.boundingVolume;
7277
+ }
7278
+ function getRegionHeightRange(region, frameState) {
7279
+ const upDirection = scratchPositionNormal.copy(frameState.camera.position).normalize();
7280
+ const cameraDirection = scratchDirection.copy(frameState.camera.direction).normalize();
7281
+ return {
7282
+ cameraHeight: frameState.camera.cartographicPosition[2],
7283
+ minimumHeight: region[4],
7284
+ maximumHeight: region[5],
7285
+ cameraDirection,
7286
+ upDirection
7287
+ };
7288
+ }
7289
+ function getCartesianHeightRange(root, boundingVolumeHeader, frameState) {
7290
+ const halfHeight = getBoundingVolumeCenterAndHalfHeight(boundingVolumeHeader, scratchCenter);
7291
+ if (halfHeight === null) {
7292
+ return null;
7293
+ }
7294
+ const localTransform = new Matrix4(root.computedTransform).invert();
7295
+ const ellipsoid = root.tileset.ellipsoid;
7296
+ const centerMagnitude = Math.hypot(scratchCenter[0], scratchCenter[1], scratchCenter[2]);
7297
+ if (centerMagnitude > ellipsoid.minimumRadius) {
7298
+ const centerCartographic = ellipsoid.cartesianToCartographic(
7299
+ scratchCenter,
7300
+ scratchCartographic
7301
+ );
7302
+ const upDirection = scratchPositionNormal.copy(frameState.camera.position).normalize();
7303
+ const cameraDirection = scratchDirection.copy(frameState.camera.direction).normalize();
7304
+ return {
7305
+ cameraHeight: frameState.camera.cartographicPosition[2],
7306
+ minimumHeight: 0,
7307
+ maximumHeight: centerCartographic[2] * 2,
7308
+ cameraDirection,
7309
+ upDirection
7310
+ };
7311
+ }
7312
+ localTransform.transformAsPoint(frameState.camera.position, scratchPosition4);
7313
+ localTransform.transformAsVector(frameState.camera.direction, scratchDirection);
7314
+ scratchDirection.normalize();
7315
+ return {
7316
+ cameraHeight: scratchPosition4[2],
7317
+ minimumHeight: scratchCenter[2] - halfHeight,
7318
+ maximumHeight: scratchCenter[2] + halfHeight,
7319
+ cameraDirection: scratchDirection,
7320
+ upDirection: unitZDirection
7321
+ };
7322
+ }
7323
+ function getBoundingVolumeCenterAndHalfHeight(boundingVolumeHeader, result) {
7324
+ if (boundingVolumeHeader.box) {
7325
+ const box = boundingVolumeHeader.box;
7326
+ result.set(box[0], box[1], box[2]);
7327
+ return getBoxHalfHeight(box);
7328
+ }
7329
+ if (boundingVolumeHeader.sphere) {
7330
+ const sphere = boundingVolumeHeader.sphere;
7331
+ result.set(sphere[0], sphere[1], sphere[2]);
7332
+ return sphere[3];
7333
+ }
7334
+ return null;
7335
+ }
7336
+ function getBoxHalfHeight(box) {
7337
+ if (box.length === 10) {
7338
+ scratchBoxQuaternion.fromArray(box, 6);
7339
+ scratchBoxXAxis.set(box[3], 0, 0).transformByQuaternion(scratchBoxQuaternion);
7340
+ scratchBoxYAxis.set(0, box[4], 0).transformByQuaternion(scratchBoxQuaternion);
7341
+ scratchBoxZAxis.set(0, 0, box[5]).transformByQuaternion(scratchBoxQuaternion);
7342
+ return Math.abs(scratchBoxXAxis[2]) + Math.abs(scratchBoxYAxis[2]) + Math.abs(scratchBoxZAxis[2]);
7343
+ }
7344
+ return Math.abs(box[5]) + Math.abs(box[8]) + Math.abs(box[11]);
7345
+ }
7346
+
7347
+ // src/constants.ts
7348
+ var TILE_CONTENT_STATE = {
7349
+ UNLOADED: 0,
7350
+ // Has never been requested
7351
+ LOADING: 1,
7352
+ // Is waiting on a pending request
7353
+ PROCESSING: 2,
7354
+ // Request received. Contents are being processed for rendering. Depending on the content, it might make its own requests for external data.
7355
+ READY: 3,
7356
+ // Ready to render.
7357
+ EXPIRED: 4,
7358
+ // Is expired and will be unloaded once new content is loaded.
7359
+ FAILED: 5
7360
+ // Request failed.
7361
+ };
7362
+ var TILE_REFINEMENT = /* @__PURE__ */ ((TILE_REFINEMENT2) => {
7363
+ TILE_REFINEMENT2[TILE_REFINEMENT2["ADD"] = 1] = "ADD";
7364
+ TILE_REFINEMENT2[TILE_REFINEMENT2["REPLACE"] = 2] = "REPLACE";
7365
+ return TILE_REFINEMENT2;
7366
+ })(TILE_REFINEMENT || {});
7367
+ var TILE_TYPE = /* @__PURE__ */ ((TILE_TYPE2) => {
7368
+ TILE_TYPE2["EMPTY"] = "empty";
7369
+ TILE_TYPE2["SCENEGRAPH"] = "scenegraph";
7370
+ TILE_TYPE2["POINTCLOUD"] = "pointcloud";
7371
+ TILE_TYPE2["MESH"] = "mesh";
7372
+ return TILE_TYPE2;
7373
+ })(TILE_TYPE || {});
7374
+ var TILESET_TYPE = /* @__PURE__ */ ((TILESET_TYPE2) => {
7375
+ TILESET_TYPE2["I3S"] = "I3S";
7376
+ TILESET_TYPE2["TILES3D"] = "TILES3D";
7377
+ return TILESET_TYPE2;
7378
+ })(TILESET_TYPE || {});
7379
+ var LOD_METRIC_TYPE = /* @__PURE__ */ ((LOD_METRIC_TYPE2) => {
7380
+ LOD_METRIC_TYPE2["GEOMETRIC_ERROR"] = "geometricError";
7381
+ LOD_METRIC_TYPE2["MAX_SCREEN_THRESHOLD"] = "maxScreenThreshold";
7382
+ return LOD_METRIC_TYPE2;
7383
+ })(LOD_METRIC_TYPE || {});
7384
+ var TILE3D_OPTIMIZATION_HINT = {
7385
+ NOT_COMPUTED: -1,
7386
+ USE_OPTIMIZATION: 1,
7387
+ SKIP_OPTIMIZATION: 0
7388
+ };
7389
+
7390
+ // src/tileset-3d/helpers/tiles-3d-request-priority.ts
7391
+ var PREFERRED_SORTING_DIGITS = 1e4;
7392
+ var FOVEATED_SORTING_SCALE = 1e4;
7393
+ var PROGRESSIVE_RESOLUTION_PENALTY = 1e8;
7394
+ var scratchBoundingSphere = new BoundingSphere();
7395
+ var scratchCenterOffset = new Vector3();
7396
+ var scratchClosestPointOnLine = new Vector3();
7397
+ var scratchVectorToLine = new Vector3();
7398
+ var scratchClosestPointOnSphere = new Vector3();
7399
+ function interpolateLinearly(minimumValue, maximumValue, interpolationAmount) {
7400
+ return minimumValue + (maximumValue - minimumValue) * interpolationAmount;
7401
+ }
7402
+ function calculateFoveatedFactor(boundingVolume, camera) {
7403
+ const boundingSphere = boundingVolume instanceof BoundingSphere ? boundingVolume : boundingVolume.getBoundingSphere(scratchBoundingSphere);
7404
+ const { center, radius } = boundingSphere;
7405
+ scratchCenterOffset.copy(center).subtract(camera.position);
7406
+ const centerZDepth = Math.max(scratchCenterOffset.dot(camera.direction), 0);
7407
+ scratchClosestPointOnLine.copy(camera.direction).scale(centerZDepth).add(camera.position);
7408
+ scratchVectorToLine.copy(scratchClosestPointOnLine).subtract(center);
7409
+ const distanceToCenterLine = scratchVectorToLine.magnitude();
7410
+ if (distanceToCenterLine <= radius) {
7411
+ return 0;
7412
+ }
7413
+ scratchClosestPointOnSphere.copy(scratchVectorToLine).normalize().scale(radius).add(center).subtract(camera.position);
7414
+ if (scratchClosestPointOnSphere.magnitude() === 0) {
7415
+ return 0;
7416
+ }
7417
+ scratchClosestPointOnSphere.normalize();
7418
+ return Math.min(Math.max(1 - scratchClosestPointOnSphere.dot(camera.direction), 0), 1);
7419
+ }
7420
+ function isProgressiveResolutionPriority(screenSpaceError, parentScreenSpaceError, maximumScreenSpaceError, progressiveResolutionHeightFraction) {
7421
+ if (!Number.isFinite(progressiveResolutionHeightFraction) || progressiveResolutionHeightFraction <= 0 || progressiveResolutionHeightFraction > 0.5) {
7422
+ return false;
7423
+ }
7424
+ if (screenSpaceError > maximumScreenSpaceError) {
7425
+ return true;
7426
+ }
7427
+ return parentScreenSpaceError !== void 0 && parentScreenSpaceError > maximumScreenSpaceError;
7428
+ }
7429
+ function isFoveatedRequestDeferred(parameters) {
7430
+ const coneSize = Number.isFinite(parameters.foveatedConeSize) ? Math.min(Math.max(parameters.foveatedConeSize, 0), 1) : 1;
7431
+ const replacementRefinement = parameters.refinement === 2 /* REPLACE */;
7432
+ if (replacementRefinement && !parameters.skipLevelOfDetail || !parameters.foveatedScreenSpaceError || coneSize >= 1 || replacementRefinement && parameters.skipLevelOfDetail && parameters.priorityProgressiveResolution) {
7433
+ return false;
7434
+ }
7435
+ const verticalFieldOfView = Number.isFinite(parameters.verticalFieldOfView) && parameters.verticalFieldOfView > 0 ? parameters.verticalFieldOfView : Math.PI / 3;
7436
+ const maximumFoveatedFactor = 1 - Math.cos(verticalFieldOfView * 0.5);
7437
+ const coneFactor = coneSize * maximumFoveatedFactor;
7438
+ if (parameters.foveatedFactor <= coneFactor || maximumFoveatedFactor <= coneFactor) {
7439
+ return false;
7440
+ }
7441
+ const interpolationAmount = Math.min(
7442
+ Math.max((parameters.foveatedFactor - coneFactor) / (maximumFoveatedFactor - coneFactor), 0),
7443
+ 1
7444
+ );
7445
+ const interpolationCallback = typeof parameters.interpolationCallback === "function" ? parameters.interpolationCallback : interpolateLinearly;
7446
+ const interpolatedRelaxation = interpolationCallback(
7447
+ parameters.minimumScreenSpaceErrorRelaxation,
7448
+ parameters.maximumScreenSpaceError,
7449
+ interpolationAmount
7450
+ );
7451
+ const screenSpaceErrorRelaxation = Number.isFinite(interpolatedRelaxation) ? Math.min(Math.max(interpolatedRelaxation, 0), parameters.maximumScreenSpaceError) : 0;
7452
+ const screenSpaceError = parameters.screenSpaceError === 0 && parameters.parentScreenSpaceError !== void 0 ? parameters.parentScreenSpaceError * 0.5 : parameters.screenSpaceError;
7453
+ return parameters.maximumScreenSpaceError - screenSpaceErrorRelaxation <= screenSpaceError;
7454
+ }
7455
+ function isFoveatedRequestDelayActive(deferralEligible, timeSinceMovement, foveatedTimeDelay) {
7456
+ return deferralEligible && Number.isFinite(timeSinceMovement) && Number.isFinite(foveatedTimeDelay) && foveatedTimeDelay > 0 && timeSinceMovement < foveatedTimeDelay;
7457
+ }
7458
+ function calculateTileRequestPriority(parameters) {
7459
+ const normalizedReverseScreenSpaceError = parameters.rootScreenSpaceError > 0 ? Math.min(
7460
+ Math.max(parameters.reverseScreenSpaceError / parameters.rootScreenSpaceError, 0),
7461
+ 1
7462
+ ) : 0;
7463
+ const preferredSortingDigits = Math.floor(
7464
+ normalizedReverseScreenSpaceError * (PREFERRED_SORTING_DIGITS - 1)
7465
+ );
7466
+ const normalizedFoveatedFactor = Number.isFinite(parameters.foveatedFactor) ? Math.min(Math.max(parameters.foveatedFactor, 0), 1) : 0;
7467
+ const foveatedSortingDigits = Math.floor(normalizedFoveatedFactor * (PREFERRED_SORTING_DIGITS - 1)) * FOVEATED_SORTING_SCALE;
7468
+ const progressiveResolutionDigits = parameters.priorityProgressiveResolution ? 0 : PROGRESSIVE_RESOLUTION_PENALTY;
7469
+ return progressiveResolutionDigits + foveatedSortingDigits + preferredSortingDigits;
7470
+ }
7471
+
6220
7472
  // src/tileset-3d/common/tileset-source.ts
6221
7473
  function isTileset3DSource(value) {
6222
7474
  return Boolean(
@@ -6225,14 +7477,35 @@ var __exports__ = (() => {
6225
7477
  }
6226
7478
 
6227
7479
  // src/tileset-3d/common/tileset-3d.ts
6228
- var DEFAULT_PROPS2 = {
7480
+ var BYTES_PER_MEBIBYTE = 1024 * 1024;
7481
+ var DEFAULT_CACHE_BYTES = 512 * BYTES_PER_MEBIBYTE;
7482
+ var DEFAULT_MAXIMUM_CACHE_OVERFLOW_BYTES = 512 * BYTES_PER_MEBIBYTE;
7483
+ var DEFAULT_I3S_CACHE_BYTES = 32 * BYTES_PER_MEBIBYTE;
7484
+ var DEFAULT_I3S_MAXIMUM_CACHE_OVERFLOW_BYTES = BYTES_PER_MEBIBYTE;
7485
+ function validateCacheByteLength(value, optionName) {
7486
+ if (!Number.isFinite(value) || value < 0) {
7487
+ throw new Error(`${optionName} must be a finite number greater than or equal to 0`);
7488
+ }
7489
+ return value;
7490
+ }
7491
+ function resolveCacheBytes(options, fallbackCacheBytes) {
7492
+ const cacheBytes = options.cacheBytes ?? (options.maximumMemoryUsage !== void 0 ? options.maximumMemoryUsage * BYTES_PER_MEBIBYTE : fallbackCacheBytes);
7493
+ return validateCacheByteLength(cacheBytes, "cacheBytes");
7494
+ }
7495
+ function resolveMaximumCacheOverflowBytes(options, fallbackOverflowBytes) {
7496
+ const maximumCacheOverflowBytes = options.maximumCacheOverflowBytes ?? (options.memoryCacheOverflow !== void 0 ? options.memoryCacheOverflow * BYTES_PER_MEBIBYTE : fallbackOverflowBytes);
7497
+ return validateCacheByteLength(maximumCacheOverflowBytes, "maximumCacheOverflowBytes");
7498
+ }
7499
+ var DEFAULT_PROPS3 = {
6229
7500
  description: "",
6230
7501
  ellipsoid: Ellipsoid.WGS84,
6231
7502
  modelMatrix: new Matrix4(),
6232
7503
  throttleRequests: true,
6233
7504
  maxRequests: 64,
6234
- maximumMemoryUsage: 32,
6235
- memoryCacheOverflow: 1,
7505
+ cacheBytes: DEFAULT_CACHE_BYTES,
7506
+ maximumCacheOverflowBytes: DEFAULT_MAXIMUM_CACHE_OVERFLOW_BYTES,
7507
+ maximumMemoryUsage: DEFAULT_CACHE_BYTES / BYTES_PER_MEBIBYTE,
7508
+ memoryCacheOverflow: DEFAULT_MAXIMUM_CACHE_OVERFLOW_BYTES / BYTES_PER_MEBIBYTE,
6236
7509
  maximumTilesSelected: 0,
6237
7510
  debounceTime: 0,
6238
7511
  onTileLoad: () => {
@@ -6250,8 +7523,18 @@ var __exports__ = (() => {
6250
7523
  },
6251
7524
  contentLoader: void 0,
6252
7525
  viewDistanceScale: 1,
7526
+ progressiveResolutionHeightFraction: 0.3,
7527
+ foveatedScreenSpaceError: true,
7528
+ foveatedConeSize: 0.1,
7529
+ foveatedMinimumScreenSpaceErrorRelaxation: 0,
7530
+ foveatedInterpolationCallback: interpolateLinearly,
7531
+ foveatedTimeDelay: 0.2,
6253
7532
  maximumScreenSpaceError: 8,
6254
- memoryAdjustedScreenSpaceError: false,
7533
+ dynamicScreenSpaceError: true,
7534
+ dynamicScreenSpaceErrorDensity: 2e-4,
7535
+ dynamicScreenSpaceErrorFactor: 24,
7536
+ dynamicScreenSpaceErrorHeightFalloff: 0.25,
7537
+ memoryAdjustedScreenSpaceError: true,
6255
7538
  loadTiles: true,
6256
7539
  updateTransforms: true,
6257
7540
  viewportTraversersMap: null,
@@ -6300,12 +7583,14 @@ var __exports__ = (() => {
6300
7583
  cartesianCenter = null;
6301
7584
  zoom = 1;
6302
7585
  boundingVolume = null;
7586
+ /** Effective dynamic SSE density calculated for the most recently traversed viewport. */
6303
7587
  dynamicScreenSpaceErrorComputedDensity = 0;
6304
- maximumMemoryUsage = 32;
7588
+ /** Estimated bytes currently retained by loaded tile content. */
6305
7589
  gpuMemoryUsageInBytes = 0;
7590
+ /** Active SSE threshold after optional cache-pressure adjustment, in logical/CSS pixels. */
6306
7591
  memoryAdjustedScreenSpaceError = 0;
6307
- _cacheBytes = 0;
6308
- _cacheOverflowBytes = 0;
7592
+ _cacheBytes = DEFAULT_CACHE_BYTES;
7593
+ _maximumCacheOverflowBytes = DEFAULT_MAXIMUM_CACHE_OVERFLOW_BYTES;
6309
7594
  _frameNumber = 0;
6310
7595
  _tiles = {};
6311
7596
  _pendingCount = 0;
@@ -6316,6 +7601,10 @@ var __exports__ = (() => {
6316
7601
  _requestedTiles = [];
6317
7602
  _emptyTiles = [];
6318
7603
  frameStateData = {};
7604
+ /** Last camera pose retained independently for each viewport. */
7605
+ _cameraMotionStates = {};
7606
+ /** Pending traversal that releases requests after camera motion settles. */
7607
+ _deferredTraversalTimeout = null;
6319
7608
  _traverser;
6320
7609
  _cache = new TilesetCache();
6321
7610
  _requestScheduler;
@@ -6328,11 +7617,28 @@ var __exports__ = (() => {
6328
7617
  * @param options Traversal and runtime options.
6329
7618
  */
6330
7619
  constructor(source, options) {
6331
- this.options = { ...DEFAULT_PROPS2, ...options };
6332
- this.loadOptions = this.options.loadOptions || {};
6333
7620
  if (!isTileset3DSource(source)) {
6334
7621
  throw new Error("Tileset3D requires a Tileset3DSource instance");
6335
7622
  }
7623
+ const suppliedOptions = options || {};
7624
+ const usesTiles3DCacheDefaults = source.type === "TILES3D" /* TILES3D */;
7625
+ const defaultCacheBytes = usesTiles3DCacheDefaults ? DEFAULT_PROPS3.cacheBytes : DEFAULT_I3S_CACHE_BYTES;
7626
+ const defaultMaximumCacheOverflowBytes = usesTiles3DCacheDefaults ? DEFAULT_PROPS3.maximumCacheOverflowBytes : DEFAULT_I3S_MAXIMUM_CACHE_OVERFLOW_BYTES;
7627
+ const cacheBytes = resolveCacheBytes(suppliedOptions, defaultCacheBytes);
7628
+ const maximumCacheOverflowBytes = resolveMaximumCacheOverflowBytes(
7629
+ suppliedOptions,
7630
+ defaultMaximumCacheOverflowBytes
7631
+ );
7632
+ this.options = {
7633
+ ...DEFAULT_PROPS3,
7634
+ ...suppliedOptions,
7635
+ cacheBytes,
7636
+ maximumCacheOverflowBytes,
7637
+ maximumMemoryUsage: cacheBytes / BYTES_PER_MEBIBYTE,
7638
+ memoryCacheOverflow: maximumCacheOverflowBytes / BYTES_PER_MEBIBYTE,
7639
+ memoryAdjustedScreenSpaceError: suppliedOptions.memoryAdjustedScreenSpaceError ?? usesTiles3DCacheDefaults
7640
+ };
7641
+ this.loadOptions = this.options.loadOptions || {};
6336
7642
  this.source = source;
6337
7643
  this.tileset = null;
6338
7644
  this.loader = this.source.loader;
@@ -6351,13 +7657,15 @@ var __exports__ = (() => {
6351
7657
  maxRequests: this.options.maxRequests
6352
7658
  });
6353
7659
  this.memoryAdjustedScreenSpaceError = this.options.maximumScreenSpaceError;
6354
- this._cacheBytes = this.options.maximumMemoryUsage * 1024 * 1024;
6355
- this._cacheOverflowBytes = this.options.memoryCacheOverflow * 1024 * 1024;
7660
+ this._cacheBytes = cacheBytes;
7661
+ this._maximumCacheOverflowBytes = maximumCacheOverflowBytes;
6356
7662
  this.stats = new Stats({ id: this.url });
6357
7663
  this._initializeStats();
6358
7664
  this.tilesetInitializationPromise = this._initializeTileSet();
6359
7665
  }
6360
7666
  destroy() {
7667
+ this._clearDeferredTraversal();
7668
+ this.source.destroy?.();
6361
7669
  this._destroy();
6362
7670
  }
6363
7671
  isLoaded() {
@@ -6369,13 +7677,76 @@ var __exports__ = (() => {
6369
7677
  get frameNumber() {
6370
7678
  return this._frameNumber;
6371
7679
  }
7680
+ /**
7681
+ * Gets or sets the soft target, in bytes, for cached tile content not needed this frame.
7682
+ *
7683
+ * Current-frame tiles remain protected even when their estimated memory exceeds this target.
7684
+ * Reducing the value makes unused least-recently-used tiles eligible for eviction on the next
7685
+ * traversal.
7686
+ * @default 536870912 for 3D Tiles; 33554432 for I3S
7687
+ */
7688
+ get cacheBytes() {
7689
+ return this._cacheBytes;
7690
+ }
7691
+ set cacheBytes(value) {
7692
+ this._cacheBytes = validateCacheByteLength(value, "cacheBytes");
7693
+ this.options.cacheBytes = this._cacheBytes;
7694
+ this.options.maximumMemoryUsage = this._cacheBytes / BYTES_PER_MEBIBYTE;
7695
+ }
7696
+ /**
7697
+ * Gets or sets the additional current-frame headroom, in bytes, before memory-adjusted SSE rises.
7698
+ *
7699
+ * The pressure ceiling is `cacheBytes + maximumCacheOverflowBytes`. This value does not change
7700
+ * the base target used to evict tiles that were not touched in the current frame.
7701
+ * @default 536870912 for 3D Tiles; 1048576 for I3S
7702
+ */
7703
+ get maximumCacheOverflowBytes() {
7704
+ return this._maximumCacheOverflowBytes;
7705
+ }
7706
+ set maximumCacheOverflowBytes(value) {
7707
+ this._maximumCacheOverflowBytes = validateCacheByteLength(value, "maximumCacheOverflowBytes");
7708
+ this.options.maximumCacheOverflowBytes = this._maximumCacheOverflowBytes;
7709
+ this.options.memoryCacheOverflow = this._maximumCacheOverflowBytes / BYTES_PER_MEBIBYTE;
7710
+ }
7711
+ /**
7712
+ * Gets or sets the soft cache target in mebibytes.
7713
+ * @deprecated Use {@link Tileset3D.cacheBytes}. Assignments remain synchronized for compatibility.
7714
+ */
7715
+ get maximumMemoryUsage() {
7716
+ return this.cacheBytes / BYTES_PER_MEBIBYTE;
7717
+ }
7718
+ set maximumMemoryUsage(value) {
7719
+ this.cacheBytes = validateCacheByteLength(value * BYTES_PER_MEBIBYTE, "maximumMemoryUsage");
7720
+ }
6372
7721
  get queryParams() {
6373
7722
  const rootUrl = this.source.getTileUrl(this.url);
6374
7723
  const queryIndex = rootUrl.indexOf("?");
6375
7724
  return queryIndex >= 0 ? rootUrl.slice(queryIndex + 1) : "";
6376
7725
  }
7726
+ /**
7727
+ * Updates traversal and runtime options, synchronizing byte-native and deprecated cache values.
7728
+ *
7729
+ * For each budget, the byte-native option takes precedence when both unit forms are supplied.
7730
+ * Unspecified budgets retain their current byte values, avoiding repeated MiB conversion.
7731
+ *
7732
+ * @param props - Partial runtime options to apply.
7733
+ */
6377
7734
  setProps(props) {
6378
- this.options = { ...this.options, ...props };
7735
+ const cacheBytes = resolveCacheBytes(props, this.cacheBytes);
7736
+ const maximumCacheOverflowBytes = resolveMaximumCacheOverflowBytes(
7737
+ props,
7738
+ this.maximumCacheOverflowBytes
7739
+ );
7740
+ this.options = {
7741
+ ...this.options,
7742
+ ...props,
7743
+ cacheBytes,
7744
+ maximumCacheOverflowBytes,
7745
+ maximumMemoryUsage: cacheBytes / BYTES_PER_MEBIBYTE,
7746
+ memoryCacheOverflow: maximumCacheOverflowBytes / BYTES_PER_MEBIBYTE
7747
+ };
7748
+ this._cacheBytes = cacheBytes;
7749
+ this._maximumCacheOverflowBytes = maximumCacheOverflowBytes;
6379
7750
  }
6380
7751
  getTileUrl(tilePath) {
6381
7752
  return this.source.getTileUrl(tilePath);
@@ -6413,13 +7784,20 @@ var __exports__ = (() => {
6413
7784
  }
6414
7785
  return this.updatePromise;
6415
7786
  }
7787
+ /**
7788
+ * Moves the active LOD threshold toward the configured SSE or away from cache pressure.
7789
+ *
7790
+ * Usage below {@link Tileset3D.cacheBytes} restores quality in two-percent steps. Usage above
7791
+ * `cacheBytes + maximumCacheOverflowBytes` reduces refinement in two-percent steps. Values in
7792
+ * the overflow window leave the threshold unchanged, preventing rapid load/evict oscillation.
7793
+ */
6416
7794
  adjustScreenSpaceError() {
6417
- if (this.gpuMemoryUsageInBytes < this._cacheBytes) {
7795
+ if (this.gpuMemoryUsageInBytes < this.cacheBytes) {
6418
7796
  this.memoryAdjustedScreenSpaceError = Math.max(
6419
7797
  this.memoryAdjustedScreenSpaceError / 1.02,
6420
7798
  this.options.maximumScreenSpaceError
6421
7799
  );
6422
- } else if (this.gpuMemoryUsageInBytes > this._cacheBytes + this._cacheOverflowBytes) {
7800
+ } else if (this.gpuMemoryUsageInBytes > this.cacheBytes + this.maximumCacheOverflowBytes) {
6423
7801
  this.memoryAdjustedScreenSpaceError *= 1.02;
6424
7802
  }
6425
7803
  }
@@ -6434,6 +7812,8 @@ var __exports__ = (() => {
6434
7812
  this._cache.reset();
6435
7813
  this._frameNumber++;
6436
7814
  this.traverseCounter = preparedViewports.length;
7815
+ const currentTime = Date.now();
7816
+ let minimumDeferredTraversalDelay = Number.POSITIVE_INFINITY;
6437
7817
  const viewportsToTraverse = [];
6438
7818
  for (const viewport of preparedViewports) {
6439
7819
  const id = viewport.id;
@@ -6452,7 +7832,58 @@ var __exports__ = (() => {
6452
7832
  continue;
6453
7833
  }
6454
7834
  const frameState = getFrameState(viewport, this._frameNumber);
6455
- this._traverser.traverse(this.roots[id], frameState, this.options);
7835
+ const root = this.roots[id];
7836
+ const cameraMotionUpdate = updateCameraMotionState(
7837
+ this._cameraMotionStates[id],
7838
+ frameState.camera.position,
7839
+ frameState.camera.direction,
7840
+ currentTime
7841
+ );
7842
+ this._cameraMotionStates[id] = cameraMotionUpdate.state;
7843
+ frameState.camera.timeSinceMovement = cameraMotionUpdate.timeSinceMovement;
7844
+ updateRootTransformForDynamicScreenSpaceError(root, this.modelMatrix);
7845
+ frameState.dynamicScreenSpaceErrorDensity = this.type === "TILES3D" /* TILES3D */ && this.options.dynamicScreenSpaceError && !frameState.viewport.orthographic ? calculateDynamicScreenSpaceErrorDensity(root, frameState, this.options) : 0;
7846
+ this.dynamicScreenSpaceErrorComputedDensity = frameState.dynamicScreenSpaceErrorDensity;
7847
+ this._traverser.traverse(root, frameState, this.options);
7848
+ if (Object.keys(this._traverser.deferredTiles).length > 0) {
7849
+ minimumDeferredTraversalDelay = Math.min(
7850
+ minimumDeferredTraversalDelay,
7851
+ Math.max(this.options.foveatedTimeDelay - cameraMotionUpdate.timeSinceMovement, 0)
7852
+ );
7853
+ }
7854
+ }
7855
+ if (Number.isFinite(minimumDeferredTraversalDelay)) {
7856
+ this._scheduleDeferredTraversal(minimumDeferredTraversalDelay);
7857
+ } else {
7858
+ this._clearDeferredTraversal();
7859
+ }
7860
+ }
7861
+ /**
7862
+ * Schedules one follow-up traversal when the moving-camera deferral window expires.
7863
+ *
7864
+ * Replacing an existing timer ensures continuing camera movement pushes the retry window forward
7865
+ * instead of creating a queue of redundant traversals.
7866
+ *
7867
+ * @param delayInSeconds - Remaining stationary delay before deferred requests become eligible.
7868
+ */
7869
+ _scheduleDeferredTraversal(delayInSeconds) {
7870
+ this._clearDeferredTraversal();
7871
+ this._deferredTraversalTimeout = setTimeout(
7872
+ () => {
7873
+ this._deferredTraversalTimeout = null;
7874
+ this.selectTiles().catch((error) => {
7875
+ const traversalError = error instanceof Error ? error : new Error(String(error));
7876
+ this._onTilesetError(traversalError);
7877
+ });
7878
+ },
7879
+ Math.max(delayInSeconds * 1e3, 0)
7880
+ );
7881
+ }
7882
+ /** Clears a pending moving-camera follow-up traversal. */
7883
+ _clearDeferredTraversal() {
7884
+ if (this._deferredTraversalTimeout !== null) {
7885
+ clearTimeout(this._deferredTraversalTimeout);
7886
+ this._deferredTraversalTimeout = null;
6456
7887
  }
6457
7888
  }
6458
7889
  _needTraverse(viewportId) {
@@ -6506,8 +7937,7 @@ var __exports__ = (() => {
6506
7937
  this._heldTiles.add(tileId);
6507
7938
  }
6508
7939
  for (const tileId of this._heldTiles) {
6509
- if (selectedIds.has(tileId))
6510
- continue;
7940
+ if (selectedIds.has(tileId)) continue;
6511
7941
  const tile = this._tiles[tileId];
6512
7942
  if (tile && tile.contentAvailable) {
6513
7943
  tile._selectedFrame = this._frameNumber;
@@ -6555,6 +7985,9 @@ var __exports__ = (() => {
6555
7985
  }
6556
7986
  _unloadTiles() {
6557
7987
  this._cache.unloadTiles(this, (tileset, tile) => tileset._unloadTile(tile));
7988
+ if (this.options.memoryAdjustedScreenSpaceError) {
7989
+ this.adjustScreenSpaceError();
7990
+ }
6558
7991
  }
6559
7992
  _updateStats() {
6560
7993
  let tilesRenderable = 0;
@@ -6638,6 +8071,27 @@ var __exports__ = (() => {
6638
8071
  this._onTileLoad(tile, loadResult);
6639
8072
  }
6640
8073
  }
8074
+ /**
8075
+ * Loads a source-managed lazy child-header group and reports failures through tile callbacks.
8076
+ *
8077
+ * Subtree metadata shares the normal loading counters but is not reported as render content and
8078
+ * is not added to the GPU cache. Traversal is notified separately after the promise settles.
8079
+ *
8080
+ * @param tile - Runtime tile that owns the lazy subtree reference.
8081
+ * @param frameState - View state that made the subtree eligible for refinement.
8082
+ */
8083
+ async _loadTileChildren(tile, frameState) {
8084
+ try {
8085
+ this._onStartTileLoading();
8086
+ await tile.loadChildren(frameState);
8087
+ } catch (error) {
8088
+ const tileError = error instanceof Error ? error : new Error("subtree load failed");
8089
+ this._onSourceError(tileError, tile);
8090
+ this._onTileLoadError(tile, tileError);
8091
+ } finally {
8092
+ this._onEndTileLoading();
8093
+ }
8094
+ }
6641
8095
  _onTileLoadError(tile, error) {
6642
8096
  this.stats.get(TILES_LOAD_FAILED).incrementCount();
6643
8097
  const message = error.message || error.toString();
@@ -6898,49 +8352,6 @@ var __exports__ = (() => {
6898
8352
  return queryIndex >= 0 ? url.slice(0, queryIndex) : url;
6899
8353
  }
6900
8354
 
6901
- // src/constants.ts
6902
- var TILE_CONTENT_STATE = {
6903
- UNLOADED: 0,
6904
- // Has never been requested
6905
- LOADING: 1,
6906
- // Is waiting on a pending request
6907
- PROCESSING: 2,
6908
- // Request received. Contents are being processed for rendering. Depending on the content, it might make its own requests for external data.
6909
- READY: 3,
6910
- // Ready to render.
6911
- EXPIRED: 4,
6912
- // Is expired and will be unloaded once new content is loaded.
6913
- FAILED: 5
6914
- // Request failed.
6915
- };
6916
- var TILE_REFINEMENT = /* @__PURE__ */ ((TILE_REFINEMENT2) => {
6917
- TILE_REFINEMENT2[TILE_REFINEMENT2["ADD"] = 1] = "ADD";
6918
- TILE_REFINEMENT2[TILE_REFINEMENT2["REPLACE"] = 2] = "REPLACE";
6919
- return TILE_REFINEMENT2;
6920
- })(TILE_REFINEMENT || {});
6921
- var TILE_TYPE = /* @__PURE__ */ ((TILE_TYPE2) => {
6922
- TILE_TYPE2["EMPTY"] = "empty";
6923
- TILE_TYPE2["SCENEGRAPH"] = "scenegraph";
6924
- TILE_TYPE2["POINTCLOUD"] = "pointcloud";
6925
- TILE_TYPE2["MESH"] = "mesh";
6926
- return TILE_TYPE2;
6927
- })(TILE_TYPE || {});
6928
- var TILESET_TYPE = /* @__PURE__ */ ((TILESET_TYPE2) => {
6929
- TILESET_TYPE2["I3S"] = "I3S";
6930
- TILESET_TYPE2["TILES3D"] = "TILES3D";
6931
- return TILESET_TYPE2;
6932
- })(TILESET_TYPE || {});
6933
- var LOD_METRIC_TYPE = /* @__PURE__ */ ((LOD_METRIC_TYPE2) => {
6934
- LOD_METRIC_TYPE2["GEOMETRIC_ERROR"] = "geometricError";
6935
- LOD_METRIC_TYPE2["MAX_SCREEN_THRESHOLD"] = "maxScreenThreshold";
6936
- return LOD_METRIC_TYPE2;
6937
- })(LOD_METRIC_TYPE || {});
6938
- var TILE3D_OPTIMIZATION_HINT = {
6939
- NOT_COMPUTED: -1,
6940
- USE_OPTIMIZATION: 1,
6941
- SKIP_OPTIMIZATION: 0
6942
- };
6943
-
6944
8355
  // src/tileset-3d/helpers/bounding-volume.ts
6945
8356
  function defined(x) {
6946
8357
  return x !== void 0 && x !== null;
@@ -6949,7 +8360,7 @@ var __exports__ = (() => {
6949
8360
  var scratchScale = new Vector3();
6950
8361
  var scratchNorthWest = new Vector3();
6951
8362
  var scratchSouthEast = new Vector3();
6952
- var scratchCenter = new Vector3();
8363
+ var scratchCenter2 = new Vector3();
6953
8364
  var scratchXAxis = new Vector3();
6954
8365
  var scratchYAxis = new Vector3();
6955
8366
  var scratchZAxis = new Vector3();
@@ -7048,17 +8459,17 @@ var __exports__ = (() => {
7048
8459
  scratchSouthEast
7049
8460
  );
7050
8461
  const centerInCartesian = new Vector3().addVectors(northWest, southEast).multiplyByScalar(0.5);
7051
- Ellipsoid.WGS84.cartesianToCartographic(centerInCartesian, scratchCenter);
8462
+ Ellipsoid.WGS84.cartesianToCartographic(centerInCartesian, scratchCenter2);
7052
8463
  Ellipsoid.WGS84.cartographicToCartesian(
7053
- [degrees(east), scratchCenter[1], scratchCenter[2]],
8464
+ [degrees(east), scratchCenter2[1], scratchCenter2[2]],
7054
8465
  scratchXAxis
7055
8466
  );
7056
8467
  Ellipsoid.WGS84.cartographicToCartesian(
7057
- [scratchCenter[0], degrees(north), scratchCenter[2]],
8468
+ [scratchCenter2[0], degrees(north), scratchCenter2[2]],
7058
8469
  scratchYAxis
7059
8470
  );
7060
8471
  Ellipsoid.WGS84.cartographicToCartesian(
7061
- [scratchCenter[0], scratchCenter[1], maxHeight],
8472
+ [scratchCenter2[0], scratchCenter2[1], maxHeight],
7062
8473
  scratchZAxis
7063
8474
  );
7064
8475
  return createBox(
@@ -7137,57 +8548,6 @@ var __exports__ = (() => {
7137
8548
  target[1][2] = Math.max(target[1][2], scratchPoint[2]);
7138
8549
  }
7139
8550
 
7140
- // src/tileset-3d/helpers/tiles-3d-lod.ts
7141
- var scratchPositionNormal = new Vector3();
7142
- var scratchCartographic = new Vector3();
7143
- var scratchMatrix2 = new Matrix4();
7144
- var scratchCenter2 = new Vector3();
7145
- var scratchPosition4 = new Vector3();
7146
- var scratchDirection = new Vector3();
7147
- function fog(distanceToCamera, density) {
7148
- const scalar = distanceToCamera * density;
7149
- return 1 - Math.exp(-(scalar * scalar));
7150
- }
7151
- function getDynamicScreenSpaceError(tileset, distanceToCamera) {
7152
- if (tileset.dynamicScreenSpaceError && tileset.dynamicScreenSpaceErrorComputedDensity) {
7153
- const density = tileset.dynamicScreenSpaceErrorComputedDensity;
7154
- const factor = tileset.dynamicScreenSpaceErrorFactor;
7155
- const dynamicError = fog(distanceToCamera, density) * factor;
7156
- return dynamicError;
7157
- }
7158
- return 0;
7159
- }
7160
- function getTiles3DScreenSpaceError(tile, frameState, useParentLodMetric) {
7161
- const tileset = tile.tileset;
7162
- const parentLodMetricValue = tile.parent && tile.parent.lodMetricValue || tile.lodMetricValue;
7163
- const lodMetricValue = useParentLodMetric ? parentLodMetricValue : tile.lodMetricValue;
7164
- if (lodMetricValue === 0) {
7165
- return 0;
7166
- }
7167
- const { viewDistanceScale } = tileset.options;
7168
- const lodScale = viewDistanceScale || 1;
7169
- const orthographicError = getOrthographicScreenSpaceError(lodMetricValue, frameState, lodScale);
7170
- if (orthographicError !== null) {
7171
- return orthographicError;
7172
- }
7173
- const distance2 = Math.max(tile._distanceToCamera, 1e-7);
7174
- const { height, sseDenominator } = frameState;
7175
- let error = lodMetricValue * height * lodScale / (distance2 * sseDenominator);
7176
- error -= getDynamicScreenSpaceError(tileset, distance2);
7177
- return error;
7178
- }
7179
- function getOrthographicScreenSpaceError(lodMetricValue, frameState, lodScale) {
7180
- const viewport = frameState.viewport;
7181
- if (!viewport?.orthographic) {
7182
- return null;
7183
- }
7184
- const metersPerPixel = viewport.metersPerPixel;
7185
- if (!Number.isFinite(metersPerPixel) || metersPerPixel <= 0) {
7186
- return null;
7187
- }
7188
- return lodMetricValue * lodScale / metersPerPixel;
7189
- }
7190
-
7191
8551
  // src/tileset-3d/helpers/i3s-lod.ts
7192
8552
  var cameraPositionCartesian = new Vector3();
7193
8553
  var toEye = new Vector3();
@@ -7385,7 +8745,7 @@ var __exports__ = (() => {
7385
8745
  };
7386
8746
 
7387
8747
  // src/tileset-3d/common/tileset-traverser.ts
7388
- var DEFAULT_PROPS3 = {
8748
+ var DEFAULT_PROPS4 = {
7389
8749
  loadSiblings: false,
7390
8750
  skipLevelOfDetail: false,
7391
8751
  updateTransforms: true,
@@ -7402,9 +8762,11 @@ var __exports__ = (() => {
7402
8762
  selectedTiles = {};
7403
8763
  // tiles should be loaded from server
7404
8764
  requestedTiles = {};
8765
+ /** Tiles intentionally held until the camera has remained still for the configured delay. */
8766
+ deferredTiles = {};
7405
8767
  // tiles does not have render content
7406
8768
  emptyTiles = {};
7407
- lastUpdate = new Date().getTime();
8769
+ lastUpdate = (/* @__PURE__ */ new Date()).getTime();
7408
8770
  updateDebounceTime = 1e3;
7409
8771
  /** temporary storage to hold the traversed tiles during a traversal */
7410
8772
  _traversalStack = new ManagedArray();
@@ -7417,7 +8779,7 @@ var __exports__ = (() => {
7417
8779
  }
7418
8780
  // TODO nested props
7419
8781
  constructor(options) {
7420
- this.options = { ...DEFAULT_PROPS3, ...options };
8782
+ this.options = { ...DEFAULT_PROPS4, ...options };
7421
8783
  }
7422
8784
  // tiles should be visible
7423
8785
  traverse(root, frameState, options) {
@@ -7430,6 +8792,7 @@ var __exports__ = (() => {
7430
8792
  }
7431
8793
  reset() {
7432
8794
  this.requestedTiles = {};
8795
+ this.deferredTiles = {};
7433
8796
  this.selectedTiles = {};
7434
8797
  this.emptyTiles = {};
7435
8798
  this._traversalStack.reset();
@@ -7482,7 +8845,7 @@ var __exports__ = (() => {
7482
8845
  this.touchTile(tile, frameState);
7483
8846
  tile._shouldRefine = shouldRefine && parentRefines;
7484
8847
  }
7485
- const newTime = new Date().getTime();
8848
+ const newTime = (/* @__PURE__ */ new Date()).getTime();
7486
8849
  if (this.traversalFinished(frameState) || newTime - this.lastUpdate > this.updateDebounceTime) {
7487
8850
  this.lastUpdate = newTime;
7488
8851
  this.options.onTraversalEnd(frameState);
@@ -7548,11 +8911,28 @@ var __exports__ = (() => {
7548
8911
  // tile to load from server
7549
8912
  loadTile(tile, frameState) {
7550
8913
  if (this.shouldLoadTile(tile)) {
8914
+ if (this.shouldDeferTileRequest(tile)) {
8915
+ this.deferredTiles[tile.id] = tile;
8916
+ return;
8917
+ }
7551
8918
  tile._requestedFrame = frameState.frameNumber;
7552
8919
  tile._priority = tile._getPriority();
7553
8920
  this.requestedTiles[tile.id] = tile;
7554
8921
  }
7555
8922
  }
8923
+ /**
8924
+ * Returns whether an eligible peripheral request should wait for camera motion to settle.
8925
+ *
8926
+ * Eligibility and the current time window are calculated together when tile visibility updates.
8927
+ * Keeping the active state on the tile also lets a queued scheduler callback cancel work that
8928
+ * became deferred after its original traversal.
8929
+ *
8930
+ * @param tile - Tile considered for loading.
8931
+ * @returns `true` when the request should be retried after the remaining delay.
8932
+ */
8933
+ shouldDeferTileRequest(tile) {
8934
+ return tile.priorityDeferred;
8935
+ }
7556
8936
  // cache tile
7557
8937
  touchTile(tile, frameState) {
7558
8938
  tile.tileset._cache.touch(tile);
@@ -7614,12 +8994,17 @@ var __exports__ = (() => {
7614
8994
  // Ignores visibility.
7615
8995
  executeEmptyTraversal(root, frameState) {
7616
8996
  let allDescendantsLoaded = true;
8997
+ let hasPendingLazyChildren = false;
7617
8998
  const stack = this._emptyTraversalStack;
7618
8999
  stack.push(root);
7619
9000
  while (stack.length > 0) {
7620
9001
  const tile = stack.pop();
7621
9002
  const traverse = !tile.hasRenderContent && this.canTraverse(tile, frameState);
7622
9003
  const emptyLeaf = !tile.hasRenderContent && tile.children.length === 0;
9004
+ if (tile.header.implicitSubtree) {
9005
+ hasPendingLazyChildren = true;
9006
+ allDescendantsLoaded = false;
9007
+ }
7623
9008
  if (!traverse && !tile.contentAvailable && !emptyLeaf) {
7624
9009
  allDescendantsLoaded = false;
7625
9010
  }
@@ -7635,7 +9020,7 @@ var __exports__ = (() => {
7635
9020
  }
7636
9021
  }
7637
9022
  }
7638
- return root.hasEmptyContent || allDescendantsLoaded;
9023
+ return root.hasEmptyContent && !hasPendingLazyChildren || allDescendantsLoaded;
7639
9024
  }
7640
9025
  };
7641
9026
 
@@ -7677,6 +9062,8 @@ var __exports__ = (() => {
7677
9062
  gpuMemoryUsageInBytes = 0;
7678
9063
  /** The tile's children - an array of Tile3D objects. */
7679
9064
  children = [];
9065
+ /** Current lifecycle state for a lazy implicit subtree rooted at this tile. */
9066
+ childrenState = "ready";
7680
9067
  depth = 0;
7681
9068
  viewportIds = [];
7682
9069
  transform = new Matrix4();
@@ -7695,10 +9082,20 @@ var __exports__ = (() => {
7695
9082
  // TODO Cesium 3d tiles specific
7696
9083
  _expireDate = null;
7697
9084
  _expiredContent = null;
9085
+ /** In-flight source request shared by repeated traversal frames. */
9086
+ _childrenPromise = null;
7698
9087
  _boundingBox = void 0;
7699
9088
  /** updated every frame for tree traversal and rendering optimizations: */
7700
9089
  _distanceToCamera = 0;
7701
9090
  _screenSpaceError = 0;
9091
+ /** SSE calculated at the reduced viewport height used to prioritize coarse coverage. */
9092
+ _screenSpaceErrorProgressiveResolution = 0;
9093
+ /** Whether this tile should be promoted into the progressive-resolution request pass. */
9094
+ _priorityProgressiveResolution = false;
9095
+ /** Angular distance from the camera view axis used to prioritize center tiles. */
9096
+ _foveatedFactor = 0;
9097
+ /** Whether this peripheral request is currently inside the camera-motion deferral window. */
9098
+ priorityDeferred = false;
7702
9099
  _visibilityPlaneMask;
7703
9100
  _visible = void 0;
7704
9101
  _contentBoundingVolume;
@@ -7743,6 +9140,7 @@ var __exports__ = (() => {
7743
9140
  this.refine = this._getRefine(header.refine);
7744
9141
  this.type = header.type;
7745
9142
  this.contentUrl = header.contentUrl;
9143
+ this.childrenState = header.implicitSubtree ? "unloaded" : "ready";
7746
9144
  this._initializeLodMetric(header);
7747
9145
  this._initializeTransforms(header);
7748
9146
  this._updateLodMetricScale();
@@ -7772,7 +9170,17 @@ var __exports__ = (() => {
7772
9170
  }
7773
9171
  /** Returns true if tile has children */
7774
9172
  get hasChildren() {
7775
- return this.children.length > 0 || this.header.children && this.header.children.length > 0;
9173
+ return this.children.length > 0 || this.header.children && this.header.children.length > 0 || Boolean(this.header.implicitSubtree && this.childrenState !== "ready");
9174
+ }
9175
+ /** Returns whether traversal may request an unresolved lazy child-header group. */
9176
+ get hasUnloadedChildren() {
9177
+ return Boolean(
9178
+ this.header.implicitSubtree && (this.childrenState === "unloaded" || this.childrenState === "failed")
9179
+ );
9180
+ }
9181
+ /** Returns whether a lazy child-header request is currently in flight. */
9182
+ get childrenLoading() {
9183
+ return this.childrenState === "loading";
7776
9184
  }
7777
9185
  /**
7778
9186
  * Determines if the tile's content is ready. This is automatically `true` for
@@ -7835,13 +9243,28 @@ var __exports__ = (() => {
7835
9243
  }
7836
9244
  return this._boundingBox;
7837
9245
  }
7838
- /** Get the tile's screen space error. */
7839
- getScreenSpaceError(frameState, useParentLodMetric) {
9246
+ /**
9247
+ * Gets the tile's screen-space error for normal refinement or a reduced-height priority pass.
9248
+ *
9249
+ * I3S already supplies a screen-oriented LOD metric and intentionally ignores the optional 3D
9250
+ * Tiles progressive-resolution height fraction.
9251
+ *
9252
+ * @param frameState - Current camera and viewport measurements.
9253
+ * @param useParentLodMetric - Whether to substitute the parent LOD metric.
9254
+ * @param viewportHeightFraction - Fraction of viewport height represented by the priority pass.
9255
+ * @returns Screen-space LOD error for the current tileset format.
9256
+ */
9257
+ getScreenSpaceError(frameState, useParentLodMetric, viewportHeightFraction = 1) {
7840
9258
  switch (this.tileset.type) {
7841
9259
  case "I3S" /* I3S */:
7842
9260
  return getProjectedRadius(this, frameState);
7843
9261
  case "TILES3D" /* TILES3D */:
7844
- return getTiles3DScreenSpaceError(this, frameState, useParentLodMetric);
9262
+ return getTiles3DScreenSpaceError(
9263
+ this,
9264
+ frameState,
9265
+ useParentLodMetric,
9266
+ viewportHeightFraction
9267
+ );
7845
9268
  default:
7846
9269
  throw new Error("Unsupported tileset type");
7847
9270
  }
@@ -7859,12 +9282,34 @@ var __exports__ = (() => {
7859
9282
  _getGpuMemoryUsageInBytes() {
7860
9283
  return this.content.gpuMemoryUsageInBytes || this.content.byteLength || 0;
7861
9284
  }
7862
- /*
7863
- * If skipLevelOfDetail is off try to load child tiles as soon as possible so that their parent can refine sooner.
7864
- * Tiles are prioritized by screen space error.
9285
+ /**
9286
+ * Returns scheduler priority for render-content requests.
9287
+ *
9288
+ * @returns Numeric priority, or a negative value when the queued request should be cancelled.
7865
9289
  */
7866
- // eslint-disable-next-line complexity
7867
9290
  _getPriority() {
9291
+ return this._calculateRequestPriority(false);
9292
+ }
9293
+ /**
9294
+ * Returns scheduler priority for lazy child-header requests.
9295
+ *
9296
+ * Contentless implicit placeholders intentionally retain `UNLOADED` content state, so metadata
9297
+ * scheduling skips only that render-content cancellation check and preserves every visibility,
9298
+ * recency, progressive, foveated, and motion-deferral rule.
9299
+ *
9300
+ * @returns Numeric priority, or a negative value when the queued request should be cancelled.
9301
+ */
9302
+ _getChildrenPriority() {
9303
+ return this._calculateRequestPriority(true);
9304
+ }
9305
+ /**
9306
+ * Calculates the common scheduler priority for content or hierarchy metadata.
9307
+ *
9308
+ * @param allowUnloadedContent - Whether an empty placeholder may request child metadata.
9309
+ * @returns Numeric priority, or a negative value when the queued request should be cancelled.
9310
+ */
9311
+ // eslint-disable-next-line complexity
9312
+ _calculateRequestPriority(allowUnloadedContent) {
7868
9313
  const traverser = this.tileset._traverser;
7869
9314
  const { skipLevelOfDetail } = traverser.options;
7870
9315
  const maySkipTile = this.refine === 1 /* ADD */ || skipLevelOfDetail;
@@ -7874,14 +9319,23 @@ var __exports__ = (() => {
7874
9319
  if (this.tileset._frameNumber - this._touchedFrame >= 1) {
7875
9320
  return -1;
7876
9321
  }
7877
- if (this.contentState === TILE_CONTENT_STATE.UNLOADED) {
9322
+ if (this.priorityDeferred) {
9323
+ return -1;
9324
+ }
9325
+ if (!allowUnloadedContent && this.contentState === TILE_CONTENT_STATE.UNLOADED) {
7878
9326
  return -1;
7879
9327
  }
7880
9328
  const parent = this.parent;
7881
9329
  const useParentScreenSpaceError = parent && (!maySkipTile || this._screenSpaceError === 0 || parent.hasTilesetContent);
7882
9330
  const screenSpaceError = useParentScreenSpaceError ? parent._screenSpaceError : this._screenSpaceError;
7883
9331
  const rootScreenSpaceError = traverser.root ? traverser.root._screenSpaceError : 0;
7884
- return Math.max(rootScreenSpaceError - screenSpaceError, 0);
9332
+ const reverseScreenSpaceError = Math.max(rootScreenSpaceError - screenSpaceError, 0);
9333
+ return calculateTileRequestPriority({
9334
+ priorityProgressiveResolution: this._priorityProgressiveResolution,
9335
+ foveatedFactor: this._foveatedFactor,
9336
+ reverseScreenSpaceError,
9337
+ rootScreenSpaceError
9338
+ });
7885
9339
  }
7886
9340
  /**
7887
9341
  * Requests the tile's content.
@@ -7923,6 +9377,87 @@ var __exports__ = (() => {
7923
9377
  requestToken.done();
7924
9378
  }
7925
9379
  }
9380
+ /**
9381
+ * Requests and installs this tile's lazy child-header group through its source.
9382
+ *
9383
+ * The same request scheduler and SSE/foveated priority value used for render content are reused
9384
+ * for subtree metadata. Repeated traversal frames share one promise, and scheduler cancellation
9385
+ * restores the unloaded state so a later frame can retry.
9386
+ *
9387
+ * @param frameState - View state that made the subtree eligible for refinement.
9388
+ * @returns Source load result, or an unloaded result if scheduling was cancelled.
9389
+ */
9390
+ async loadChildren(frameState) {
9391
+ if (!this.header.implicitSubtree || this.childrenState === "ready") {
9392
+ return { loaded: false, tileCount: 0, childSubtreeCount: 0 };
9393
+ }
9394
+ if (this._childrenPromise) {
9395
+ return await this._childrenPromise;
9396
+ }
9397
+ if (!this.tileset.source.loadTileChildren) {
9398
+ throw new Error("Tileset source does not support lazy tile children");
9399
+ }
9400
+ this.childrenState = "loading";
9401
+ this._childrenPromise = this._loadChildrenWithScheduler(frameState);
9402
+ try {
9403
+ return await this._childrenPromise;
9404
+ } finally {
9405
+ this._childrenPromise = null;
9406
+ }
9407
+ }
9408
+ /**
9409
+ * Runs one lazy child-header load after obtaining a scheduler token.
9410
+ *
9411
+ * @param frameState - View state forwarded to the source implementation.
9412
+ * @returns Source load result.
9413
+ */
9414
+ async _loadChildrenWithScheduler(frameState) {
9415
+ const requestToken = await this.tileset._requestScheduler.scheduleRequest(
9416
+ `${this.id}:implicit-subtree`,
9417
+ this._getChildrenPriority.bind(this)
9418
+ );
9419
+ if (!requestToken) {
9420
+ this.childrenState = "unloaded";
9421
+ return { loaded: false, tileCount: 0, childSubtreeCount: 0 };
9422
+ }
9423
+ try {
9424
+ const loadResult = await this.tileset.source.loadTileChildren(this, frameState);
9425
+ this.childrenState = loadResult.loaded ? "ready" : "unloaded";
9426
+ return loadResult;
9427
+ } catch (error) {
9428
+ this.childrenState = "failed";
9429
+ throw error;
9430
+ } finally {
9431
+ requestToken.done();
9432
+ }
9433
+ }
9434
+ /**
9435
+ * Replaces a contentless implicit placeholder with its materialized subtree-root header.
9436
+ *
9437
+ * Transform state is intentionally preserved: the placeholder already owns the composed
9438
+ * transform, and reapplying the root transform here would double-transform bounds and geometric
9439
+ * error. Content and raw geometric error are reinitialized from the newly available header.
9440
+ *
9441
+ * @param materializedHeader - Available root header produced from one subtree resource.
9442
+ */
9443
+ applyImplicitSubtreeHeader(materializedHeader) {
9444
+ const existingTransform = this.header.transform;
9445
+ const existingTransformMatrix = this.header.transformMatrix;
9446
+ this.header = {
9447
+ ...this.header,
9448
+ ...materializedHeader,
9449
+ transform: existingTransform,
9450
+ transformMatrix: existingTransformMatrix,
9451
+ implicitSubtree: void 0
9452
+ };
9453
+ this.refine = this._getRefine(materializedHeader.refine);
9454
+ this.type = materializedHeader.type;
9455
+ this.contentUrl = materializedHeader.contentUrl;
9456
+ this._initializeLodMetric(materializedHeader);
9457
+ this._updateLodMetricScale();
9458
+ this._initializeBoundingVolumes(this.header);
9459
+ this._initializeContent(this.header);
9460
+ }
7926
9461
  // Unloads the tile's content.
7927
9462
  unloadContent() {
7928
9463
  if (this.content && this.content.destroy) {
@@ -7955,12 +9490,69 @@ var __exports__ = (() => {
7955
9490
  }
7956
9491
  this._distanceToCamera = this.distanceToTile(frameState);
7957
9492
  this._screenSpaceError = this.getScreenSpaceError(frameState, false);
9493
+ this._updateRequestPriorityMetrics(frameState);
7958
9494
  this._visibilityPlaneMask = this.visibility(frameState, parentVisibilityPlaneMask);
7959
9495
  this._visible = this._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE;
7960
9496
  this._inRequestVolume = this.insideViewerRequestVolume(frameState);
7961
9497
  this._frameNumber = frameState.frameNumber;
7962
9498
  this.viewportIds = viewportIds;
7963
9499
  }
9500
+ /**
9501
+ * Updates progressive-resolution and foveated request metrics for a 3D Tiles traversal frame.
9502
+ *
9503
+ * I3S retains its format-specific screen-threshold ordering and is explicitly isolated from
9504
+ * geometric-error scheduling. Orthographic viewports still receive progressive coverage, but
9505
+ * angular foveation is disabled because an orthographic frustum has no perspective field of view.
9506
+ *
9507
+ * @param frameState - Current camera and viewport measurements.
9508
+ */
9509
+ _updateRequestPriorityMetrics(frameState) {
9510
+ if (this.tileset.type !== "TILES3D" /* TILES3D */) {
9511
+ this._screenSpaceErrorProgressiveResolution = this._screenSpaceError;
9512
+ this._priorityProgressiveResolution = false;
9513
+ this._foveatedFactor = 0;
9514
+ this.priorityDeferred = false;
9515
+ return;
9516
+ }
9517
+ const options = this.tileset.options;
9518
+ const progressiveResolutionHeightFraction = options.progressiveResolutionHeightFraction;
9519
+ this._screenSpaceErrorProgressiveResolution = this.getScreenSpaceError(
9520
+ frameState,
9521
+ false,
9522
+ progressiveResolutionHeightFraction
9523
+ );
9524
+ this._priorityProgressiveResolution = isProgressiveResolutionPriority(
9525
+ this._screenSpaceErrorProgressiveResolution,
9526
+ this.parent?._screenSpaceErrorProgressiveResolution,
9527
+ this.tileset.memoryAdjustedScreenSpaceError,
9528
+ progressiveResolutionHeightFraction
9529
+ );
9530
+ if (!options.foveatedScreenSpaceError || frameState.viewport?.orthographic || !frameState.camera?.position || !frameState.camera?.direction) {
9531
+ this._foveatedFactor = 0;
9532
+ this.priorityDeferred = false;
9533
+ return;
9534
+ }
9535
+ this._foveatedFactor = calculateFoveatedFactor(this.boundingVolume, frameState.camera);
9536
+ const deferralEligible = isFoveatedRequestDeferred({
9537
+ refinement: this.refine,
9538
+ skipLevelOfDetail: this.tileset._traverser.options.skipLevelOfDetail,
9539
+ foveatedScreenSpaceError: options.foveatedScreenSpaceError,
9540
+ foveatedConeSize: options.foveatedConeSize,
9541
+ minimumScreenSpaceErrorRelaxation: options.foveatedMinimumScreenSpaceErrorRelaxation,
9542
+ interpolationCallback: options.foveatedInterpolationCallback,
9543
+ foveatedFactor: this._foveatedFactor,
9544
+ verticalFieldOfView: frameState.camera.verticalFieldOfView,
9545
+ screenSpaceError: this._screenSpaceError,
9546
+ parentScreenSpaceError: this.parent?._screenSpaceError,
9547
+ maximumScreenSpaceError: this.tileset.memoryAdjustedScreenSpaceError,
9548
+ priorityProgressiveResolution: this._priorityProgressiveResolution
9549
+ });
9550
+ this.priorityDeferred = isFoveatedRequestDelayActive(
9551
+ deferralEligible,
9552
+ frameState.camera.timeSinceMovement,
9553
+ options.foveatedTimeDelay
9554
+ );
9555
+ }
7964
9556
  // Determines whether the tile's bounding volume intersects the culling volume.
7965
9557
  // @param {FrameState} frameState The frame state.
7966
9558
  // @param {Number} parentVisibilityPlaneMask The parent's plane mask to speed up the visibility check.
@@ -8089,6 +9681,10 @@ var __exports__ = (() => {
8089
9681
  this._distanceToCamera = 0;
8090
9682
  this._centerZDepth = 0;
8091
9683
  this._screenSpaceError = 0;
9684
+ this._screenSpaceErrorProgressiveResolution = 0;
9685
+ this._priorityProgressiveResolution = false;
9686
+ this._foveatedFactor = 0;
9687
+ this.priorityDeferred = false;
8092
9688
  this._visibilityPlaneMask = CullingVolume.MASK_INDETERMINATE;
8093
9689
  this._visible = void 0;
8094
9690
  this._inRequestVolume = false;
@@ -8104,8 +9700,15 @@ var __exports__ = (() => {
8104
9700
  _getRefine(refine) {
8105
9701
  return refine || this.parent && this.parent.refine || 2 /* REPLACE */;
8106
9702
  }
9703
+ /**
9704
+ * Returns whether loaded content is an external tileset.
9705
+ *
9706
+ * Parsed shape is authoritative because valid external resources may be extensionless, signed,
9707
+ * or use a misleading filename. This method is called only after source loading has populated
9708
+ * `content`.
9709
+ */
8107
9710
  _isTileset() {
8108
- return this.contentUrl.indexOf(".json") !== -1;
9711
+ return this.content?.shape === "tileset3d";
8109
9712
  }
8110
9713
  _onContentLoaded() {
8111
9714
  switch (this.content && this.content.type) {
@@ -8127,6 +9730,13 @@ var __exports__ = (() => {
8127
9730
  this.computedTransform,
8128
9731
  this.boundingVolume
8129
9732
  );
9733
+ if (header.viewerRequestVolume) {
9734
+ this._viewerRequestVolume = createBoundingVolume(
9735
+ header.viewerRequestVolume,
9736
+ this.computedTransform,
9737
+ this._viewerRequestVolume
9738
+ );
9739
+ }
8130
9740
  const content = header.content;
8131
9741
  if (!content) {
8132
9742
  return;
@@ -8138,13 +9748,6 @@ var __exports__ = (() => {
8138
9748
  this._contentBoundingVolume
8139
9749
  );
8140
9750
  }
8141
- if (header.viewerRequestVolume) {
8142
- this._viewerRequestVolume = createBoundingVolume(
8143
- header.viewerRequestVolume,
8144
- this.computedTransform,
8145
- this._viewerRequestVolume
8146
- );
8147
- }
8148
9751
  }
8149
9752
  /**
8150
9753
  * Updates the tile's computed transform, world-space geometric error, and bounding volumes.
@@ -8164,9 +9767,72 @@ var __exports__ = (() => {
8164
9767
 
8165
9768
  // src/tileset-3d/format-3d-tiles/tileset-3d-traverser.ts
8166
9769
  var Tileset3DTraverser = class extends TilesetTraverser {
9770
+ /** Pending implicit subtree requests keyed by viewport and tile identity. */
9771
+ pendingImplicitSubtrees = /* @__PURE__ */ new Map();
9772
+ /**
9773
+ * Reports whether every implicit subtree request started by this viewport traversal has settled.
9774
+ *
9775
+ * @param frameState - Traversal frame whose completion callback is being considered.
9776
+ * @returns `true` when no matching subtree metadata request remains pending.
9777
+ */
9778
+ traversalFinished(frameState) {
9779
+ const viewportPrefix = `${frameState.viewport.id}:`;
9780
+ for (const [requestKey, frameNumber] of this.pendingImplicitSubtrees) {
9781
+ if (requestKey.startsWith(viewportPrefix) && frameNumber === frameState.frameNumber) {
9782
+ return false;
9783
+ }
9784
+ }
9785
+ return true;
9786
+ }
8167
9787
  compareDistanceToCamera(a, b) {
8168
9788
  return b._distanceToCamera === 0 && a._distanceToCamera === 0 ? b._centerZDepth - a._centerZDepth : b._distanceToCamera - a._distanceToCamera;
8169
9789
  }
9790
+ /**
9791
+ * Starts an implicit subtree request only after culling, request-volume, and SSE eligibility.
9792
+ *
9793
+ * The current parent remains the traversal boundary while metadata is loading, which preserves
9794
+ * REPLACE coverage and ADD accumulation. Request scheduling reads the tile's existing
9795
+ * progressive-resolution and foveated metrics, so subtree metadata follows the same priority
9796
+ * policy as render content. A completion notification lets the owning tileset publish the newly
9797
+ * materialized traversal result on the next update.
9798
+ *
9799
+ * @param tile - Tile whose existing or lazy children should be updated.
9800
+ * @param frameState - Current culling and LOD state.
9801
+ */
9802
+ updateChildTiles(tile, frameState) {
9803
+ if (tile.hasUnloadedChildren && tile.isVisibleAndInRequestVolume && this.shouldRefine(tile, frameState)) {
9804
+ const requestKey = `${frameState.viewport.id}:${tile.id}`;
9805
+ if (!this.pendingImplicitSubtrees.has(requestKey)) {
9806
+ this.pendingImplicitSubtrees.set(requestKey, frameState.frameNumber);
9807
+ void tile.tileset._loadTileChildren(tile, frameState).finally(() => {
9808
+ this.handleImplicitSubtreeLoad(tile, frameState, requestKey);
9809
+ });
9810
+ }
9811
+ }
9812
+ super.updateChildTiles(tile, frameState);
9813
+ }
9814
+ /**
9815
+ * Resumes the same traversal frame after one lazy subtree request settles.
9816
+ *
9817
+ * Successful materialization traverses the new headers immediately. Failed or scheduler-cancelled
9818
+ * requests finish the current traversal without retrying in a tight loop; a later application
9819
+ * update may retry them.
9820
+ *
9821
+ * @param tile - Subtree-root tile whose request settled.
9822
+ * @param frameState - Frame that started the request.
9823
+ * @param requestKey - Viewport-scoped pending-request identity.
9824
+ */
9825
+ handleImplicitSubtreeLoad(tile, frameState, requestKey) {
9826
+ this.pendingImplicitSubtrees.delete(requestKey);
9827
+ if (this._frameNumber !== frameState.frameNumber) {
9828
+ return;
9829
+ }
9830
+ if (tile.childrenState === "ready") {
9831
+ this.executeTraversal(tile, frameState);
9832
+ } else if (this.traversalFinished(frameState)) {
9833
+ this.options.onTraversalEnd(frameState);
9834
+ }
9835
+ }
8170
9836
  updateTileVisibility(tile, frameState) {
8171
9837
  super.updateTileVisibility(tile, frameState);
8172
9838
  if (!tile.isVisibleAndInRequestVolume) {
@@ -8249,6 +9915,263 @@ var __exports__ = (() => {
8249
9915
  return size;
8250
9916
  }
8251
9917
 
9918
+ // src/tileset-3d/format-3d-tiles/implicit-tiling.ts
9919
+ var QUADTREE_CHILD_COUNT = 4;
9920
+ var OCTREE_CHILD_COUNT = 8;
9921
+ function createImplicitSubtreeReference(descriptor, coordinates) {
9922
+ return {
9923
+ coordinates,
9924
+ subtreeUrl: replaceImplicitUrlTemplate(descriptor.subtreesUrlTemplate, coordinates),
9925
+ descriptor
9926
+ };
9927
+ }
9928
+ function materializeImplicitSubtree(subtree, reference) {
9929
+ const { descriptor } = reference;
9930
+ validateImplicitDescriptor(descriptor);
9931
+ const counters = { tileCount: 0, childSubtreeCount: 0 };
9932
+ const root = materializeAvailableTile(
9933
+ subtree,
9934
+ reference,
9935
+ { localLevel: 0, mortonIndex: 0 },
9936
+ reference.coordinates,
9937
+ counters
9938
+ );
9939
+ if (!root) {
9940
+ throw new Error(
9941
+ `Invalid 3D Tiles subtree: root tile is unavailable at ${reference.subtreeUrl}`
9942
+ );
9943
+ }
9944
+ return { root, ...counters };
9945
+ }
9946
+ function replaceImplicitUrlTemplate(templateUrl, coordinates) {
9947
+ const values = {
9948
+ level: coordinates.level,
9949
+ x: coordinates.x,
9950
+ y: coordinates.y,
9951
+ z: coordinates.z
9952
+ };
9953
+ return templateUrl.replace(
9954
+ /{(level|x|y|z)}/gi,
9955
+ (_match, coordinateName) => String(values[coordinateName.toLowerCase()])
9956
+ );
9957
+ }
9958
+ function materializeAvailableTile(subtree, reference, localCoordinates, globalCoordinates, counters) {
9959
+ const { descriptor } = reference;
9960
+ const childCount = getImplicitChildCount(descriptor.subdivisionScheme);
9961
+ const availabilityIndex = getLevelOffset(childCount, localCoordinates.localLevel) + localCoordinates.mortonIndex;
9962
+ if (!getAvailabilityValue(subtree.tileAvailability, availabilityIndex)) {
9963
+ return null;
9964
+ }
9965
+ counters.tileCount++;
9966
+ const contentAvailable = getAvailabilityValue(
9967
+ getPrimaryContentAvailability(subtree.contentAvailability),
9968
+ availabilityIndex
9969
+ );
9970
+ const children = [];
9971
+ if (globalCoordinates.level < descriptor.maximumLevel) {
9972
+ if (localCoordinates.localLevel + 1 < descriptor.subtreeLevels) {
9973
+ for (let childIndex = 0; childIndex < childCount; childIndex++) {
9974
+ const childCoordinates = getChildCoordinates(
9975
+ globalCoordinates,
9976
+ childIndex,
9977
+ descriptor.subdivisionScheme
9978
+ );
9979
+ const childHeader = materializeAvailableTile(
9980
+ subtree,
9981
+ reference,
9982
+ {
9983
+ localLevel: localCoordinates.localLevel + 1,
9984
+ mortonIndex: localCoordinates.mortonIndex * childCount + childIndex
9985
+ },
9986
+ childCoordinates,
9987
+ counters
9988
+ );
9989
+ if (childHeader) {
9990
+ children.push(childHeader);
9991
+ }
9992
+ }
9993
+ } else {
9994
+ for (let childIndex = 0; childIndex < childCount; childIndex++) {
9995
+ const childSubtreeIndex = localCoordinates.mortonIndex * childCount + childIndex;
9996
+ if (getAvailabilityValue(subtree.childSubtreeAvailability, childSubtreeIndex)) {
9997
+ const childCoordinates = getChildCoordinates(
9998
+ globalCoordinates,
9999
+ childIndex,
10000
+ descriptor.subdivisionScheme
10001
+ );
10002
+ children.push(createLazyImplicitTileHeader(descriptor, childCoordinates));
10003
+ counters.childSubtreeCount++;
10004
+ }
10005
+ }
10006
+ }
10007
+ }
10008
+ return formatImplicitTileHeader(
10009
+ descriptor,
10010
+ reference,
10011
+ globalCoordinates,
10012
+ contentAvailable,
10013
+ children
10014
+ );
10015
+ }
10016
+ function createLazyImplicitTileHeader(descriptor, coordinates) {
10017
+ const reference = createImplicitSubtreeReference(descriptor, coordinates);
10018
+ return {
10019
+ id: getImplicitTileId(reference, coordinates),
10020
+ children: [],
10021
+ implicitSubtree: reference,
10022
+ boundingVolume: calculateImplicitBoundingVolume(
10023
+ descriptor.rootBoundingVolume,
10024
+ coordinates,
10025
+ descriptor.subdivisionScheme
10026
+ ),
10027
+ geometricError: descriptor.rootLodMetricValue / 2 ** coordinates.level,
10028
+ lodMetricType: descriptor.lodMetricType,
10029
+ lodMetricValue: descriptor.rootLodMetricValue / 2 ** coordinates.level,
10030
+ refine: descriptor.refine,
10031
+ type: "empty" /* EMPTY */
10032
+ };
10033
+ }
10034
+ function formatImplicitTileHeader(descriptor, reference, coordinates, contentAvailable, children) {
10035
+ const contentUrl = contentAvailable && descriptor.contentUrlTemplate ? replaceImplicitUrlTemplate(descriptor.contentUrlTemplate, coordinates) : void 0;
10036
+ const lodMetricValue = descriptor.rootLodMetricValue / 2 ** coordinates.level;
10037
+ return {
10038
+ id: getImplicitTileId(reference, coordinates),
10039
+ children,
10040
+ contentUrl,
10041
+ content: contentUrl ? { ...descriptor.contentHeader, uri: contentUrl } : void 0,
10042
+ refine: descriptor.refine,
10043
+ type: getImplicitTileType(contentUrl),
10044
+ lodMetricType: descriptor.lodMetricType,
10045
+ lodMetricValue,
10046
+ geometricError: lodMetricValue,
10047
+ boundingVolume: calculateImplicitBoundingVolume(
10048
+ descriptor.rootBoundingVolume,
10049
+ coordinates,
10050
+ descriptor.subdivisionScheme
10051
+ )
10052
+ };
10053
+ }
10054
+ function getImplicitTileId(reference, coordinates) {
10055
+ return `${reference.subtreeUrl}#implicit=${coordinates.level}/${coordinates.x}/${coordinates.y}/${coordinates.z}`;
10056
+ }
10057
+ function getAvailabilityValue(availability, index) {
10058
+ if (typeof availability?.constant === "number") {
10059
+ return Boolean(availability.constant);
10060
+ }
10061
+ const bitstream = availability?.explicitBitstream;
10062
+ if (!bitstream) {
10063
+ return false;
10064
+ }
10065
+ const byteIndex = Math.floor(index / 8);
10066
+ const bitIndex = index % 8;
10067
+ return Boolean(bitstream[byteIndex] >> bitIndex & 1);
10068
+ }
10069
+ function getPrimaryContentAvailability(availability) {
10070
+ return Array.isArray(availability) ? availability[0] || { constant: 0 } : availability || { constant: 0 };
10071
+ }
10072
+ function getLevelOffset(childCount, localLevel) {
10073
+ return (childCount ** localLevel - 1) / (childCount - 1);
10074
+ }
10075
+ function getChildCoordinates(parent, childIndex, subdivisionScheme) {
10076
+ return {
10077
+ level: parent.level + 1,
10078
+ x: parent.x * 2 + (childIndex & 1),
10079
+ y: parent.y * 2 + (childIndex >> 1 & 1),
10080
+ z: subdivisionScheme === "OCTREE" ? parent.z * 2 + (childIndex >> 2 & 1) : 0
10081
+ };
10082
+ }
10083
+ function getImplicitChildCount(subdivisionScheme) {
10084
+ return subdivisionScheme === "OCTREE" ? OCTREE_CHILD_COUNT : QUADTREE_CHILD_COUNT;
10085
+ }
10086
+ function calculateImplicitBoundingVolume(rootBoundingVolume, coordinates, subdivisionScheme) {
10087
+ if (rootBoundingVolume.region) {
10088
+ const [west, south, east, north, minimumHeight, maximumHeight] = rootBoundingVolume.region;
10089
+ const divisionCount = 2 ** coordinates.level;
10090
+ const longitudeSize = (east - west) / divisionCount;
10091
+ const latitudeSize = (north - south) / divisionCount;
10092
+ const childWest = west + longitudeSize * coordinates.x;
10093
+ const childSouth = south + latitudeSize * coordinates.y;
10094
+ let childMinimumHeight = minimumHeight;
10095
+ let childMaximumHeight = maximumHeight;
10096
+ if (subdivisionScheme === "OCTREE") {
10097
+ const heightSize = (maximumHeight - minimumHeight) / divisionCount;
10098
+ childMinimumHeight = minimumHeight + heightSize * coordinates.z;
10099
+ childMaximumHeight = childMinimumHeight + heightSize;
10100
+ }
10101
+ return {
10102
+ region: [
10103
+ childWest,
10104
+ childSouth,
10105
+ childWest + longitudeSize,
10106
+ childSouth + latitudeSize,
10107
+ childMinimumHeight,
10108
+ childMaximumHeight
10109
+ ]
10110
+ };
10111
+ }
10112
+ if (rootBoundingVolume.box) {
10113
+ if (rootBoundingVolume.s2VolumeInfo) {
10114
+ return { box: [...rootBoundingVolume.box], s2VolumeInfo: rootBoundingVolume.s2VolumeInfo };
10115
+ }
10116
+ const box = rootBoundingVolume.box;
10117
+ const divisionCount = 2 ** coordinates.level;
10118
+ const xOffset = -1 + (2 * coordinates.x + 1) / divisionCount;
10119
+ const yOffset = -1 + (2 * coordinates.y + 1) / divisionCount;
10120
+ const zOffset = -1 + (2 * coordinates.z + 1) / divisionCount;
10121
+ const divideHeight = subdivisionScheme === "OCTREE";
10122
+ const center = [
10123
+ box[0] + box[3] * xOffset + box[6] * yOffset + (divideHeight ? box[9] * zOffset : 0),
10124
+ box[1] + box[4] * xOffset + box[7] * yOffset + (divideHeight ? box[10] * zOffset : 0),
10125
+ box[2] + box[5] * xOffset + box[8] * yOffset + (divideHeight ? box[11] * zOffset : 0)
10126
+ ];
10127
+ return {
10128
+ box: [
10129
+ ...center,
10130
+ box[3] / divisionCount,
10131
+ box[4] / divisionCount,
10132
+ box[5] / divisionCount,
10133
+ box[6] / divisionCount,
10134
+ box[7] / divisionCount,
10135
+ box[8] / divisionCount,
10136
+ divideHeight ? box[9] / divisionCount : box[9],
10137
+ divideHeight ? box[10] / divisionCount : box[10],
10138
+ divideHeight ? box[11] / divisionCount : box[11]
10139
+ ]
10140
+ };
10141
+ }
10142
+ throw new Error(
10143
+ `Unsupported implicit 3D Tiles bounding volume: ${JSON.stringify(rootBoundingVolume)}`
10144
+ );
10145
+ }
10146
+ function getImplicitTileType(contentUrl) {
10147
+ if (!contentUrl) {
10148
+ return "empty" /* EMPTY */;
10149
+ }
10150
+ const extension = contentUrl.split("?")[0].split(".").pop()?.toLowerCase();
10151
+ switch (extension) {
10152
+ case "pnts":
10153
+ return "pointcloud" /* POINTCLOUD */;
10154
+ case "i3dm":
10155
+ case "b3dm":
10156
+ case "glb":
10157
+ case "gltf":
10158
+ return "scenegraph" /* SCENEGRAPH */;
10159
+ default:
10160
+ return extension || "empty" /* EMPTY */;
10161
+ }
10162
+ }
10163
+ function validateImplicitDescriptor(descriptor) {
10164
+ if (descriptor.subdivisionScheme !== "QUADTREE" && descriptor.subdivisionScheme !== "OCTREE") {
10165
+ throw new Error(`Unsupported implicit subdivision scheme: ${descriptor.subdivisionScheme}`);
10166
+ }
10167
+ if (!Number.isInteger(descriptor.subtreeLevels) || descriptor.subtreeLevels < 1) {
10168
+ throw new Error("Implicit subtreeLevels must be a positive integer");
10169
+ }
10170
+ if (!Number.isInteger(descriptor.maximumLevel) || descriptor.maximumLevel < 0) {
10171
+ throw new Error("Implicit availableLevels must describe at least the root level");
10172
+ }
10173
+ }
10174
+
8252
10175
  // src/tileset-3d/format-3d-tiles/tiles-3d-source.ts
8253
10176
  var EMPTY_CONTENT_FORMATS = {
8254
10177
  draco: false,
@@ -8256,6 +10179,7 @@ var __exports__ = (() => {
8256
10179
  dds: false,
8257
10180
  ktx2: false
8258
10181
  };
10182
+ var DEFAULT_MAXIMUM_CACHED_SUBTREES = 32;
8259
10183
  var Tiles3DSource = class {
8260
10184
  /** 3D Tiles format discriminator. */
8261
10185
  type = "TILES3D" /* TILES3D */;
@@ -8284,6 +10208,23 @@ var __exports__ = (() => {
8284
10208
  /** Normalized source metadata after initialization. */
8285
10209
  metadata;
8286
10210
  queryParams = {};
10211
+ /** Final request URLs cached by unmodified tile content URL. */
10212
+ tileUrlCache = /* @__PURE__ */ new Map();
10213
+ /** Parsed subtree promises keyed by final source URL for request deduplication and LRU reuse. */
10214
+ implicitSubtreeCache = /* @__PURE__ */ new Map();
10215
+ /** URLs whose subtree resource requests have not settled. */
10216
+ pendingImplicitSubtreeUrls = /* @__PURE__ */ new Set();
10217
+ /** Maximum number of settled parsed subtrees retained by this source. */
10218
+ maximumCachedSubtrees;
10219
+ /** Mutable counters exposed as a defensive snapshot through {@link getImplicitTilingStats}. */
10220
+ implicitTilingStats = {
10221
+ requestedSubtrees: 0,
10222
+ loadedSubtrees: 0,
10223
+ cacheHits: 0,
10224
+ materializedTiles: 0
10225
+ };
10226
+ /** Whether the owning tileset has released this source. */
10227
+ destroyed = false;
8287
10228
  extensionsUsed = [];
8288
10229
  resolver;
8289
10230
  rootTileset;
@@ -8302,6 +10243,22 @@ var __exports__ = (() => {
8302
10243
  this.resolver = request.resolver;
8303
10244
  this.coreApi = request.coreApi;
8304
10245
  this.loadOptions = loadOptions;
10246
+ const maximumCachedSubtrees = Number(
10247
+ loadOptions["3d-tiles"]?.maximumCachedSubtrees
10248
+ );
10249
+ this.maximumCachedSubtrees = Number.isFinite(maximumCachedSubtrees) ? Math.max(0, Math.floor(maximumCachedSubtrees)) : DEFAULT_MAXIMUM_CACHED_SUBTREES;
10250
+ }
10251
+ /**
10252
+ * Releases URL and parsed-subtree caches and blocks late subtree installation.
10253
+ *
10254
+ * The injected resolver or core API remains responsible for transport-level abort signals. A
10255
+ * request that cannot be aborted may finish parsing, but it will not mutate destroyed tiles.
10256
+ */
10257
+ destroy() {
10258
+ this.destroyed = true;
10259
+ this.tileUrlCache.clear();
10260
+ this.implicitSubtreeCache.clear();
10261
+ this.pendingImplicitSubtreeUrls.clear();
8305
10262
  }
8306
10263
  /**
8307
10264
  * Fetches and validates top-level metadata, then normalizes common fields used by {@link Tileset3D}.
@@ -8320,7 +10277,9 @@ var __exports__ = (() => {
8320
10277
  this.tileset = this.rootTileset;
8321
10278
  if (this.rootTileset.queryString) {
8322
10279
  const searchParams = new URLSearchParams(this.rootTileset.queryString);
8323
- Object.assign(this.queryParams, Object.fromEntries(searchParams.entries()));
10280
+ for (const [parameterName, parameterValue] of searchParams.entries()) {
10281
+ this.setQueryParameter(parameterName, parameterValue);
10282
+ }
8324
10283
  }
8325
10284
  this.asset = this.rootTileset.asset;
8326
10285
  if (!this.asset) {
@@ -8330,7 +10289,7 @@ var __exports__ = (() => {
8330
10289
  throw new Error("The tileset must be 3D Tiles version either 0.0 or 1.0 or 1.1.");
8331
10290
  }
8332
10291
  if ("tilesetVersion" in this.asset) {
8333
- this.queryParams.v = this.asset.tilesetVersion;
10292
+ this.setQueryParameter("v", this.asset.tilesetVersion);
8334
10293
  }
8335
10294
  this.properties = this.rootTileset.properties;
8336
10295
  this.extras = this.rootTileset.extras;
@@ -8367,7 +10326,7 @@ var __exports__ = (() => {
8367
10326
  return this.getMetadata().tileset;
8368
10327
  }
8369
10328
  /**
8370
- * Builds the eager runtime tile tree for a 3D Tiles subtree.
10329
+ * Builds explicit runtime headers while leaving implicit subtree references lazy.
8371
10330
  */
8372
10331
  initializeTileHeaders(tileset, tilesetJson, parentTile) {
8373
10332
  const rootTile = new Tile3D(tileset, tilesetJson.root, parentTile || void 0);
@@ -8386,7 +10345,7 @@ var __exports__ = (() => {
8386
10345
  const url = new URL(childTile.contentUrl);
8387
10346
  const session = url.searchParams.get("session");
8388
10347
  if (session) {
8389
- this.queryParams.session = session;
10348
+ this.setQueryParameter("session", session);
8390
10349
  }
8391
10350
  }
8392
10351
  tile.children.push(childTile);
@@ -8412,7 +10371,9 @@ var __exports__ = (() => {
8412
10371
  ...this.loadOptions,
8413
10372
  [this.loader.id]: {
8414
10373
  ...tilesetLoaderOptions,
8415
- isTileset: tile.type === "json",
10374
+ // Content bytes, rather than URL suffixes, distinguish external tilesets from renderable
10375
+ // payloads. This is required for signed and extensionless resources.
10376
+ isTileset: "auto",
8416
10377
  assetGltfUpAxis: this.asset && this.asset.gltfUpAxis || "Y"
8417
10378
  }
8418
10379
  };
@@ -8420,17 +10381,81 @@ var __exports__ = (() => {
8420
10381
  tile.content = content;
8421
10382
  return {
8422
10383
  loaded: true,
8423
- nestedTileset: tile.contentUrl.includes(".json") ? content : void 0
10384
+ nestedTileset: content?.shape === "tileset3d" ? content : void 0
10385
+ };
10386
+ }
10387
+ /**
10388
+ * Loads, materializes, and installs exactly one implicit subtree.
10389
+ *
10390
+ * Final URLs pass through the same query inheritance and archive/custom resolver path as render
10391
+ * content. Parsed resources are deduplicated by final URL, while the pure materializer creates
10392
+ * lazy placeholders for every available child subtree instead of recursively fetching them.
10393
+ *
10394
+ * @param tile - Existing contentless subtree-root placeholder.
10395
+ * @param frameState - View state that made this request eligible; priority is consumed earlier.
10396
+ * @returns Counts describing the installed subtree.
10397
+ */
10398
+ async loadTileChildren(tile, frameState) {
10399
+ void frameState;
10400
+ const reference = tile.header.implicitSubtree;
10401
+ if (!reference) {
10402
+ return { loaded: false, tileCount: 0, childSubtreeCount: 0 };
10403
+ }
10404
+ const subtreeUrl = this.getTileUrl(reference.subtreeUrl);
10405
+ const subtree = await this.loadImplicitSubtreeResource(subtreeUrl);
10406
+ if (this.destroyed || tile.isDestroyed()) {
10407
+ return { loaded: false, tileCount: 0, childSubtreeCount: 0 };
10408
+ }
10409
+ const materializedSubtree = materializeImplicitSubtree(subtree, {
10410
+ ...reference,
10411
+ subtreeUrl
10412
+ });
10413
+ tile.applyImplicitSubtreeHeader(materializedSubtree.root);
10414
+ const materializedTileCount = this.initializeMaterializedChildren(
10415
+ tile.tileset,
10416
+ tile,
10417
+ materializedSubtree.root.children
10418
+ );
10419
+ this.implicitTilingStats.loadedSubtrees++;
10420
+ this.implicitTilingStats.materializedTiles += materializedTileCount;
10421
+ return {
10422
+ loaded: true,
10423
+ tileCount: materializedSubtree.tileCount,
10424
+ childSubtreeCount: materializedSubtree.childSubtreeCount
10425
+ };
10426
+ }
10427
+ /**
10428
+ * Returns a snapshot of implicit-subtree request, cache, and materialization counters.
10429
+ *
10430
+ * @returns Immutable-by-convention diagnostic values for runtime inspection.
10431
+ */
10432
+ getImplicitTilingStats() {
10433
+ return {
10434
+ ...this.implicitTilingStats,
10435
+ cachedSubtrees: this.implicitSubtreeCache.size - this.pendingImplicitSubtreeUrls.size,
10436
+ pendingSubtrees: this.pendingImplicitSubtreeUrls.size
8424
10437
  };
8425
10438
  }
8426
10439
  /**
8427
10440
  * Resolves a tile content URL with source-managed query parameters.
10441
+ *
10442
+ * Existing per-resource parameters take precedence over inherited root, version, and session
10443
+ * values. Completed URLs are cached by the original tile path; {@link setQueryParameter}
10444
+ * invalidates the cache before changed source state can be observed.
10445
+ *
10446
+ * @param tilePath - Unmodified absolute content URL or data URL from the tile header.
10447
+ * @returns Content URL with any missing source parameters appended.
8428
10448
  */
8429
10449
  getTileUrl(tilePath) {
8430
10450
  if (tilePath.startsWith("data:")) {
8431
10451
  return tilePath;
8432
10452
  }
10453
+ const cachedTileUrl = this.tileUrlCache.get(tilePath);
10454
+ if (cachedTileUrl) {
10455
+ return cachedTileUrl;
10456
+ }
8433
10457
  if (!Object.keys(this.queryParams).length) {
10458
+ this.tileUrlCache.set(tilePath, tilePath);
8434
10459
  return tilePath;
8435
10460
  }
8436
10461
  const [pathWithoutQuery, existingQuery = ""] = tilePath.split("?");
@@ -8441,7 +10466,26 @@ var __exports__ = (() => {
8441
10466
  }
8442
10467
  }
8443
10468
  const queryParams = mergedQueryParams.toString();
8444
- return queryParams ? `${pathWithoutQuery}?${queryParams}` : pathWithoutQuery;
10469
+ const tileUrl = queryParams ? `${pathWithoutQuery}?${queryParams}` : pathWithoutQuery;
10470
+ this.tileUrlCache.set(tilePath, tileUrl);
10471
+ return tileUrl;
10472
+ }
10473
+ /**
10474
+ * Updates an inherited source query parameter and invalidates derived request URLs.
10475
+ *
10476
+ * Root tokens and tileset versions normally settle during initialization. Some providers expose
10477
+ * a session parameter on a child URL, so invalidation is required to prevent URLs cached earlier
10478
+ * in header construction from retaining stale authentication state.
10479
+ *
10480
+ * @param parameterName - Query parameter name.
10481
+ * @param parameterValue - Query parameter value.
10482
+ */
10483
+ setQueryParameter(parameterName, parameterValue) {
10484
+ if (this.queryParams[parameterName] === parameterValue) {
10485
+ return;
10486
+ }
10487
+ this.queryParams[parameterName] = parameterValue;
10488
+ this.tileUrlCache.clear();
8445
10489
  }
8446
10490
  /**
8447
10491
  * Derives the default view state from the root bounding volume.
@@ -8501,11 +10545,11 @@ var __exports__ = (() => {
8501
10545
  /**
8502
10546
  * Loads data through injected core APIs so this module stays independent from `@loaders.gl/core`.
8503
10547
  */
8504
- async loadWithCoreApi(url, options) {
10548
+ async loadWithCoreApi(url, options, loader = this.loader) {
8505
10549
  if (!this.coreApi) {
8506
10550
  throw new Error("Tiles3DSource requires an injected coreApi to load tileset data");
8507
10551
  }
8508
- return await this.coreApi.load(url, this.loader, options);
10552
+ return await this.coreApi.load(url, loader, options);
8509
10553
  }
8510
10554
  /**
8511
10555
  * Loads data through an injected resolver when present, otherwise through the injected core API.
@@ -8517,13 +10561,91 @@ var __exports__ = (() => {
8517
10561
  return await this.loadWithCoreApi(url, options);
8518
10562
  }
8519
10563
  /**
8520
- * Loads tile content through an injected resolver when present, otherwise through the injected core API.
10564
+ * Loads an arbitrary source resource through an injected resolver or core API.
8521
10565
  */
8522
- async loadResourceData(url, options) {
10566
+ async loadResourceData(url, options, loader = this.loader) {
8523
10567
  if (this.resolver) {
8524
- return await this.resolver.loadResource(url, this.loader, options);
10568
+ return await this.resolver.loadResource(url, loader, options);
10569
+ }
10570
+ return await this.loadWithCoreApi(url, options, loader);
10571
+ }
10572
+ /**
10573
+ * Returns a parsed subtree from the LRU cache or starts one source-managed request.
10574
+ *
10575
+ * @param subtreeUrl - Final subtree URL after query inheritance.
10576
+ * @returns Parsed subtree availability data.
10577
+ */
10578
+ async loadImplicitSubtreeResource(subtreeUrl) {
10579
+ const cachedSubtree = this.implicitSubtreeCache.get(subtreeUrl);
10580
+ if (cachedSubtree) {
10581
+ this.implicitTilingStats.cacheHits++;
10582
+ this.implicitSubtreeCache.delete(subtreeUrl);
10583
+ this.implicitSubtreeCache.set(subtreeUrl, cachedSubtree);
10584
+ return await cachedSubtree;
10585
+ }
10586
+ this.implicitTilingStats.requestedSubtrees++;
10587
+ this.pendingImplicitSubtreeUrls.add(subtreeUrl);
10588
+ const loaderOptions = this.loadOptions[this.loader.id] || {};
10589
+ const subtreePromise = this.loadResourceData(subtreeUrl, {
10590
+ ...this.loadOptions,
10591
+ [this.loader.id]: {
10592
+ ...loaderOptions,
10593
+ isTileset: false,
10594
+ isSubtree: true
10595
+ }
10596
+ });
10597
+ this.implicitSubtreeCache.set(subtreeUrl, subtreePromise);
10598
+ try {
10599
+ return await subtreePromise;
10600
+ } catch (error) {
10601
+ this.implicitSubtreeCache.delete(subtreeUrl);
10602
+ throw error;
10603
+ } finally {
10604
+ this.pendingImplicitSubtreeUrls.delete(subtreeUrl);
10605
+ this.trimImplicitSubtreeCache();
10606
+ }
10607
+ }
10608
+ /**
10609
+ * Instantiates every header represented by one materialized subtree.
10610
+ *
10611
+ * @param tileset - Owning runtime tileset.
10612
+ * @param parentTile - Existing materialized parent.
10613
+ * @param childHeaders - Headers to install below the parent.
10614
+ * @returns Number of newly allocated runtime tile nodes.
10615
+ */
10616
+ initializeMaterializedChildren(tileset, parentTile, childHeaders) {
10617
+ let materializedTileCount = 0;
10618
+ const stack = [
10619
+ { parentTile, childHeaders }
10620
+ ];
10621
+ while (stack.length > 0) {
10622
+ const entry = stack.pop();
10623
+ for (const childHeader of entry.childHeaders) {
10624
+ const childTile = new Tile3D(tileset, childHeader, entry.parentTile);
10625
+ entry.parentTile.children.push(childTile);
10626
+ childTile.depth = entry.parentTile.depth + 1;
10627
+ tileset.stats.get("Tiles In Tileset(s)").incrementCount();
10628
+ materializedTileCount++;
10629
+ if (childHeader.children?.length) {
10630
+ stack.push({ parentTile: childTile, childHeaders: childHeader.children });
10631
+ }
10632
+ }
10633
+ }
10634
+ return materializedTileCount;
10635
+ }
10636
+ /** Evicts least-recently-used settled subtree entries until the configured bound is met. */
10637
+ trimImplicitSubtreeCache() {
10638
+ if (this.implicitSubtreeCache.size <= this.maximumCachedSubtrees) {
10639
+ return;
10640
+ }
10641
+ for (const subtreeUrl of this.implicitSubtreeCache.keys()) {
10642
+ if (!this.pendingImplicitSubtreeUrls.has(subtreeUrl)) {
10643
+ this.implicitSubtreeCache.delete(subtreeUrl);
10644
+ }
10645
+ if (this.implicitSubtreeCache.size <= this.maximumCachedSubtrees) {
10646
+ break;
10647
+ }
8525
10648
  }
8526
- return await this.loadWithCoreApi(url, options);
8527
10649
  }
8528
10650
  };
8529
10651
  function isTilesetRequest(input) {
@@ -8738,7 +10860,7 @@ var __exports__ = (() => {
8738
10860
  tile.children.push(childTile);
8739
10861
  const frameState = this._tileManager.find(childTile.id).frameState;
8740
10862
  this.updateTile(childTile, frameState);
8741
- if (this._frameNumber === frameState.frameNumber && (this.traversalFinished(frameState) || new Date().getTime() - this.lastUpdate > this.updateDebounceTime)) {
10863
+ if (this._frameNumber === frameState.frameNumber && (this.traversalFinished(frameState) || (/* @__PURE__ */ new Date()).getTime() - this.lastUpdate > this.updateDebounceTime)) {
8742
10864
  this.executeTraversal(childTile, frameState);
8743
10865
  }
8744
10866
  }
@@ -9234,7 +11356,7 @@ var __exports__ = (() => {
9234
11356
  },
9235
11357
  tileSource: null
9236
11358
  };
9237
- var Tileset2D = class {
11359
+ var Tileset2D = class _Tileset2D {
9238
11360
  /** Live counters describing shared tileset state. */
9239
11361
  stats;
9240
11362
  /** Cached metadata returned by the backing TileSource, if any. */
@@ -9296,7 +11418,7 @@ var __exports__ = (() => {
9296
11418
  }
9297
11419
  /** Convenience factory for wrapping a loaders.gl `TileSource`. */
9298
11420
  static fromTileSource(tileSource, opts = {}) {
9299
- return new Tileset2D({ ...opts, tileSource });
11421
+ return new _Tileset2D({ ...opts, tileSource });
9300
11422
  }
9301
11423
  /** All tiles currently present in the shared cache. */
9302
11424
  get tiles() {
@@ -9738,7 +11860,7 @@ var __exports__ = (() => {
9738
11860
  getImage: async () => null,
9739
11861
  debounceTime: 0
9740
11862
  };
9741
- var ImageSet = class {
11863
+ var ImageSet = class _ImageSet {
9742
11864
  /** Cached metadata returned by the backing source, if any. */
9743
11865
  metadata = null;
9744
11866
  _opts;
@@ -9761,7 +11883,7 @@ var __exports__ = (() => {
9761
11883
  }
9762
11884
  /** Convenience factory for wrapping a loaders.gl {@link ImageSource}. */
9763
11885
  static fromImageSource(imageSource, opts = {}) {
9764
- return new ImageSet({ ...opts, imageSource });
11886
+ return new _ImageSet({ ...opts, imageSource });
9765
11887
  }
9766
11888
  /** Whether the manager has no requests in flight and a current image. */
9767
11889
  get isLoaded() {
@@ -9870,7 +11992,7 @@ var __exports__ = (() => {
9870
11992
  }
9871
11993
  this._emitUpdate();
9872
11994
  } catch (error) {
9873
- if (isAbortError(error)) {
11995
+ if (isAbortError2(error)) {
9874
11996
  return;
9875
11997
  }
9876
11998
  const normalizedError = normalizeImageSetError(error, "Image request failed");
@@ -9951,7 +12073,7 @@ var __exports__ = (() => {
9951
12073
  this._abortController = null;
9952
12074
  }
9953
12075
  };
9954
- function isAbortError(error) {
12076
+ function isAbortError2(error) {
9955
12077
  return error instanceof Error && error.name === "AbortError";
9956
12078
  }
9957
12079
  function normalizeImageSetError(error, message) {
@@ -9982,7 +12104,7 @@ var __exports__ = (() => {
9982
12104
  debounceTime: 0,
9983
12105
  shouldRefetch: () => true
9984
12106
  };
9985
- var RasterSet = class {
12107
+ var RasterSet = class _RasterSet {
9986
12108
  /** Cached metadata returned by the backing source, if any. */
9987
12109
  metadata = null;
9988
12110
  _opts;
@@ -10005,7 +12127,7 @@ var __exports__ = (() => {
10005
12127
  }
10006
12128
  /** Convenience factory for wrapping a loaders.gl {@link RasterSource}. */
10007
12129
  static fromRasterSource(rasterSource, opts = {}) {
10008
- return new RasterSet({ ...opts, rasterSource });
12130
+ return new _RasterSet({ ...opts, rasterSource });
10009
12131
  }
10010
12132
  /** Whether the manager has no requests in flight and a current raster. */
10011
12133
  get isLoaded() {
@@ -10124,7 +12246,7 @@ var __exports__ = (() => {
10124
12246
  }
10125
12247
  this._emitUpdate();
10126
12248
  } catch (error) {
10127
- if (isAbortError2(error)) {
12249
+ if (isAbortError3(error)) {
10128
12250
  return;
10129
12251
  }
10130
12252
  const normalizedError = normalizeRasterSetError(error, "Raster request failed");
@@ -10213,7 +12335,7 @@ var __exports__ = (() => {
10213
12335
  }
10214
12336
  return new Error(message);
10215
12337
  }
10216
- function isAbortError2(error) {
12338
+ function isAbortError3(error) {
10217
12339
  return error instanceof Error && error.name === "AbortError";
10218
12340
  }
10219
12341
 
@@ -10283,7 +12405,7 @@ var __exports__ = (() => {
10283
12405
  };
10284
12406
 
10285
12407
  // src/point-cloud/point-cloud-tileset.ts
10286
- var DEFAULT_PROPS4 = {
12408
+ var DEFAULT_PROPS5 = {
10287
12409
  debounceTime: 0,
10288
12410
  minimumNodePixelSize: 150,
10289
12411
  maximumScreenSpaceError: 1,
@@ -10313,7 +12435,7 @@ var __exports__ = (() => {
10313
12435
  tilesMap = /* @__PURE__ */ new Map();
10314
12436
  constructor(dataSource, options = {}) {
10315
12437
  this.dataSource = dataSource;
10316
- this.options = { ...DEFAULT_PROPS4, ...options };
12438
+ this.options = { ...DEFAULT_PROPS5, ...options };
10317
12439
  this.tilesetInitializationPromise = this.initialize();
10318
12440
  }
10319
12441
  tilesetInitializationPromise;