@loaders.gl/tile-converter 5.0.0-alpha.11 → 5.0.0-alpha.14

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 (152) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +180 -4
  3. package/bin/converter-v5.js +2 -0
  4. package/dist/converter.min.cjs +172 -182
  5. package/dist/i3s-server/bin/i3s-server.min.cjs +113 -158
  6. package/dist/index.cjs +18832 -19228
  7. package/dist/index.js +19566 -19968
  8. package/dist/v5/adapters.cjs +2002 -230
  9. package/dist/v5/adapters.d.ts +20 -0
  10. package/dist/v5/adapters.js +2005 -214
  11. package/dist/v5/archive-source.d.ts +36 -0
  12. package/dist/v5/browser-tileset-source.d.ts +55 -11
  13. package/dist/v5/browser.cjs +18 -0
  14. package/dist/v5/browser.d.ts +15 -1
  15. package/dist/v5/browser.js +26 -0
  16. package/dist/v5/conversion-api.d.ts +34 -0
  17. package/dist/v5/converter-cli.d.ts +1 -0
  18. package/dist/v5/converter-cli.js +1230 -0
  19. package/dist/v5/core.cjs +547 -77
  20. package/dist/v5/core.d.ts +3 -1
  21. package/dist/v5/core.js +540 -70
  22. package/dist/v5/feature-arrow.d.ts +1 -1
  23. package/dist/v5/i3s-mesh-conversion.d.ts +5 -1
  24. package/dist/v5/i3s-mesh-source-features.d.ts +2 -2
  25. package/dist/v5/i3s-mesh-source-material.d.ts +1 -1
  26. package/dist/v5/i3s-mesh-source.d.ts +2 -2
  27. package/dist/v5/i3s-selection-source.d.ts +70 -0
  28. package/dist/v5/index.cjs +15 -0
  29. package/dist/v5/index.d.ts +13 -1
  30. package/dist/v5/index.js +22 -0
  31. package/dist/v5/mesh-conversion.d.ts +21 -3
  32. package/dist/v5/mesh-draco.d.ts +2 -2
  33. package/dist/v5/mesh-features.d.ts +2 -2
  34. package/dist/v5/mesh-hierarchy.d.ts +47 -0
  35. package/dist/v5/mesh-source-features.d.ts +6 -5
  36. package/dist/v5/mesh-source-schema.d.ts +12 -0
  37. package/dist/v5/mesh-source-texture.d.ts +3 -3
  38. package/dist/v5/mesh-source.d.ts +10 -7
  39. package/dist/v5/mesh-tileset-sink.d.ts +5 -0
  40. package/dist/v5/mesh.d.ts +1 -1
  41. package/dist/v5/node-cli.d.ts +9 -0
  42. package/dist/v5/node-conversion.d.ts +34 -0
  43. package/dist/v5/node-feature-schema.d.ts +3 -0
  44. package/dist/v5/node-i3s-source.d.ts +41 -0
  45. package/dist/v5/node-source.d.ts +47 -0
  46. package/dist/v5/node.cjs +913 -0
  47. package/dist/v5/node.d.ts +6 -0
  48. package/dist/v5/node.js +890 -0
  49. package/dist/v5/point-cloud-attributes.d.ts +19 -0
  50. package/dist/v5/point-cloud-conversion.d.ts +1 -1
  51. package/dist/v5/point-cloud-copc.d.ts +44 -0
  52. package/dist/v5/point-cloud-file.d.ts +83 -0
  53. package/dist/v5/point-cloud-potree.d.ts +47 -0
  54. package/dist/v5/point-cloud-source-encoder.d.ts +1 -1
  55. package/dist/v5/point-cloud-source.d.ts +1 -1
  56. package/dist/v5/point-cloud-tileset.d.ts +44 -0
  57. package/dist/v5/point-cloud.d.ts +1 -1
  58. package/dist/v5/resource-manifest.d.ts +10 -1
  59. package/dist/v5/selected-mesh-source.d.ts +53 -0
  60. package/dist/v5/spatial-conversion.d.ts +1 -1
  61. package/dist/v5/tileset-source.d.ts +1 -1
  62. package/package.json +25 -32
  63. package/src/index.ts +4 -1
  64. package/src/v4/3d-tiles-converter/3d-tiles-converter.ts +11 -12
  65. package/src/v4/3d-tiles-converter/helpers/3d-tiles-content-converter.ts +7 -3
  66. package/src/v4/3d-tiles-converter/helpers/i3s-obb-to-3d-tiles-obb.ts +2 -2
  67. package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +4 -6
  68. package/src/v4/3d-tiles-converter/helpers/texture-atlas.ts +1 -1
  69. package/src/v4/3d-tiles-converter/helpers/validate-i3s-spatial-reference.ts +2 -2
  70. package/src/v4/3d-tiles-converter/json-templates/tileset.ts +58 -39
  71. package/src/v4/converter-cli.ts +10 -4
  72. package/src/v4/deps-installer/deps-installer.ts +3 -3
  73. package/src/v4/i3s-converter/helpers/attribute-metadata-info.ts +1 -1
  74. package/src/v4/i3s-converter/helpers/batch-ids-extensions.ts +9 -10
  75. package/src/v4/i3s-converter/helpers/conversion-gltf.ts +108 -0
  76. package/src/v4/i3s-converter/helpers/coordinate-converter.ts +6 -5
  77. package/src/v4/i3s-converter/helpers/create-scene-server-path.ts +5 -6
  78. package/src/v4/i3s-converter/helpers/feature-attributes.ts +4 -4
  79. package/src/v4/i3s-converter/helpers/geometry-attributes.ts +1 -1
  80. package/src/v4/i3s-converter/helpers/geometry-converter.ts +69 -77
  81. package/src/v4/i3s-converter/helpers/gltf-attributes.ts +58 -74
  82. package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +6 -54
  83. package/src/v4/i3s-converter/helpers/node-debug.ts +7 -3
  84. package/src/v4/i3s-converter/helpers/node-index-document.ts +11 -9
  85. package/src/v4/i3s-converter/helpers/node-pages.ts +3 -4
  86. package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +2 -2
  87. package/src/v4/i3s-converter/helpers/tileset-traversal.ts +1 -1
  88. package/src/v4/i3s-converter/i3s-converter.ts +29 -31
  89. package/src/v4/i3s-converter/json-templates/geometry-definitions.ts +71 -66
  90. package/src/v4/i3s-converter/json-templates/layers.ts +97 -132
  91. package/src/v4/i3s-converter/json-templates/metadata.ts +31 -21
  92. package/src/v4/i3s-converter/json-templates/node.ts +60 -74
  93. package/src/v4/i3s-converter/json-templates/scene-server.ts +36 -27
  94. package/src/v4/i3s-converter/json-templates/shared-resources.ts +62 -123
  95. package/src/v4/i3s-converter/json-templates/store.ts +39 -57
  96. package/src/v4/i3s-converter/types.ts +4 -4
  97. package/src/v4/i3s-server/controllers/slpk-controller.ts +3 -3
  98. package/src/v4/i3s-server/utils/create-scene-server.ts +3 -3
  99. package/src/v4/lib/utils/builder-input.ts +8 -0
  100. package/src/v4/lib/utils/conversion-dump.ts +5 -1
  101. package/src/v4/lib/utils/file-utils.ts +1 -1
  102. package/src/v4/lib/utils/hash-string.ts +21 -0
  103. package/src/v4/lib/utils/lod-conversion-utils.ts +3 -3
  104. package/src/v4/pgm-loader.ts +2 -2
  105. package/src/v5/adapters.ts +53 -0
  106. package/src/v5/archive-source.ts +235 -0
  107. package/src/v5/browser-tileset-source.ts +418 -48
  108. package/src/v5/browser.ts +46 -0
  109. package/src/v5/conversion-api.ts +184 -17
  110. package/src/v5/conversion-archive.ts +2 -2
  111. package/src/v5/converter-cli.ts +28 -0
  112. package/src/v5/core.ts +7 -0
  113. package/src/v5/feature-arrow.ts +2 -2
  114. package/src/v5/i3s-mesh-conversion.ts +41 -3
  115. package/src/v5/i3s-mesh-sink.ts +1 -1
  116. package/src/v5/i3s-mesh-source-features.ts +3 -3
  117. package/src/v5/i3s-mesh-source-material.ts +18 -11
  118. package/src/v5/i3s-mesh-source.ts +3 -2
  119. package/src/v5/i3s-selection-source.ts +298 -0
  120. package/src/v5/index.ts +41 -0
  121. package/src/v5/mesh-conversion.ts +24 -4
  122. package/src/v5/mesh-draco.ts +2 -2
  123. package/src/v5/mesh-features.ts +3 -3
  124. package/src/v5/mesh-hierarchy.ts +648 -0
  125. package/src/v5/mesh-source-features.ts +165 -51
  126. package/src/v5/mesh-source-schema.ts +140 -0
  127. package/src/v5/mesh-source-texture.ts +49 -19
  128. package/src/v5/mesh-source.ts +80 -49
  129. package/src/v5/mesh-tileset-archive.ts +1 -1
  130. package/src/v5/mesh-tileset-sink.ts +2 -2
  131. package/src/v5/mesh.ts +1 -1
  132. package/src/v5/node-cli.ts +201 -0
  133. package/src/v5/node-conversion.ts +362 -0
  134. package/src/v5/node-feature-schema.ts +155 -0
  135. package/src/v5/node-i3s-source.ts +181 -0
  136. package/src/v5/node-source.ts +328 -0
  137. package/src/v5/node.ts +16 -0
  138. package/src/v5/point-cloud-attributes.ts +113 -0
  139. package/src/v5/point-cloud-conversion.ts +1 -1
  140. package/src/v5/point-cloud-copc.ts +289 -0
  141. package/src/v5/point-cloud-file.ts +306 -0
  142. package/src/v5/point-cloud-potree.ts +134 -0
  143. package/src/v5/point-cloud-source-encoder.ts +1 -1
  144. package/src/v5/point-cloud-source.ts +1 -1
  145. package/src/v5/point-cloud-spatial.ts +2 -2
  146. package/src/v5/point-cloud-tileset.ts +137 -0
  147. package/src/v5/point-cloud.ts +3 -3
  148. package/src/v5/resource-fetcher.ts +4 -1
  149. package/src/v5/resource-manifest.ts +40 -5
  150. package/src/v5/selected-mesh-source.ts +384 -0
  151. package/src/v5/spatial-conversion.ts +90 -19
  152. package/src/v5/tileset-source.ts +2 -2
