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

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 (83) hide show
  1. package/README.md +178 -2
  2. package/bin/converter-v5.js +2 -0
  3. package/dist/converter.min.cjs +162 -163
  4. package/dist/i3s-server/bin/i3s-server.min.cjs +73 -73
  5. package/dist/index.cjs +1749 -2152
  6. package/dist/index.js +1748 -2162
  7. package/dist/v5/adapters.cjs +1550 -128
  8. package/dist/v5/adapters.d.ts +16 -0
  9. package/dist/v5/adapters.js +1558 -117
  10. package/dist/v5/archive-source.d.ts +36 -0
  11. package/dist/v5/browser-tileset-source.d.ts +55 -11
  12. package/dist/v5/browser.cjs +18 -0
  13. package/dist/v5/browser.d.ts +14 -0
  14. package/dist/v5/browser.js +26 -0
  15. package/dist/v5/conversion-api.d.ts +9 -0
  16. package/dist/v5/converter-cli.d.ts +1 -0
  17. package/dist/v5/converter-cli.js +1214 -0
  18. package/dist/v5/core.cjs +471 -60
  19. package/dist/v5/core.d.ts +2 -0
  20. package/dist/v5/core.js +471 -60
  21. package/dist/v5/i3s-mesh-conversion.d.ts +4 -0
  22. package/dist/v5/i3s-selection-source.d.ts +70 -0
  23. package/dist/v5/index.cjs +10 -0
  24. package/dist/v5/index.d.ts +8 -0
  25. package/dist/v5/index.js +14 -0
  26. package/dist/v5/mesh-conversion.d.ts +18 -0
  27. package/dist/v5/mesh-hierarchy.d.ts +47 -0
  28. package/dist/v5/mesh-source-features.d.ts +2 -1
  29. package/dist/v5/mesh-source-schema.d.ts +12 -0
  30. package/dist/v5/mesh-source.d.ts +5 -2
  31. package/dist/v5/mesh-tileset-sink.d.ts +5 -0
  32. package/dist/v5/node-cli.d.ts +9 -0
  33. package/dist/v5/node-conversion.d.ts +32 -0
  34. package/dist/v5/node-feature-schema.d.ts +3 -0
  35. package/dist/v5/node-i3s-source.d.ts +41 -0
  36. package/dist/v5/node-source.d.ts +47 -0
  37. package/dist/v5/node.cjs +900 -0
  38. package/dist/v5/node.d.ts +6 -0
  39. package/dist/v5/node.js +877 -0
  40. package/dist/v5/point-cloud-file.d.ts +83 -0
  41. package/dist/v5/point-cloud-potree.d.ts +47 -0
  42. package/dist/v5/point-cloud-tileset.d.ts +44 -0
  43. package/dist/v5/resource-manifest.d.ts +10 -1
  44. package/dist/v5/selected-mesh-source.d.ts +53 -0
  45. package/package.json +33 -25
  46. package/src/index.ts +4 -1
  47. package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +2 -4
  48. package/src/v4/converter-cli.ts +8 -2
  49. package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +3 -51
  50. package/src/v4/i3s-converter/helpers/node-index-document.ts +5 -2
  51. package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +1 -1
  52. package/src/v4/i3s-converter/json-templates/store.ts +2 -1
  53. package/src/v5/adapters.ts +48 -0
  54. package/src/v5/archive-source.ts +235 -0
  55. package/src/v5/browser-tileset-source.ts +418 -48
  56. package/src/v5/browser.ts +45 -0
  57. package/src/v5/conversion-api.ts +69 -9
  58. package/src/v5/converter-cli.ts +28 -0
  59. package/src/v5/core.ts +6 -0
  60. package/src/v5/i3s-mesh-conversion.ts +40 -2
  61. package/src/v5/i3s-mesh-sink.ts +1 -1
  62. package/src/v5/i3s-mesh-source.ts +1 -0
  63. package/src/v5/i3s-selection-source.ts +298 -0
  64. package/src/v5/index.ts +26 -0
  65. package/src/v5/mesh-conversion.ts +20 -0
  66. package/src/v5/mesh-hierarchy.ts +648 -0
  67. package/src/v5/mesh-source-features.ts +39 -7
  68. package/src/v5/mesh-source-schema.ts +134 -0
  69. package/src/v5/mesh-source.ts +16 -4
  70. package/src/v5/mesh-tileset-sink.ts +1 -1
  71. package/src/v5/node-cli.ts +197 -0
  72. package/src/v5/node-conversion.ts +345 -0
  73. package/src/v5/node-feature-schema.ts +155 -0
  74. package/src/v5/node-i3s-source.ts +181 -0
  75. package/src/v5/node-source.ts +328 -0
  76. package/src/v5/node.ts +16 -0
  77. package/src/v5/point-cloud-file.ts +306 -0
  78. package/src/v5/point-cloud-potree.ts +130 -0
  79. package/src/v5/point-cloud-tileset.ts +137 -0
  80. package/src/v5/resource-fetcher.ts +4 -1
  81. package/src/v5/resource-manifest.ts +40 -5
  82. package/src/v5/selected-mesh-source.ts +384 -0
  83. package/src/v5/spatial-conversion.ts +88 -17