package/dist/v5/core.js CHANGED
@@ -27,9 +27,23 @@ async function convertTileset(options) {
27
27
  measureOutputBytes,
28
28
  maxInputResourceBytes = Number.POSITIVE_INFINITY,
29
29
  maxOutputResourceBytes = Number.POSITIVE_INFINITY,
30
+ maxInputBytes = Number.POSITIVE_INFINITY,
31
+ maxOutputBytes = Number.POSITIVE_INFINITY,
32
+ maxInputResources = Number.POSITIVE_INFINITY,
33
+ maxOutputResources = Number.POSITIVE_INFINITY,
30
34
  signal,
31
35
  onProgress
32
36
  } = options;
37
+ const started = options.profile ? performance.now() : 0;
38
+ const timings = options.profile ? {
39
+ inspectMilliseconds: 0,
40
+ readMilliseconds: 0,
41
+ convertMilliseconds: 0,
42
+ validateMilliseconds: 0,
43
+ writeMilliseconds: 0,
44
+ finalizeMilliseconds: 0,
45
+ totalMilliseconds: 0
46
+ } : void 0;
33
47
  let inputResources = 0;
34
48
  let outputResources = 0;
35
49
  let inputBytes = 0;
@@ -40,16 +54,34 @@ async function convertTileset(options) {
40
54
  onProgress?.({ phase, inputResources, outputResources, inputBytes, outputBytes });
41
55
  };
42
56
  try {
43
- if (maxInputResourceBytes !== Number.POSITIVE_INFINITY && (!Number.isSafeInteger(maxInputResourceBytes) || maxInputResourceBytes < 0)) {
44
- throw new TileConversionError(
45
- "INVALID_INPUT_RESOURCE_LIMIT",
46
- "maxInputResourceBytes must be a nonnegative safe integer or Infinity"
47
- );
57
+ for (const [name, limit, code] of [
58
+ ["maxInputResourceBytes", maxInputResourceBytes, "INVALID_INPUT_RESOURCE_LIMIT"],
59
+ ["maxOutputResourceBytes", maxOutputResourceBytes, "INVALID_OUTPUT_RESOURCE_LIMIT"],
60
+ ["maxInputBytes", maxInputBytes, "INVALID_INPUT_BYTES_LIMIT"],
61
+ ["maxOutputBytes", maxOutputBytes, "INVALID_OUTPUT_BYTES_LIMIT"],
62
+ ["maxInputResources", maxInputResources, "INVALID_INPUT_COUNT_LIMIT"],
63
+ ["maxOutputResources", maxOutputResources, "INVALID_OUTPUT_COUNT_LIMIT"]
64
+ ]) {
65
+ if (limit !== Infinity && (!Number.isSafeInteger(limit) || limit < 0)) {
66
+ throw new TileConversionError(
67
+ code,
68
+ name + " must be a nonnegative safe integer or Infinity"
69
+ );
70
+ }
48
71
  }
49
72
  reportProgress("inspect");
50
- const inspection = await inspectTileset(source, signal);
51
- for await (const inputResource of source.read(inspection, signal)) {
73
+ const inspection = await measureConversionOperation(
74
+ timings,
75
+ "inspectMilliseconds",
76
+ () => inspectTileset(source, signal)
77
+ );
78
+ for await (const inputResource of measureConversionIterable(
79
+ source.read(inspection, signal),
80
+ timings,
81
+ "readMilliseconds"
82
+ )) {
52
83
  throwIfAborted(signal);
84
+ checkConversionTotal(inputResources, 1, maxInputResources, "INPUT_COUNT_LIMIT_EXCEEDED");
53
85
  const inputResourceBytes = measureInputBytes(inputResource);
54
86
  if (!Number.isSafeInteger(inputResourceBytes) || inputResourceBytes < 0) {
55
87
  throw new TileConversionError(
@@ -63,23 +95,53 @@ async function convertTileset(options) {
63
95
  `Input resource is ${inputResourceBytes} bytes, exceeding the configured limit of ${maxInputResourceBytes} bytes`
64
96
  );
65
97
  }
98
+ checkConversionTotal(
99
+ inputBytes,
100
+ inputResourceBytes,
101
+ maxInputBytes,
102
+ "INPUT_BYTES_LIMIT_EXCEEDED"
103
+ );
66
104
  inputResources++;
67
105
  inputBytes += inputResourceBytes;
68
106
  reportProgress("convert");
69
- const convertedResources = await codec.convert(inputResource, inspection, signal);
70
- for await (const outputResource of convertedResources) {
107
+ const convertedResources = await measureConversionOperation(
108
+ timings,
109
+ "convertMilliseconds",
110
+ () => codec.convert(inputResource, inspection, signal)
111
+ );
112
+ for await (const outputResource of measureConversionIterable(
113
+ convertedResources,
114
+ timings,
115
+ "convertMilliseconds"
116
+ )) {
71
117
  throwIfAborted(signal);
118
+ checkConversionTotal(outputResources, 1, maxOutputResources, "OUTPUT_COUNT_LIMIT_EXCEEDED");
72
119
  reportProgress("validate");
73
- const resourceDiagnostics = await codec.validateOutput?.(outputResource, signal) || [];
120
+ const resourceDiagnostics = await measureConversionOperation(
121
+ timings,
122
+ "validateMilliseconds",
123
+ () => codec.validateOutput?.(outputResource, signal)
124
+ ) || [];
74
125
  diagnostics.push(...resourceDiagnostics);
75
126
  const outputResourceBytes = measureOutputBytes(outputResource);
76
- largestOutputResourceBytes = Math.max(largestOutputResourceBytes, outputResourceBytes);
127
+ if (!Number.isSafeInteger(outputResourceBytes) || outputResourceBytes < 0) {
128
+ throw new TileConversionError(
129
+ "INVALID_OUTPUT_RESOURCE_SIZE",
130
+ "measureOutputBytes must return a nonnegative safe integer"
131
+ );
132
+ }
77
133
  if (outputResourceBytes > maxOutputResourceBytes) {
78
134
  throw new TileConversionError(
79
135
  "OUTPUT_RESOURCE_TOO_LARGE",
80
136
  `Output resource is ${outputResourceBytes} bytes, exceeding the configured limit of ${maxOutputResourceBytes} bytes`
81
137
  );
82
138
  }
139
+ checkConversionTotal(
140
+ outputBytes,
141
+ outputResourceBytes,
142
+ maxOutputBytes,
143
+ "OUTPUT_BYTES_LIMIT_EXCEEDED"
144
+ );
83
145
  if (resourceDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
84
146
  throw new TileConversionError(
85
147
  "OUTPUT_VALIDATION_FAILED",
@@ -88,8 +150,13 @@ async function convertTileset(options) {
88
150
  );
89
151
  }
90
152
  throwIfAborted(signal);
91
- await sink.write(outputResource, signal);
153
+ await measureConversionOperation(
154
+ timings,
155
+ "writeMilliseconds",
156
+ () => sink.write(outputResource, signal)
157
+ );
92
158
  throwIfAborted(signal);
159
+ largestOutputResourceBytes = Math.max(largestOutputResourceBytes, outputResourceBytes);
93
160
  outputResources++;
94
161
  outputBytes += outputResourceBytes;
95
162
  reportProgress("convert");
@@ -106,8 +173,9 @@ async function convertTileset(options) {
106
173
  };
107
174
  throwIfAborted(signal);
108
175
  reportProgress("finalize");
109
- await sink.finalize(report);
110
- return report;
176
+ await measureConversionOperation(timings, "finalizeMilliseconds", () => sink.finalize(report));
177
+ if (timings) timings.totalMilliseconds = Math.max(0, performance.now() - started);
178
+ return timings ? { ...report, timings } : report;
111
179
  } catch (error) {
112
180
  try {
113
181
  await sink.abort(error);
@@ -134,6 +202,44 @@ function throwIfAborted(signal) {
134
202
  throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
135
203
  }
136
204
  }
205
+ function checkConversionTotal(current, increment, limit, code) {
206
+ if (increment > limit - current) {
207
+ throw new TileConversionError(
208
+ code,
209
+ `Conversion exceeds cumulative limit ${limit}: ${current} processed, next resource adds ${increment}`
210
+ );
211
+ }
212
+ if (increment > Number.MAX_SAFE_INTEGER - current) {
213
+ throw new TileConversionError(
214
+ "RESOURCE_TOTAL_OVERFLOW",
215
+ "Conversion totals must remain safe integers"
216
+ );
217
+ }
218
+ }
219
+ function measureConversionOperation(timings, stage, operation) {
220
+ if (!timings) return operation();
221
+ const started = performance.now();
222
+ return (async () => {
223
+ try {
224
+ return await operation();
225
+ } finally {
226
+ timings[stage] += Math.max(0, performance.now() - started);
227
+ }
228
+ })();
229
+ }
230
+ function measureConversionIterable(iterable, timings, stage) {
231
+ if (!timings) return iterable;
232
+ return {
233
+ [Symbol.asyncIterator]() {
234
+ const iterator = iterable[Symbol.asyncIterator]();
235
+ return {
236
+ next: async () => measureConversionOperation(timings, stage, () => iterator.next()),
237
+ ...iterator.return ? { return: iterator.return.bind(iterator) } : {},
238
+ ...iterator.throw ? { throw: iterator.throw.bind(iterator) } : {}
239
+ };
240
+ }
241
+ };
242
+ }
137
243
 
138
244
  // src/v5/browser-source.ts
139
245
  function createBrowserTileConversionSource(options) {
@@ -271,7 +377,7 @@ import {
271
377
  I3SSpatialTransformer,
272
378
  markTilesetSpatialReferenceTransformed,
273
379
  Tiles3DSpatialTransformer
274
- } from "@loaders.gl/tiles";
380
+ } from "@loaders.gl/tiles-3d";
275
381
  import { Matrix4 } from "@math.gl/core";
276
382
  function createTiles3DConversionSpatialContext(discovered, options = {}) {
277
383
  const spatialReference = createConversionSpatialReference(discovered, options);
@@ -291,8 +397,8 @@ function createTiles3DConversionSpatialContext(discovered, options = {}) {
291
397
  const outputSpatialReference = transformer ? markTilesetSpatialReferenceTransformed(spatialReference) : spatialReference;
292
398
  return {
293
399
  spatialReference: outputSpatialReference,
294
- transformPositions: (positions) => transformer ? transformer.transformPositions(positions) : Float64Array.from(positions),
295
- transformNormals: (normals, positions) => transformer ? transformer.transformNormals(normals, positions) : Float32Array.from(normals),
400
+ transformPositions: (positions) => transformer ? transformer.transformPositions(positions) : copyNativePositions(positions),
401
+ transformNormals: (normals, positions) => transformer ? transformer.transformNormals(normals, positions) : copyNativeNormals(normals, positions),
296
402
  transformBoundingVolume: (volume, sourceTransform) => transformer ? transformer.transformBoundingVolume(volume, sourceTransform) : cloneBoundingVolume(volume)
297
403
  };
298
404
  }
@@ -314,16 +420,9 @@ function createI3SConversionSpatialContext(discovered, options = {}) {
314
420
  const outputSpatialReference = transformer ? transformer.spatialReference : spatialReference;
315
421
  let nativeNormalTransformer;
316
422
  const transformNativeNormals = (normals, positions, normalReferenceFrame) => {
423
+ validateNormalReferenceFrame(normalReferenceFrame);
424
+ const targetNormals = copyNativeNormals(normals, positions);
317
425
  if (!spatialReference.sourceCrs || ["local", "cartesian"].includes(spatialReference.coordinateFrame)) {
318
- const targetNormals = Float32Array.from(normals);
319
- for (let index = 0; index < normals.length; index += 3) {
320
- const length = Math.hypot(normals[index], normals[index + 1], normals[index + 2]);
321
- if (length) {
322
- for (let axis = 0; axis < 3; axis++) {
323
- targetNormals[index + axis] = normals[index + axis] / length;
324
- }
325
- }
326
- }
327
426
  return targetNormals;
328
427
  }
329
428
  nativeNormalTransformer ||= createSpatialTransformer(
@@ -337,7 +436,7 @@ function createI3SConversionSpatialContext(discovered, options = {}) {
337
436
  options
338
437
  )
339
438
  );
340
- return nativeNormalTransformer.transformNormals(normals, positions, normalReferenceFrame);
439
+ return nativeNormalTransformer.transformNormals(targetNormals, positions, normalReferenceFrame);
341
440
  };
342
441
  return {
343
442
  spatialReference: outputSpatialReference,
@@ -358,7 +457,7 @@ function createI3SConversionSpatialContext(discovered, options = {}) {
358
457
  };
359
458
  },
360
459
  transformPositionsAsync: (positions, sourceOrigin) => transformer ? transformer.transformPositionsAsync(positions, sourceOrigin) : Promise.resolve(createNativeI3SPositions(positions, sourceOrigin, spatialReference)),
361
- transformNormals: (normals, positions, normalReferenceFrame) => transformer ? transformer.transformNormals(normals, positions, normalReferenceFrame) : Float32Array.from(normals),
460
+ transformNormals: (normals, positions, normalReferenceFrame) => transformer ? transformer.transformNormals(normals, positions, normalReferenceFrame) : transformNativeNormals(normals, positions, normalReferenceFrame),
362
461
  transformBoundingVolume: (bounds) => transformer ? transformer.transformBoundingVolume(bounds) : cloneI3SBounds(bounds),
363
462
  transformBoundingVolumeAsync: (bounds) => transformer ? transformer.transformBoundingVolumeAsync(bounds) : Promise.resolve(cloneI3SBounds(bounds))
364
463
  };
@@ -442,21 +541,30 @@ function cloneI3SBounds(bounds) {
442
541
  };
443
542
  }
444
543
  function createNativeI3SPositions(sourcePositions, sourceOrigin, spatialReference) {
445
- if (sourcePositions.length % 3 !== 0 || sourceOrigin.length < 3) {
446
- throw new Error("I3S positions and origin must contain complete xyz coordinates");
544
+ const sourcePositions64 = copyNativePositions(sourcePositions);
545
+ if (sourceOrigin.length !== 3 || Array.from(sourceOrigin).some((value) => !Number.isFinite(value))) {
546
+ throw new TileConversionError(
547
+ "SPATIAL_INPUT_INVALID",
548
+ "I3S origins require three finite coordinates"
549
+ );
447
550
  }
448
551
  const origin = [
449
552
  Number(sourceOrigin[0]),
450
553
  Number(sourceOrigin[1]),
451
554
  Number(sourceOrigin[2])
452
555
  ];
453
- const sourcePositions64 = Float64Array.from(sourcePositions);
454
556
  const positions = new Float32Array(sourcePositions.length);
455
557
  for (let index = 0; index < sourcePositions.length; index += 3) {
456
558
  positions[index] = Number(sourcePositions[index]) - origin[0];
457
559
  positions[index + 1] = Number(sourcePositions[index + 1]) - origin[1];
458
560
  positions[index + 2] = Number(sourcePositions[index + 2]) - origin[2];
459
561
  }
562
+ if (positions.some((value) => !Number.isFinite(value))) {
563
+ throw new TileConversionError(
564
+ "SPATIAL_INPUT_INVALID",
565
+ "Native I3S offsets exceed float32 range"
566
+ );
567
+ }
460
568
  const isGeographic = spatialReference.coordinateFrame === "geographic";
461
569
  const modelMatrix = isGeographic ? new Matrix4() : new Matrix4().translate(origin);
462
570
  return {
@@ -468,9 +576,64 @@ function createNativeI3SPositions(sourcePositions, sourceOrigin, spatialReferenc
468
576
  modelMatrix
469
577
  };
470
578
  }
579
+ function copyNativePositions(positions) {
580
+ validateNativePositions(positions);
581
+ return Float64Array.from(positions);
582
+ }
583
+ function validateNativePositions(positions) {
584
+ if (positions.length % 3) {
585
+ throw new TileConversionError(
586
+ "SPATIAL_INPUT_INVALID",
587
+ "Positions require complete finite xyz coordinates"
588
+ );
589
+ }
590
+ for (let index = 0; index < positions.length; index++) {
591
+ if (!Number.isFinite(positions[index])) {
592
+ throw new TileConversionError(
593
+ "SPATIAL_INPUT_INVALID",
594
+ "Positions require complete finite xyz coordinates"
595
+ );
596
+ }
597
+ }
598
+ }
599
+ function copyNativeNormals(normals, positions) {
600
+ validateNativePositions(positions);
601
+ if (normals.length !== positions.length) {
602
+ throw new TileConversionError(
603
+ "SPATIAL_INPUT_INVALID",
604
+ "Normals require finite xyz values matching positions"
605
+ );
606
+ }
607
+ const output = new Float32Array(normals.length);
608
+ for (let index = 0; index < normals.length; index += 3) {
609
+ const scale = Math.max(
610
+ Math.abs(normals[index]),
611
+ Math.abs(normals[index + 1]),
612
+ Math.abs(normals[index + 2])
613
+ );
614
+ if (!Number.isFinite(scale)) {
615
+ throw new TileConversionError(
616
+ "SPATIAL_INPUT_INVALID",
617
+ "Normals require finite xyz values matching positions"
618
+ );
619
+ }
620
+ const length = scale ? Math.hypot(normals[index] / scale, normals[index + 1] / scale, normals[index + 2] / scale) : 0;
621
+ for (let axis = 0; axis < 3; axis++)
622
+ output[index + axis] = length ? normals[index + axis] / scale / length : 0;
623
+ }
624
+ return output;
625
+ }
626
+ function validateNormalReferenceFrame(frame) {
627
+ if (frame !== void 0 && !["earth-centered", "vertex-reference-frame"].includes(frame)) {
628
+ throw new TileConversionError(
629
+ "SPATIAL_INPUT_INVALID",
630
+ "Unsupported I3S normal reference frame"
631
+ );
632
+ }
633
+ }
471
634
 
472
635
  // src/v5/tileset-source.ts
473
- import { traverseTilesetContents } from "@loaders.gl/tiles";
636
+ import { traverseTilesetContents } from "@loaders.gl/tiles-3d";
474
637
  function createTilesetConversionSource(tileset, options = {}) {
475
638
  return {
476
639
  async inspect(signal) {
@@ -492,10 +655,17 @@ function createTilesetConversionSource(tileset, options = {}) {
492
655
  // src/v5/resource-manifest.ts
493
656
  var TILE_RESOURCE_MANIFEST_VERSION = 1;
494
657
  async function createManifestBackedTileConversionSink(options) {
658
+ const maxResources = options.maxResources ?? Infinity;
659
+ if (maxResources !== Infinity && (!Number.isSafeInteger(maxResources) || maxResources < 0)) {
660
+ throw new TileConversionError(
661
+ "INVALID_MANIFEST_RESOURCE_LIMIT",
662
+ "maxResources must be a nonnegative safe integer or Infinity"
663
+ );
664
+ }
495
665
  const priorManifest = await options.manifestStore.load();
496
666
  const resources = /* @__PURE__ */ new Map();
497
667
  if (priorManifest) {
498
- validateManifest(priorManifest);
668
+ validateManifest(priorManifest, maxResources);
499
669
  for (const entry of priorManifest.resources) {
500
670
  resources.set(entry.resourceId, entry);
501
671
  }
@@ -521,6 +691,7 @@ async function createManifestBackedTileConversionSink(options) {
521
691
  if (writeFailed) {
522
692
  throw writeFailure;
523
693
  }
694
+ signal?.throwIfAborted();
524
695
  const entry = createManifestEntry(options, resource);
525
696
  const priorEntry = resources.get(entry.resourceId);
526
697
  if (priorEntry) {
@@ -530,7 +701,14 @@ async function createManifestBackedTileConversionSink(options) {
530
701
  `Resource "${entry.resourceId}" changed since the saved manifest`
531
702
  );
532
703
  }
533
- return;
704
+ const verified = options.verifyResource ? await options.verifyResource({ ...priorEntry }, signal) : true;
705
+ signal?.throwIfAborted();
706
+ if (verified === true) return;
707
+ } else if (resources.size >= maxResources) {
708
+ throw new TileConversionError(
709
+ "MANIFEST_RESOURCE_LIMIT_EXCEEDED",
710
+ "Resource checkpoint exceeds maxResources"
711
+ );
534
712
  }
535
713
  await options.sink.write(resource, signal);
536
714
  resources.set(entry.resourceId, entry);
@@ -579,11 +757,17 @@ function createManifestEntry(options, resource) {
579
757
  }
580
758
  return { resourceId, byteLength, fingerprint };
581
759
  }
582
- function validateManifest(manifest) {
760
+ function validateManifest(manifest, maxResources) {
583
761
  if (typeof manifest !== "object" || manifest === null || manifest.version !== TILE_RESOURCE_MANIFEST_VERSION || typeof manifest.complete !== "boolean" || !Array.isArray(manifest.resources)) {
584
762
  throw new TileConversionError(
585
763
  "UNSUPPORTED_RESOURCE_MANIFEST",
586
- `Unsupported or malformed resource manifest version: ${String(manifest.version)}`
764
+ `Unsupported or malformed resource manifest version: ${String(manifest?.version)}`
765
+ );
766
+ }
767
+ if (manifest.resources.length > maxResources) {
768
+ throw new TileConversionError(
769
+ "MANIFEST_RESOURCE_LIMIT_EXCEEDED",
770
+ "Loaded manifest exceeds maxResources"
587
771
  );
588
772
  }
589
773
  const resourceIds = /* @__PURE__ */ new Set();
@@ -675,50 +859,67 @@ function measureBlobPart(part) {
675
859
 
676
860
  // src/v5/browser-tileset-source.ts
677
861
  function createBrowserTilesetConversionSource(options) {
678
- if (!Number.isSafeInteger(options.maxInputBytes) || options.maxInputBytes < 1 || !Number.isSafeInteger(options.maxInputResources) || options.maxInputResources < 1) {
862
+ const maxTilesetDocuments = options.maxTilesetDocuments ?? 1;
863
+ if (options.inspectNestedTilesets !== void 0 && typeof options.inspectNestedTilesets !== "boolean" || !Number.isSafeInteger(maxTilesetDocuments) || maxTilesetDocuments < 1 || !Number.isSafeInteger(options.maxInputBytes) || options.maxInputBytes < 1 || !Number.isSafeInteger(options.maxInputResources) || options.maxInputResources < 1) {
679
864
  throw new TileConversionError(
680
865
  "INVALID_BROWSER_SOURCE_OPTIONS",
681
- "Browser tileset sources require positive byte and resource limits"
866
+ "Browser tileset sources require positive byte, declaration and document limits"
682
867
  );
683
868
  }
684
869
  const fetcher = options.fetcher ?? fetch;
685
870
  return {
686
871
  async inspect(signal) {
687
872
  throwIfAborted4(signal);
688
- const rootUrl = new URL(options.input).href;
689
- const response = await fetcher(rootUrl, { signal });
873
+ const requestedUrl = new URL(options.input).href;
874
+ const response = await fetcher(requestedUrl, { signal });
875
+ const rootUrl = response.url || requestedUrl;
690
876
  if (!response.ok) {
877
+ await response.body?.cancel();
691
878
  throw new TileConversionError(
692
879
  "BROWSER_SOURCE_FETCH_FAILED",
693
880
  `Could not fetch browser tileset (${response.status} ${response.statusText})`
694
881
  );
695
882
  }
696
883
  const data = await readResponseBytes2(response, options.maxInputBytes, signal);
697
- let tileset;
698
- try {
699
- tileset = JSON.parse(new TextDecoder().decode(data));
700
- } catch {
701
- throw new TileConversionError("INVALID_BROWSER_TILESET", "Tileset input is not valid JSON");
702
- }
703
- if (!isRecord(tileset) || !["1.0", "1.1"].includes(tileset.asset?.version || "") || !isRecord(tileset.root)) {
704
- throw new TileConversionError(
705
- "UNSUPPORTED_BROWSER_TILESET",
706
- "Browser tileset traversal supports explicit 3D Tiles 1.0 and 1.1 documents"
707
- );
708
- }
884
+ const tileset = parseTilesetDocument(data);
709
885
  const resources = collectResources(tileset.root, options.maxInputResources);
710
- return { rootUrl, tileset, resources, tilesetBytes: data.byteLength };
886
+ const inspection = { rootUrl, tileset, resources, tilesetBytes: data.byteLength };
887
+ return options.inspectNestedTilesets ? expandInspection(inspection, options, maxTilesetDocuments, fetcher, signal) : inspection;
711
888
  },
712
889
  async *read(inspection, signal) {
713
890
  let inputBytes = inspection.tilesetBytes;
714
- for (const descriptor of inspection.resources) {
891
+ let declarations = inspection.resources.length;
892
+ let documents = 1;
893
+ const stack = [
894
+ {
895
+ resources: inspection.resources,
896
+ index: 0,
897
+ tileset: inspection.tileset,
898
+ url: inspection.rootUrl,
899
+ ancestors: /* @__PURE__ */ new Set([getDocumentKey(inspection.rootUrl), getDocumentKey(options.input)])
900
+ }
901
+ ];
902
+ while (stack.length) {
715
903
  throwIfAborted4(signal);
716
- const resourceUrl = resolveResourceUrl(descriptor.uri, inspection.rootUrl);
904
+ const frame = stack[stack.length - 1];
905
+ if (frame.index === frame.resources.length) {
906
+ stack.pop();
907
+ continue;
908
+ }
909
+ const descriptor = frame.resources[frame.index++];
910
+ const resourceUrl = resolveResourceUrl(descriptor.uri, descriptor.tilesetUrl ?? frame.url);
911
+ if (frame.ancestors.has(getDocumentKey(resourceUrl))) {
912
+ throw new TileConversionError(
913
+ "BROWSER_TILESET_CYCLE",
914
+ "External tileset references an active ancestor document"
915
+ );
916
+ }
717
917
  const response = await fetcher(resourceUrl, { signal });
718
918
  if (!response.ok) {
919
+ await response.body?.cancel();
719
920
  throw new TileConversionError(
720
921
  "BROWSER_SOURCE_FETCH_FAILED",
721
- `Could not fetch browser tile content (${response.status} ${response.statusText})`,
922
+ "Could not fetch browser tile content (" + response.status + " " + response.statusText + ")",
722
923
  [
723
924
  {
724
925
  code: "BROWSER_SOURCE_FETCH_FAILED",
@@ -729,28 +930,280 @@ function createBrowserTilesetConversionSource(options) {
729
930
  ]
730
931
  );
731
932
  }
732
- const remainingBytes = options.maxInputBytes - inputBytes;
733
- const data = await readResponseBytes2(response, remainingBytes, signal);
933
+ const documentUrl = response.url || resourceUrl;
934
+ if (frame.ancestors.has(getDocumentKey(documentUrl))) {
935
+ await response.body?.cancel();
936
+ throw new TileConversionError(
937
+ "BROWSER_TILESET_CYCLE",
938
+ "Redirected content references an active ancestor document"
939
+ );
940
+ }
941
+ const data = await readResponseBytes2(response, options.maxInputBytes - inputBytes, signal);
734
942
  inputBytes += data.byteLength;
735
943
  const contentType = response.headers.get("content-type") ?? void 0;
736
944
  if (isJsonContent(data, contentType)) {
737
- throw new TileConversionError(
738
- "UNSUPPORTED_BROWSER_TILESET_CONTENT",
739
- `Nested tilesets and JSON tile content are not supported: ${descriptor.uri}`
740
- );
945
+ if (inspection.documents)
946
+ throw new TileConversionError(
947
+ "UNSUPPORTED_BROWSER_TILESET_CONTENT",
948
+ "Expanded inspection selections must be binary content; declare external metadata with a .json URI"
949
+ );
950
+ if (maxTilesetDocuments === 1) {
951
+ throw new TileConversionError(
952
+ "UNSUPPORTED_BROWSER_TILESET_CONTENT",
953
+ "Nested tilesets and JSON tile content require an explicit document budget"
954
+ );
955
+ }
956
+ if (documents >= maxTilesetDocuments) {
957
+ throw new TileConversionError(
958
+ "INPUT_TILESET_COUNT_EXCEEDED",
959
+ "Browser tileset exceeds its document limit"
960
+ );
961
+ }
962
+ let tile = frame.tileset.root;
963
+ for (const childIndex of descriptor.tilePath) tile = getTileChild(tile, childIndex);
964
+ if (tile.children?.length) {
965
+ throw new TileConversionError(
966
+ "INVALID_BROWSER_TILESET",
967
+ "External tileset references cannot also have child tiles"
968
+ );
969
+ }
970
+ const tileset = parseTilesetDocument(data);
971
+ const resources = collectResources(
972
+ tileset.root,
973
+ options.maxInputResources - declarations,
974
+ descriptor.transform
975
+ ).map((resource) => ({
976
+ ...resource,
977
+ resourceId: descriptor.resourceId + "/" + resource.resourceId,
978
+ tilesetUrl: documentUrl
979
+ }));
980
+ declarations += resources.length;
981
+ documents++;
982
+ stack.push({
983
+ resources,
984
+ index: 0,
985
+ tileset,
986
+ url: documentUrl,
987
+ ancestors: /* @__PURE__ */ new Set([
988
+ ...frame.ancestors,
989
+ getDocumentKey(resourceUrl),
990
+ getDocumentKey(documentUrl)
991
+ ])
992
+ });
993
+ continue;
741
994
  }
742
- yield { ...descriptor, data, ...contentType ? { contentType } : {} };
995
+ yield { ...descriptor, data, contentUrl: documentUrl, ...contentType ? { contentType } : {} };
743
996
  }
744
997
  }
745
998
  };
746
999
  }
747
- function collectResources(root, maxInputResources) {
1000
+ function selectBrowserTilesetContent(inspection, descriptor) {
1001
+ const placement = inspection.documents?.[descriptor.documentIndex ?? 0];
1002
+ if (inspection.documents && (!Number.isSafeInteger(descriptor.documentIndex) || !placement))
1003
+ throw new TileConversionError(
1004
+ "INVALID_BROWSER_SELECTION",
1005
+ "Content descriptor has no inspected document placement"
1006
+ );
1007
+ const document = structuredClone(placement?.tileset ?? inspection.tileset);
1008
+ const originalRoot = document.root;
1009
+ let transform = [...placement?.parentTransform ?? identityMatrix()];
1010
+ let maximum = placement?.parentGeometricError ?? 0;
1011
+ let tile = originalRoot;
1012
+ for (let depth = 0; ; depth++) {
1013
+ transform = multiplyMatrices(transform, normalizeMatrix(tile.transform));
1014
+ if (typeof tile.geometricError !== "number" || !Number.isFinite(tile.geometricError) || tile.geometricError < 0)
1015
+ throw new TileConversionError(
1016
+ "MESH_GEOMETRIC_ERROR_INVALID",
1017
+ "Every selected ancestor must declare a finite nonnegative geometricError"
1018
+ );
1019
+ maximum = Math.max(maximum, tile.geometricError * getTransformScale(transform));
1020
+ if (depth === descriptor.tilePath.length) break;
1021
+ const child = getTileChild(tile, descriptor.tilePath[depth]);
1022
+ delete tile.content;
1023
+ delete tile.contents;
1024
+ tile.children = [child];
1025
+ tile = child;
1026
+ }
1027
+ if (placement && placement.parentGeometricError === void 0 && descriptor.documentIndex !== 0)
1028
+ throw new TileConversionError(
1029
+ "MESH_GEOMETRIC_ERROR_INVALID",
1030
+ "External ancestors must declare geometricError"
1031
+ );
1032
+ const isLeaf = !tile.children?.length;
1033
+ const contentIndex = Number(descriptor.resourceId.match(/content-(\d+)$/)?.[1]);
1034
+ const content = tile.contents?.[contentIndex] ?? tile.content;
1035
+ if (!content || (content.uri ?? content.url) !== descriptor.uri)
1036
+ throw new TileConversionError(
1037
+ "INVALID_BROWSER_SELECTION",
1038
+ "Content descriptor does not match its inspected document"
1039
+ );
1040
+ tile.content = content;
1041
+ delete tile.contents;
1042
+ delete tile.children;
1043
+ if (placement)
1044
+ originalRoot.transform = multiplyMatrices(
1045
+ [...placement.parentTransform],
1046
+ normalizeMatrix(originalRoot.transform)
1047
+ );
1048
+ if (!Number.isFinite(maximum))
1049
+ throw new TileConversionError(
1050
+ "MESH_GEOMETRIC_ERROR_INVALID",
1051
+ "Scaled geometricError must remain finite"
1052
+ );
1053
+ return {
1054
+ tileset: document,
1055
+ contentUrl: resolveResourceUrl(descriptor.uri, placement?.url ?? inspection.rootUrl),
1056
+ geometricError: maximum,
1057
+ isLeaf
1058
+ };
1059
+ }
1060
+ function getTileChild(tile, childIndex) {
1061
+ if (typeof childIndex !== "number" || !Number.isSafeInteger(childIndex) || childIndex < 0 || !Array.isArray(tile.children) || childIndex >= tile.children.length)
1062
+ throw new TileConversionError(
1063
+ "INVALID_BROWSER_SELECTION",
1064
+ "Tile paths require in-bounds nonnegative integer child indexes"
1065
+ );
1066
+ const child = tile.children.at(childIndex);
1067
+ if (!isRecord(child))
1068
+ throw new TileConversionError(
1069
+ "INVALID_BROWSER_SELECTION",
1070
+ "Selected child tile must be an object"
1071
+ );
1072
+ return child;
1073
+ }
1074
+ async function expandInspection(inspection, options, maxTilesetDocuments, fetcher, signal) {
1075
+ const documents = [
1076
+ {
1077
+ url: inspection.rootUrl,
1078
+ tileset: inspection.tileset,
1079
+ parentTransform: identityMatrix(),
1080
+ parentGeometricError: 0
1081
+ }
1082
+ ];
748
1083
  const resources = [];
1084
+ let bytes = inspection.tilesetBytes;
1085
+ let declarations = inspection.resources.length;
749
1086
  const stack = [
750
- { tile: root, path: [], transform: identityMatrix() }
1087
+ {
1088
+ resources: inspection.resources,
1089
+ index: 0,
1090
+ documentIndex: 0,
1091
+ ancestors: /* @__PURE__ */ new Set([getDocumentKey(inspection.rootUrl), getDocumentKey(options.input)])
1092
+ }
1093
+ ];
1094
+ while (stack.length) {
1095
+ throwIfAborted4(signal);
1096
+ const frame = stack[stack.length - 1];
1097
+ if (frame.index === frame.resources.length) {
1098
+ stack.pop();
1099
+ continue;
1100
+ }
1101
+ const descriptor = frame.resources[frame.index++];
1102
+ const placement = documents[frame.documentIndex];
1103
+ const resourceUrl = resolveResourceUrl(descriptor.uri, placement.url);
1104
+ if (!new URL(resourceUrl).pathname.toLowerCase().endsWith(".json")) {
1105
+ resources.push({ ...descriptor, documentIndex: frame.documentIndex });
1106
+ continue;
1107
+ }
1108
+ if (frame.ancestors.has(getDocumentKey(resourceUrl)))
1109
+ throw new TileConversionError(
1110
+ "BROWSER_TILESET_CYCLE",
1111
+ "External tileset references an active ancestor document"
1112
+ );
1113
+ if (documents.length >= maxTilesetDocuments)
1114
+ throw new TileConversionError(
1115
+ "INPUT_TILESET_COUNT_EXCEEDED",
1116
+ "Browser tileset exceeds its document limit"
1117
+ );
1118
+ let tile = placement.tileset.root;
1119
+ for (const childIndex of descriptor.tilePath) tile = tile.children[childIndex];
1120
+ if (tile.children?.length || tile.contents?.length)
1121
+ throw new TileConversionError(
1122
+ "INVALID_BROWSER_TILESET",
1123
+ "Inspected external tilesets require one content and no child tiles"
1124
+ );
1125
+ const response = await fetcher(resourceUrl, { signal });
1126
+ const url = response.url || resourceUrl;
1127
+ if (!response.ok || frame.ancestors.has(getDocumentKey(url))) {
1128
+ await response.body?.cancel();
1129
+ throw new TileConversionError(
1130
+ response.ok ? "BROWSER_TILESET_CYCLE" : "BROWSER_SOURCE_FETCH_FAILED",
1131
+ "Could not inspect external tileset"
1132
+ );
1133
+ }
1134
+ const data = await readResponseBytes2(response, options.maxInputBytes - bytes, signal);
1135
+ bytes += data.byteLength;
1136
+ const tileset = parseTilesetDocument(data);
1137
+ const prefix = descriptor.resourceId + "/";
1138
+ const nestedResources = collectResources(
1139
+ tileset.root,
1140
+ options.maxInputResources - declarations,
1141
+ descriptor.transform
1142
+ ).map((resource) => ({ ...resource, resourceId: prefix + resource.resourceId, tilesetUrl: url }));
1143
+ declarations += nestedResources.length;
1144
+ let parentGeometricError;
1145
+ try {
1146
+ parentGeometricError = selectBrowserTilesetContent(
1147
+ { ...inspection, documents },
1148
+ { ...descriptor, documentIndex: frame.documentIndex }
1149
+ ).geometricError;
1150
+ } catch (error) {
1151
+ if (!(error instanceof TileConversionError) || error.code !== "MESH_GEOMETRIC_ERROR_INVALID")
1152
+ throw error;
1153
+ }
1154
+ const documentIndex = documents.length;
1155
+ documents.push({
1156
+ url,
1157
+ tileset,
1158
+ parentResourceId: descriptor.resourceId,
1159
+ parentTransform: descriptor.transform,
1160
+ parentGeometricError
1161
+ });
1162
+ stack.push({
1163
+ resources: nestedResources,
1164
+ index: 0,
1165
+ documentIndex,
1166
+ ancestors: /* @__PURE__ */ new Set([...frame.ancestors, getDocumentKey(resourceUrl), getDocumentKey(url)])
1167
+ });
1168
+ }
1169
+ return { ...inspection, resources, documents, tilesetBytes: bytes };
1170
+ }
1171
+ function getTransformScale(transform) {
1172
+ const columns = [0, 1, 2].map(
1173
+ (column) => [0, 1, 2].reduce((total, row) => total + Math.abs(transform[column * 4 + row]), 0)
1174
+ );
1175
+ const rows = [0, 1, 2].map(
1176
+ (row) => [0, 1, 2].reduce((total, column) => total + Math.abs(transform[column * 4 + row]), 0)
1177
+ );
1178
+ return Math.sqrt(Math.max(...columns)) * Math.sqrt(Math.max(...rows));
1179
+ }
1180
+ function parseTilesetDocument(data) {
1181
+ let tileset;
1182
+ try {
1183
+ tileset = JSON.parse(new TextDecoder().decode(data));
1184
+ } catch {
1185
+ throw new TileConversionError("INVALID_BROWSER_TILESET", "Tileset input is not valid JSON");
1186
+ }
1187
+ if (!isRecord(tileset) || !["1.0", "1.1"].includes(tileset.asset?.version || "") || !isRecord(tileset.root)) {
1188
+ throw new TileConversionError(
1189
+ "UNSUPPORTED_BROWSER_TILESET",
1190
+ "Browser tileset traversal supports explicit 3D Tiles 1.0 and 1.1 documents"
1191
+ );
1192
+ }
1193
+ return tileset;
1194
+ }
1195
+ function getDocumentKey(url) {
1196
+ const documentUrl = new URL(url);
1197
+ documentUrl.hash = "";
1198
+ return documentUrl.href;
1199
+ }
1200
+ function collectResources(root, maxInputResources, parentTransform = identityMatrix()) {
1201
+ const resources = [];
1202
+ const stack = [
1203
+ { tile: root, path: [], transform: [...parentTransform] }
751
1204
  ];
752
1205
  while (stack.length > 0) {
753
- const { tile, path, transform: parentTransform } = stack.pop();
1206
+ const { tile, path, transform: parentTransform2 } = stack.pop();
754
1207
  if (!isRecord(tile)) {
755
1208
  throw new TileConversionError("INVALID_BROWSER_TILESET", "Tile entries must be objects");
756
1209
  }
@@ -766,7 +1219,19 @@ function collectResources(root, maxInputResources) {
766
1219
  if (tile.children !== void 0 && !Array.isArray(tile.children)) {
767
1220
  throw new TileConversionError("INVALID_BROWSER_TILESET", "Tile children must be an array");
768
1221
  }
769
- const transform = multiplyMatrices(parentTransform, normalizeMatrix(tile.transform));
1222
+ if (tile.content !== void 0 && tile.contents !== void 0) {
1223
+ throw new TileConversionError(
1224
+ "INVALID_BROWSER_TILESET",
1225
+ "Tile content and contents are mutually exclusive"
1226
+ );
1227
+ }
1228
+ const transform = multiplyMatrices(parentTransform2, normalizeMatrix(tile.transform));
1229
+ if (transform.some((value) => !Number.isFinite(value))) {
1230
+ throw new TileConversionError(
1231
+ "INVALID_BROWSER_TILESET",
1232
+ "Composed tile transforms must remain finite"
1233
+ );
1234
+ }
770
1235
  const contents = tile.contents ?? (tile.content ? [tile.content] : []);
771
1236
  for (let contentIndex = 0; contentIndex < contents.length; contentIndex++) {
772
1237
  const content = contents[contentIndex];
@@ -815,15 +1280,15 @@ async function readResponseBytes2(response, maximumBytes, signal) {
815
1280
  "Browser inputs must provide a readable response stream"
816
1281
  );
817
1282
  }
818
- const data = new Uint8Array(maximumBytes);
819
1283
  let byteLength = 0;
820
1284
  try {
1285
+ const data = new Uint8Array(maximumBytes);
821
1286
  while (true) {
822
1287
  throwIfAborted4(signal);
823
1288
  const { done, value } = await reader.read();
1289
+ throwIfAborted4(signal);
824
1290
  if (done) break;
825
1291
  if (value.byteLength > maximumBytes - byteLength) {
826
- await reader.cancel();
827
1292
  throw new TileConversionError(
828
1293
  "INPUT_RESOURCE_TOO_LARGE",
829
1294
  `Browser tileset input exceeds the configured ${maximumBytes} byte remaining limit`
@@ -832,10 +1297,14 @@ async function readResponseBytes2(response, maximumBytes, signal) {
832
1297
  data.set(value, byteLength);
833
1298
  byteLength += value.byteLength;
834
1299
  }
1300
+ return data.slice(0, byteLength);
1301
+ } catch (error) {
1302
+ await reader.cancel().catch(() => {
1303
+ });
1304
+ throw error;
835
1305
  } finally {
836
1306
  reader.releaseLock();
837
1307
  }
838
- return data.subarray(0, byteLength);
839
1308
  }
840
1309
  function isJsonContent(data, contentType) {
841
1310
  if (contentType?.includes("json")) return true;
@@ -894,6 +1363,7 @@ export {
894
1363
  createTiles3DConversionSpatialContext,
895
1364
  createTilesetConversionSource,
896
1365
  inspectTileset,
1366
+ selectBrowserTilesetContent,
897
1367
  traversePointCloudSource,
898
1368
  validateTileset
899
1369
  };