@@ -111,6 +111,14 @@ export interface ConvertTilesetOptions<TInspection, TInput, TOutput> {
111
111
  readonly maxInputResourceBytes?: number;
112
112
  /** Reject any output resource larger than this configured byte limit. */
113
113
  readonly maxOutputResourceBytes?: number;
114
+ /** Maximum measured input bytes across this run; does not bound source allocations. */
115
+ readonly maxInputBytes?: number;
116
+ /** Maximum measured output bytes across this run, including checkpointed resources. */
117
+ readonly maxOutputBytes?: number;
118
+ /** Maximum input resources consumed by this run, including zero-byte resources. */
119
+ readonly maxInputResources?: number;
120
+ /** Maximum outputs processed by this run, including zero-byte and checkpointed resources. */
121
+ readonly maxOutputResources?: number;
114
122
  /** Cancel inspection, conversion, validation, or writes. */
115
123
  readonly signal?: AbortSignal;
116
124
  /** Receive progress updates after each resource transition. */
@@ -155,6 +163,7 @@ export async function inspectTileset<TInspection, TInput>(
155
163
  * This does not limit source decoding allocations or content retained by a source.
156
164
  * Writes are awaited one resource at a time, keeping queued output bytes bounded by the
157
165
  * largest emitted resource and the configured `maxOutputResourceBytes` limit.
166
+ * Cumulative byte/count limits bound measured work in this run, not total allocation memory.
158
167
  */
159
168
  export async function convertTileset<TInspection, TInput, TOutput>(
160
169
  options: ConvertTilesetOptions<TInspection, TInput, TOutput>
@@ -167,6 +176,10 @@ export async function convertTileset<TInspection, TInput, TOutput>(
167
176
  measureOutputBytes,
168
177
  maxInputResourceBytes = Number.POSITIVE_INFINITY,
169
178
  maxOutputResourceBytes = Number.POSITIVE_INFINITY,
179
+ maxInputBytes = Number.POSITIVE_INFINITY,
180
+ maxOutputBytes = Number.POSITIVE_INFINITY,
181
+ maxInputResources = Number.POSITIVE_INFINITY,
182
+ maxOutputResources = Number.POSITIVE_INFINITY,
170
183
  signal,
171
184
  onProgress
172
185
  } = options;
@@ -181,19 +194,26 @@ export async function convertTileset<TInspection, TInput, TOutput>(
181
194
  };
182
195
 
183
196
  try {
184
- if (
185
- maxInputResourceBytes !== Number.POSITIVE_INFINITY &&
186
- (!Number.isSafeInteger(maxInputResourceBytes) || maxInputResourceBytes < 0)
187
- ) {
188
- throw new TileConversionError(
189
- 'INVALID_INPUT_RESOURCE_LIMIT',
190
- 'maxInputResourceBytes must be a nonnegative safe integer or Infinity'
191
- );
197
+ for (const [name, limit, code] of [
198
+ ['maxInputResourceBytes', maxInputResourceBytes, 'INVALID_INPUT_RESOURCE_LIMIT'],
199
+ ['maxOutputResourceBytes', maxOutputResourceBytes, 'INVALID_OUTPUT_RESOURCE_LIMIT'],
200
+ ['maxInputBytes', maxInputBytes, 'INVALID_INPUT_BYTES_LIMIT'],
201
+ ['maxOutputBytes', maxOutputBytes, 'INVALID_OUTPUT_BYTES_LIMIT'],
202
+ ['maxInputResources', maxInputResources, 'INVALID_INPUT_COUNT_LIMIT'],
203
+ ['maxOutputResources', maxOutputResources, 'INVALID_OUTPUT_COUNT_LIMIT']
204
+ ] as const) {
205
+ if (limit !== Infinity && (!Number.isSafeInteger(limit) || limit < 0)) {
206
+ throw new TileConversionError(
207
+ code,
208
+ name + ' must be a nonnegative safe integer or Infinity'
209
+ );
210
+ }
192
211
  }
193
212
  reportProgress('inspect');
194
213
  const inspection = await inspectTileset(source, signal);
195
214
  for await (const inputResource of source.read(inspection, signal)) {
196
215
  throwIfAborted(signal);
216
+ checkConversionTotal(inputResources, 1, maxInputResources, 'INPUT_COUNT_LIMIT_EXCEEDED');
197
217
  const inputResourceBytes = measureInputBytes(inputResource);
198
218
  if (!Number.isSafeInteger(inputResourceBytes) || inputResourceBytes < 0) {
199
219
  throw new TileConversionError(
@@ -207,23 +227,41 @@ export async function convertTileset<TInspection, TInput, TOutput>(
207
227
  `Input resource is ${inputResourceBytes} bytes, exceeding the configured limit of ${maxInputResourceBytes} bytes`
208
228
  );
209
229
  }
230
+ checkConversionTotal(
231
+ inputBytes,
232
+ inputResourceBytes,
233
+ maxInputBytes,
234
+ 'INPUT_BYTES_LIMIT_EXCEEDED'
235
+ );
210
236
  inputResources++;
211
237
  inputBytes += inputResourceBytes;
212
238
  reportProgress('convert');
213
239
  const convertedResources = await codec.convert(inputResource, inspection, signal);
214
240
  for await (const outputResource of convertedResources) {
215
241
  throwIfAborted(signal);
242
+ checkConversionTotal(outputResources, 1, maxOutputResources, 'OUTPUT_COUNT_LIMIT_EXCEEDED');
216
243
  reportProgress('validate');
217
244
  const resourceDiagnostics = (await codec.validateOutput?.(outputResource, signal)) || [];
218
245
  diagnostics.push(...resourceDiagnostics);
219
246
  const outputResourceBytes = measureOutputBytes(outputResource);
220
- largestOutputResourceBytes = Math.max(largestOutputResourceBytes, outputResourceBytes);
247
+ if (!Number.isSafeInteger(outputResourceBytes) || outputResourceBytes < 0) {
248
+ throw new TileConversionError(
249
+ 'INVALID_OUTPUT_RESOURCE_SIZE',
250
+ 'measureOutputBytes must return a nonnegative safe integer'
251
+ );
252
+ }
221
253
  if (outputResourceBytes > maxOutputResourceBytes) {
222
254
  throw new TileConversionError(
223
255
  'OUTPUT_RESOURCE_TOO_LARGE',
224
256
  `Output resource is ${outputResourceBytes} bytes, exceeding the configured limit of ${maxOutputResourceBytes} bytes`
225
257
  );
226
258
  }
259
+ checkConversionTotal(
260
+ outputBytes,
261
+ outputResourceBytes,
262
+ maxOutputBytes,
263
+ 'OUTPUT_BYTES_LIMIT_EXCEEDED'
264
+ );
227
265
  if (resourceDiagnostics.some(diagnostic => diagnostic.severity === 'error')) {
228
266
  throw new TileConversionError(
229
267
  'OUTPUT_VALIDATION_FAILED',
@@ -234,6 +272,7 @@ export async function convertTileset<TInspection, TInput, TOutput>(
234
272
  throwIfAborted(signal);
235
273
  await sink.write(outputResource, signal);
236
274
  throwIfAborted(signal);
275
+ largestOutputResourceBytes = Math.max(largestOutputResourceBytes, outputResourceBytes);
237
276
  outputResources++;
238
277
  outputBytes += outputResourceBytes;
239
278
  reportProgress('convert');
@@ -285,3 +324,24 @@ function throwIfAborted(signal?: AbortSignal): void {
285
324
  throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');
286
325
  }
287
326
  }
327
+
328
+ /** Checks a cumulative budget without overflowing safe integer accounting. */
329
+ function checkConversionTotal(
330
+ current: number,
331
+ increment: number,
332
+ limit: number,
333
+ code: string
334
+ ): void {
335
+ if (increment > limit - current) {
336
+ throw new TileConversionError(
337
+ code,
338
+ `Conversion exceeds cumulative limit ${limit}: ${current} processed, next resource adds ${increment}`
339
+ );
340
+ }
341
+ if (increment > Number.MAX_SAFE_INTEGER - current) {
342
+ throw new TileConversionError(
343
+ 'RESOURCE_TOTAL_OVERFLOW',
344
+ 'Conversion totals must remain safe integers'
345
+ );
346
+ }
347
+ }
@@ -0,0 +1,28 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {runTileConverterCLI} from './node-cli.js';
6
+
7
+ /** Runs the Node process adapter and maps SIGINT into cooperative I/O cancellation. */
8
+ async function main(): Promise<void> {
9
+ const controller = new AbortController();
10
+ /** Cancels the active operation; promise cleanup closes source and output handles. */
11
+ const cancelOperation = (): void =>
12
+ controller.abort(new Error('Tile conversion operation interrupted'));
13
+ process.on('SIGINT', cancelOperation);
14
+ try {
15
+ process.exitCode = await runTileConverterCLI(
16
+ process.argv.slice(2),
17
+ {
18
+ stdout: text => process.stdout.write(text),
19
+ stderr: text => process.stderr.write(text)
20
+ },
21
+ controller.signal
22
+ );
23
+ } finally {
24
+ process.removeListener('SIGINT', cancelOperation);
25
+ }
26
+ }
27
+
28
+ void main();
package/src/v5/core.ts CHANGED
@@ -59,3 +59,9 @@ export type {
59
59
  BrowserTilesetResourceDescriptor,
60
60
  BrowserTilesetConversionInputResource
61
61
  } from './browser-tileset-source.js';
62
+
63
+ export {selectBrowserTilesetContent} from './browser-tileset-source.js';
64
+ export type {
65
+ BrowserTilesetDocumentPlacement,
66
+ BrowserTilesetContentSelection
67
+ } from './browser-tileset-source.js';
@@ -2,7 +2,11 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import {encodeI3SMeshLayer, encodeI3SMeshLayerWithDraco} from '@loaders.gl/i3s';
5
+ import {
6
+ encodeI3SMeshLayer,
7
+ encodeI3SMeshLayerWithDraco,
8
+ preloadI3SMeshCompression
9
+ } from '@loaders.gl/i3s';
6
10
  import type {
7
11
  EncodedI3SMeshLayer,
8
12
  I3SMeshWriterOptions,
@@ -23,6 +27,8 @@ import type {BrowserTileConversionFile} from '@loaders.gl/tile-converter/v5/core
23
27
  export interface I3SMeshConversionResource extends EncodedI3SMeshLayer {
24
28
  /** Source placement identifier used for diagnostics. */
25
29
  readonly id: string;
30
+ /** Original I3S layer type when authoring from an I3S source. */
31
+ readonly sourceLayerType?: '3DObject' | 'IntegratedMesh';
26
32
  }
27
33
 
28
34
  /** Spatial preparation and required authoring limits for the I3S codec. */
@@ -34,6 +40,8 @@ export interface I3SMeshConversionCodecOptions
34
40
  readonly normalReferenceFrame?: 'earth-centered' | 'vertex-reference-frame';
35
41
  /** Lossless Edge Breaker geometry by default; false retains raw I3S geometry. */
36
42
  readonly draco?: boolean;
43
+ /** Explicitly authorize IntegratedMesh input to become 3DObject output; omission rejects it. */
44
+ readonly integratedMeshPolicy?: 'as-3d-object';
37
45
  /** Full Draco encoder/decoder runtime URLs or injected modules. */
38
46
  readonly dracoLibraryOptions?: I3SDracoLibraryOptions;
39
47
  }
@@ -42,6 +50,15 @@ export interface I3SMeshConversionCodecOptions
42
50
  export function createI3SMeshConversionCodec<TInspection = unknown>(
43
51
  options: I3SMeshConversionCodecOptions
44
52
  ): TileConversionCodec<TInspection, MeshSourceResource, I3SMeshConversionResource> {
53
+ if (
54
+ options.integratedMeshPolicy !== undefined &&
55
+ options.integratedMeshPolicy !== 'as-3d-object'
56
+ ) {
57
+ throw new TileConversionError(
58
+ 'I3S_LAYER_TYPE_POLICY_INVALID',
59
+ 'Unknown IntegratedMesh conversion policy'
60
+ );
61
+ }
45
62
  const reference = options.spatialContext.spatialReference;
46
63
  const height =
47
64
  reference.targetHeightReference === 'native'
@@ -65,6 +82,15 @@ export function createI3SMeshConversionCodec<TInspection = unknown>(
65
82
  /** Places a source mesh in ECEF and authors one layer without modifying source arrays. */
66
83
  async *convert(resource, _inspection, signal) {
67
84
  signal?.throwIfAborted();
85
+ if (
86
+ resource.sourceLayerType === 'IntegratedMesh' &&
87
+ options.integratedMeshPolicy !== 'as-3d-object'
88
+ ) {
89
+ throw new TileConversionError(
90
+ 'I3S_LAYER_TYPE_CHANGE_REQUIRED',
91
+ 'IntegratedMesh to 3DObject conversion requires integratedMeshPolicy=as-3d-object'
92
+ );
93
+ }
68
94
  const mesh = validateMeshGeometry(resource.mesh, true);
69
95
  const positions = mesh.attributes.POSITION.value;
70
96
  const context = options.spatialContext;
@@ -86,6 +112,7 @@ export function createI3SMeshConversionCodec<TInspection = unknown>(
86
112
  if (mesh.attributes.NORMAL) mesh.attributes.NORMAL = {value: transformed.normals!, size: 3};
87
113
  let layer: EncodedI3SMeshLayer;
88
114
  try {
115
+ await preloadI3SMeshCompression();
89
116
  const writerOptions = {
90
117
  ...options,
91
118
  material: resource.material,
@@ -105,11 +132,22 @@ export function createI3SMeshConversionCodec<TInspection = unknown>(
105
132
  signal?.throwIfAborted();
106
133
  objectIdOffset +=
107
134
  resource.features?.batches.reduce((total, batch) => total + batch.data.numRows, 0) ?? 1;
108
- yield {id: resource.id, ...layer};
135
+ yield {id: resource.id, ...layer, sourceLayerType: resource.sourceLayerType};
109
136
  },
110
137
  /** Reports measured precision and explicitly authorized integer representation changes. */
111
138
  async validateOutput(resource) {
112
139
  return [
140
+ ...(resource.sourceLayerType === 'IntegratedMesh'
141
+ ? [
142
+ {
143
+ code: 'I3S_LAYER_TYPE_CHANGED',
144
+ message:
145
+ 'Explicitly authored IntegratedMesh geometry as I3S 1.7 3DObject; integrated surface layer semantics and source LOD are not preserved',
146
+ severity: 'warning' as const,
147
+ resourceId: resource.id
148
+ }
149
+ ]
150
+ : []),
113
151
  ...(resource.maximumPositionError > 0
114
152
  ? [
115
153
  {
@@ -113,7 +113,7 @@ export function createI3SMeshSink(options: I3SMeshSinkOptions): I3SMeshSink {
113
113
  'I3S collections require 1–64 meshes and a positive resource byte limit'
114
114
  );
115
115
  const memory = createBoundedMemoryTileConversionSink(options);
116
- const compressor = new GZipCompressor({useNative: false, gzip: {mtime: 0}});
116
+ const compressor = new GZipCompressor({useNative: false});
117
117
  const decompressor = new GZipDecompressor({useNative: false});
118
118
  const leaves: {node: MeshNode; legacy: Record<string, unknown>}[] = [];
119
119
  const identifiers = new Set<string>();
@@ -212,6 +212,7 @@ export function createI3SMeshTilesetConversionSource(
212
212
  yield {
213
213
  id: `${item.tile.id}/${entry.index}`,
214
214
  origin: [0, 0, 0],
215
+ sourceLayerType: metadata.tileset.layerType,
215
216
  mesh,
216
217
  normalReferenceFrame:
217
218
  content.normalReferenceFrame === 'vertex-reference-frame'
@@ -0,0 +1,298 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {coreApi, parse} from '@loaders.gl/core';
6
+ import {I3SLoader} from '@loaders.gl/i3s';
7
+ import type {I3STileHeader, SceneLayer3D} from '@loaders.gl/i3s';
8
+ import {I3SSource, Tileset3D} from '@loaders.gl/tiles';
9
+ import type {TilesetSourceResolver} from '@loaders.gl/tiles';
10
+ import {
11
+ createI3SConversionSpatialContext,
12
+ TileConversionError
13
+ } from '@loaders.gl/tile-converter/v5/core';
14
+ import {createI3SMeshTilesetConversionSource} from './i3s-mesh-source.js';
15
+ import type {I3SMeshSourceFeatureOptions} from './i3s-mesh-source.js';
16
+ import {SELECTED_MESH_LIMITS, measureMeshBytes} from './selected-mesh-source.js';
17
+ import type {SelectedMeshSourceOptions} from './selected-mesh-source.js';
18
+
19
+ /** Inspected leaf mesh identity and immutable normalized header. */
20
+ export interface I3SMeshConversionResourceDescriptor {
21
+ /** Unique declared node identity. */
22
+ readonly resourceId: string;
23
+ /** Source-managed geometry URL. */
24
+ readonly uri: string;
25
+ /** Normalized header without runtime objects. */
26
+ readonly header: I3STileHeader;
27
+ }
28
+
29
+ /** Metadata-only I3S selection, independent of browser or Node transport ownership. */
30
+ export interface I3SMeshConversionInspection {
31
+ /** Discriminant for I3S mesh input. */
32
+ readonly kind: 'i3s';
33
+ /** Absolute layer or archive virtual root URL. */
34
+ readonly rootUrl: string;
35
+ /** Original layer declarations. */
36
+ readonly layer: SceneLayer3D;
37
+ /** Ordered leaf mesh declarations. */
38
+ readonly resources: readonly I3SMeshConversionResourceDescriptor[];
39
+ /** Previously consumed transport bytes, supplied by the owner. */
40
+ readonly metadataBytes: number;
41
+ }
42
+
43
+ /** Platform-injected transport and decoder policy for bounded I3S selection. */
44
+ export interface I3SMeshSelectionOptions {
45
+ /** Root layer or confined archive root URL. */
46
+ readonly rootUrl: string;
47
+ /** Lifetime-budgeted transport shared between metadata and payload reads. */
48
+ readonly fetcher: typeof fetch;
49
+ /** Operation cancellation. */
50
+ readonly signal: AbortSignal;
51
+ /** Bundled or application-owned Draco assets. */
52
+ readonly dracoLibraryOptions: SelectedMeshSourceOptions['dracoLibraryOptions'];
53
+ /** Maximum decoded mesh bytes; callers may lower the 16 MiB profile. */
54
+ readonly maxInputBytes?: number;
55
+ /** Maximum inspected nodes; callers may lower the 1,000 node profile. */
56
+ readonly maxInputResources?: number;
57
+ }
58
+
59
+ /** Creates a dedicated source-coordinate runtime with transport and all decoder assets injected. */
60
+ function createRuntime(
61
+ transport: I3SMeshSelectionOptions,
62
+ layer?: SceneLayer3D,
63
+ header?: I3STileHeader
64
+ ): Tileset3D {
65
+ const libraries = transport.dracoLibraryOptions;
66
+ const sourceUrl = new URL(transport.rootUrl);
67
+ sourceUrl.search = '';
68
+ sourceUrl.hash = '';
69
+ const options = {
70
+ modules: libraries.modules,
71
+ core: {
72
+ worker: false,
73
+ fetch: transport.fetcher,
74
+ CDN: libraries.CDN,
75
+ useLocalLibraries: libraries.useLocalLibraries
76
+ },
77
+ draco: {decoderProfile: libraries.decoderProfile},
78
+ // The injected transport owns credential scope; runtime query parameters apply to every URL.
79
+ i3s: {geometryMode: 'source' as const, decodeTextures: false, useCompressedTextures: false}
80
+ };
81
+ /** Parses a source-managed resource with the same transport available to nested dependencies. */
82
+ const loadResource: TilesetSourceResolver['loadResource'] = async (url, loader, loadOptions) => {
83
+ const response = await transport.fetcher(url);
84
+ const contextUrl = new URL(url);
85
+ contextUrl.search = '';
86
+ contextUrl.hash = '';
87
+ return parse(await response.arrayBuffer(), loader, loadOptions, {
88
+ url: contextUrl.href,
89
+ fetch: transport.fetcher,
90
+ coreApi,
91
+ _parse: parse
92
+ });
93
+ };
94
+ const source = new I3SSource(
95
+ header && layer
96
+ ? {
97
+ ...layer,
98
+ root: {...header, children: []},
99
+ type: 'I3S',
100
+ url: sourceUrl.href,
101
+ basePath: sourceUrl.href,
102
+ loader: I3SLoader,
103
+ coreApi
104
+ }
105
+ : {
106
+ url: sourceUrl.href,
107
+ loader: I3SLoader,
108
+ coreApi,
109
+ resolver: {loadRoot: loadResource, loadResource}
110
+ },
111
+ options
112
+ );
113
+ // Preloaded selection still uses this resolver for content; never let core perform unbounded I/O.
114
+ source.coreApi = {...coreApi, load: loadResource as typeof coreApi.load};
115
+ return new Tileset3D(source, {i3s: options.i3s});
116
+ }
117
+
118
+ /** Inspects supported layer/leaf declarations without requesting geometry, textures or attributes. */
119
+ export async function inspectI3SMeshConversionSource(
120
+ options: I3SMeshSelectionOptions
121
+ ): Promise<I3SMeshConversionInspection> {
122
+ const {signal} = options;
123
+ validateSelectionOptions(options);
124
+ signal.throwIfAborted();
125
+ const transport = options;
126
+ let runtime: Tileset3D | undefined;
127
+ try {
128
+ const response = await transport.fetcher(transport.rootUrl);
129
+ const layer = (await response.json()) as SceneLayer3D;
130
+ if (!['3DObject', 'IntegratedMesh'].includes(layer.layerType))
131
+ throw new Error('Conversion accepts I3S 3DObject or IntegratedMesh layers.');
132
+ // Reuse the inspected root; all further metadata requests are still charged.
133
+ const rootFetcher = transport.fetcher;
134
+ const cachedFetcher: typeof fetch = (url, options) => {
135
+ const requestUrl = new URL(url instanceof Request ? url.url : String(url));
136
+ const root = new URL(transport.rootUrl);
137
+ return requestUrl.origin === root.origin && requestUrl.pathname === root.pathname
138
+ ? Promise.resolve(new Response(JSON.stringify(layer)))
139
+ : rootFetcher(url, options);
140
+ };
141
+ runtime = createRuntime({...transport, fetcher: cachedFetcher});
142
+ await runtime.tilesetInitializationPromise;
143
+ await createI3SMeshTilesetConversionSource(runtime).inspect(signal);
144
+ const stack = [runtime.root!];
145
+ const identifiers = new Set<string>();
146
+ const resources: I3SMeshConversionResourceDescriptor[] = [];
147
+ while (stack.length) {
148
+ signal.throwIfAborted();
149
+ const tile = stack.pop()!;
150
+ if (
151
+ !['string', 'number'].includes(typeof tile.header.id) ||
152
+ String(tile.header.id).length === 0
153
+ )
154
+ throw new Error('I3S nodes require a declared identity.');
155
+ const identifier = String(tile.header.id);
156
+ if (identifiers.has(identifier))
157
+ throw new Error('Repeated I3S node identity is unsupported.');
158
+ identifiers.add(identifier);
159
+ if (
160
+ identifiers.size + stack.length + (tile.header.children?.length ?? 0) >
161
+ (options.maxInputResources ?? SELECTED_MESH_LIMITS.maxInputResources)
162
+ )
163
+ throw new Error('I3S inspection exceeds the 1,000 node limit.');
164
+ await runtime.source.loadTileChildrenForTraversal!(tile, signal);
165
+ if (!tile.children.length && tile.contentUrl)
166
+ resources.push({
167
+ resourceId: identifier,
168
+ uri: tile.contentUrl,
169
+ header: structuredClone(tile.header)
170
+ });
171
+ for (const child of [...tile.children].reverse()) stack.push(child);
172
+ }
173
+ signal.throwIfAborted();
174
+ if (!resources.length) throw new Error('No supported leaf mesh nodes are declared.');
175
+ return {
176
+ kind: 'i3s',
177
+ rootUrl: transport.rootUrl,
178
+ layer,
179
+ resources,
180
+ metadataBytes: 0
181
+ };
182
+ } finally {
183
+ runtime?.destroy();
184
+ }
185
+ }
186
+
187
+ /** Converts explicitly selected I3S leaf meshes into a bounded flat collection in either format. */
188
+ export async function createSelectedI3SMeshConversionSource(
189
+ inspection: I3SMeshConversionInspection,
190
+ options: I3SMeshSelectionOptions & {
191
+ /** Explicit distinct inspected leaf-node identities. */
192
+ readonly resourceIds: readonly string[];
193
+ /** Explicit attribute mapping for feature-bearing meshes. */
194
+ readonly features?: I3SMeshSourceFeatureOptions;
195
+ }
196
+ ) {
197
+ const {resourceIds, signal, features} = options;
198
+ validateSelectionOptions(options);
199
+ signal.throwIfAborted();
200
+ if (inspection.rootUrl !== options.rootUrl)
201
+ throw new TileConversionError(
202
+ 'INVALID_I3S_SELECTION',
203
+ 'Selection transport must match the inspected layer'
204
+ );
205
+ if (
206
+ !resourceIds.length ||
207
+ resourceIds.length > SELECTED_MESH_LIMITS.maxSelectedResources ||
208
+ new Set(resourceIds).size !== resourceIds.length ||
209
+ resourceIds.some(
210
+ identifier => !inspection.resources.some(resource => resource.resourceId === identifier)
211
+ )
212
+ )
213
+ throw new Error('Select between 1 and 64 distinct leaf mesh nodes.');
214
+ const selected = inspection.resources.filter(resource =>
215
+ resourceIds.includes(resource.resourceId)
216
+ );
217
+ if (
218
+ selected.some(
219
+ resource =>
220
+ resource.header.children?.length ||
221
+ !resource.header.contentUrl ||
222
+ resource.uri !== resource.header.contentUrl
223
+ )
224
+ )
225
+ throw new Error('Only inspected leaf mesh headers can be converted.');
226
+ const transport = options;
227
+ let decodedBytes = 0;
228
+ let meshCount = 0;
229
+ {
230
+ const qualification = createRuntime(transport, inspection.layer, selected[0].header);
231
+ let spatialContext: ReturnType<typeof createI3SConversionSpatialContext>;
232
+ try {
233
+ await qualification.tilesetInitializationPromise;
234
+ const metadata = await createI3SMeshTilesetConversionSource(qualification).inspect(signal);
235
+ spatialContext = createI3SConversionSpatialContext(metadata.spatialReference!, {
236
+ targetCrs: 'EPSG:4978'
237
+ });
238
+ } finally {
239
+ qualification.destroy();
240
+ }
241
+ const source = {
242
+ /** Reuses only immutable inspected declarations. */
243
+ inspect: async () => inspection,
244
+ /** Releases each dedicated node runtime before moving on or closing early. */
245
+ async *read() {
246
+ for (const descriptor of selected) {
247
+ const runtime = createRuntime(transport, inspection.layer, descriptor.header);
248
+ try {
249
+ await runtime.tilesetInitializationPromise;
250
+ const meshSource = createI3SMeshTilesetConversionSource(runtime, {
251
+ unloadContent: true,
252
+ features,
253
+ readExternalResource: async uri =>
254
+ new Uint8Array(
255
+ await (await transport.fetcher(runtime.source.getTileUrl(uri))).arrayBuffer()
256
+ )
257
+ });
258
+ const metadata = await meshSource.inspect(signal);
259
+ for await (const resource of meshSource.read(metadata, signal)) {
260
+ decodedBytes += measureMeshBytes(resource);
261
+ if (
262
+ ++meshCount > SELECTED_MESH_LIMITS.maxMeshResources ||
263
+ decodedBytes > (options.maxInputBytes ?? SELECTED_MESH_LIMITS.maxInputBytes)
264
+ )
265
+ throw new TileConversionError(
266
+ 'INPUT_RESOURCE_TOO_LARGE',
267
+ 'Selected I3S decoded resources exceed the input/mesh budget'
268
+ );
269
+ yield {...resource, id: descriptor.resourceId};
270
+ }
271
+ } finally {
272
+ runtime.destroy();
273
+ }
274
+ }
275
+ }
276
+ };
277
+ return {source, spatialContext};
278
+ }
279
+ }
280
+
281
+ /** Keeps public selected-source limits within the same bounded profile as platform adapters. */
282
+ function validateSelectionOptions(options: I3SMeshSelectionOptions): void {
283
+ for (const [value, maximum] of [
284
+ [
285
+ options.maxInputBytes ?? SELECTED_MESH_LIMITS.maxInputBytes,
286
+ SELECTED_MESH_LIMITS.maxInputBytes
287
+ ],
288
+ [
289
+ options.maxInputResources ?? SELECTED_MESH_LIMITS.maxInputResources,
290
+ SELECTED_MESH_LIMITS.maxInputResources
291
+ ]
292
+ ])
293
+ if (!Number.isSafeInteger(value) || value < 1 || value > maximum)
294
+ throw new TileConversionError(
295
+ 'INVALID_I3S_SELECTION_LIMIT',
296
+ 'I3S selection limits must be positive safe integers within the bounded profile'
297
+ );
298
+ }
package/src/v5/index.ts CHANGED
@@ -129,3 +129,29 @@ export type {
129
129
  I3SMeshTilesetSourceOptions,
130
130
  I3SMeshSourceFeatureOptions
131
131
  } from '@loaders.gl/tile-converter/v5/adapters';
132
+
133
+ export {convertPointCloudToTileset} from '@loaders.gl/tile-converter/v5/adapters';
134
+ export type {
135
+ ConvertPointCloudToTilesetOptions,
136
+ ConvertedPointCloudTileset
137
+ } from '@loaders.gl/tile-converter/v5/adapters';
138
+
139
+ export {
140
+ encodePotreeArchive,
141
+ openPointCloudFile,
142
+ extractMeshVertices
143
+ } from '@loaders.gl/tile-converter/v5/adapters';
144
+ export type {
145
+ EncodePotreeArchiveOptions,
146
+ OpenPointCloudFileOptions,
147
+ OpenedPointCloudFile,
148
+ ExtractMeshVerticesOptions
149
+ } from '@loaders.gl/tile-converter/v5/adapters';
150
+
151
+ export {createMeshSourceFeatureOptions} from '@loaders.gl/tile-converter/v5/adapters';
152
+ export type {MeshSourceSchemaOptions} from '@loaders.gl/tile-converter/v5/adapters';
153
+ export {convertPointCloudToPotree} from '@loaders.gl/tile-converter/v5/adapters';
154
+ export type {
155
+ ConvertPointCloudToPotreeOptions,
156
+ ConvertedPointCloudPotree
157
+ } from '@loaders.gl/tile-converter/v5/adapters';
@@ -21,6 +21,15 @@ import {encodeDracoMeshTile} from './mesh-draco.js';
21
21
  export interface MeshConversionInput {
22
22
  /** Caller-owned resource identifier, forwarded to the output sink. */
23
23
  readonly id: string;
24
+ /** Inspected content placement, used by the hierarchy sink independently of primitive IDs. */
25
+ readonly contentResourceId?: string;
26
+ /** Ordered primitive within a content; last marks complete extraction for hierarchy authoring. */
27
+ readonly contentPrimitive?: {
28
+ /** Zero-based primitive placement index in scene traversal order. */
29
+ readonly index: number;
30
+ /** Whether this is the final primitive placement in the content. */
31
+ readonly last: boolean;
32
+ };
24
33
  /** Triangle geometry with absolute packed Float32 or Float64 source positions. */
25
34
  readonly mesh: MeshGeometry;
26
35
  /** Original I3S vector basis; overrides the codec default when supplied by a source. */
@@ -37,6 +46,15 @@ export interface MeshConversionInput {
37
46
  export interface EncodedMeshConversionResource {
38
47
  /** Caller-owned resource identifier. */
39
48
  readonly id: string;
49
+ /** Inspected content placement, retained by the codec for hierarchy authoring. */
50
+ readonly contentResourceId?: string;
51
+ /** Ordered primitive within a content; last marks complete extraction for hierarchy authoring. */
52
+ readonly contentPrimitive?: {
53
+ /** Zero-based primitive placement index in scene traversal order. */
54
+ readonly index: number;
55
+ /** Whether this is the final primitive placement in the content. */
56
+ readonly last: boolean;
57
+ };
40
58
  /** Self-contained GLB with local positions and transformed normals. */
41
59
  readonly glb: ArrayBuffer;
42
60
  /** Target-frame translation to add to the encoded positions. */
@@ -205,6 +223,8 @@ export function createMeshConversionCodec<TInspection = unknown>(
205
223
  }
206
224
  yield {
207
225
  id: resource.id,
226
+ contentResourceId: resource.contentResourceId,
227
+ contentPrimitive: resource.contentPrimitive,
208
228
  glb: encoded.glb,
209
229
  origin,
210
230
  boundingBox: [minimum, maximum],