@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
@@ -0,0 +1,1214 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+
16
+ // src/v5/conversion-api.ts
17
+ var TileConversionError2;
18
+ var init_conversion_api = __esm({
19
+ "src/v5/conversion-api.ts"() {
20
+ "use strict";
21
+ TileConversionError2 = class extends Error {
22
+ /** Stable machine-readable failure code. */
23
+ code;
24
+ /** Diagnostics associated with this failure. */
25
+ diagnostics;
26
+ /** Creates a typed conversion failure. */
27
+ constructor(code, message, diagnostics = []) {
28
+ super(message);
29
+ this.name = "TileConversionError";
30
+ this.code = code;
31
+ this.diagnostics = diagnostics;
32
+ }
33
+ };
34
+ }
35
+ });
36
+
37
+ // src/v5/resource-fetcher.ts
38
+ function createTileConversionResourceFetcher(options) {
39
+ let consumed = options.initialInputBytes ?? 0;
40
+ if (!Number.isSafeInteger(options.maxInputBytes) || options.maxInputBytes < 1 || !Number.isSafeInteger(consumed) || consumed < 0 || consumed > options.maxInputBytes)
41
+ throw new TileConversionError2(
42
+ "INVALID_INPUT_MEMORY_LIMIT",
43
+ "Resource fetch requires a positive aggregate byte budget and valid initial bytes"
44
+ );
45
+ const fetcher = options.fetcher ?? fetch;
46
+ return async (input, requestOptions) => {
47
+ options.signal?.throwIfAborted();
48
+ const url = new URL(input instanceof Request ? input.url : String(input));
49
+ if (!["http:", "https:", "blob:", "data:"].includes(url.protocol))
50
+ throw new TileConversionError2(
51
+ "EXTERNAL_RESOURCE_UNSUPPORTED",
52
+ "Conversion transport requires HTTP(S), Blob or embedded data resources"
53
+ );
54
+ const requestSignal = requestOptions?.signal ?? (input instanceof Request ? input.signal : void 0);
55
+ const signal = options.signal && requestSignal ? AbortSignal.any([options.signal, requestSignal]) : options.signal ?? requestSignal;
56
+ const response = await fetcher(input, { ...requestOptions, signal });
57
+ if (!response.ok) {
58
+ await response.body?.cancel();
59
+ throw new TileConversionError2(
60
+ "BROWSER_SOURCE_FETCH_FAILED",
61
+ `Could not fetch conversion resource (${response.status})`
62
+ );
63
+ }
64
+ const reader = response.body?.getReader();
65
+ if (!reader)
66
+ throw new TileConversionError2(
67
+ "BROWSER_SOURCE_STREAM_UNAVAILABLE",
68
+ "Conversion resources require readable response streams"
69
+ );
70
+ const cancelRead = () => {
71
+ void reader.cancel(signal?.reason).catch(() => {
72
+ });
73
+ };
74
+ signal?.addEventListener("abort", cancelRead, { once: true });
75
+ const parts = [];
76
+ let byteLength = 0;
77
+ try {
78
+ while (true) {
79
+ signal?.throwIfAborted();
80
+ const { done, value } = await reader.read();
81
+ signal?.throwIfAborted();
82
+ if (done) break;
83
+ if (value.byteLength > options.maxInputBytes - consumed)
84
+ throw new TileConversionError2(
85
+ "INPUT_RESOURCE_TOO_LARGE",
86
+ "Selected contents and external dependencies exceed the aggregate input byte budget"
87
+ );
88
+ consumed += value.byteLength;
89
+ byteLength += value.byteLength;
90
+ parts.push(value);
91
+ }
92
+ } catch (error) {
93
+ await reader.cancel(error);
94
+ throw error;
95
+ } finally {
96
+ signal?.removeEventListener("abort", cancelRead);
97
+ reader.releaseLock();
98
+ }
99
+ const bytes = new Uint8Array(byteLength);
100
+ let offset = 0;
101
+ for (const part of parts) {
102
+ bytes.set(part, offset);
103
+ offset += part.byteLength;
104
+ }
105
+ const boundedResponse = new Response(bytes, {
106
+ status: response.status,
107
+ statusText: response.statusText,
108
+ headers: response.headers
109
+ });
110
+ Object.defineProperty(boundedResponse, "url", { value: response.url });
111
+ return boundedResponse;
112
+ };
113
+ }
114
+ var init_resource_fetcher = __esm({
115
+ "src/v5/resource-fetcher.ts"() {
116
+ "use strict";
117
+ init_conversion_api();
118
+ }
119
+ });
120
+
121
+ // src/v5/archive-source.ts
122
+ import { BlobFile, HttpFile } from "@loaders.gl/loader-utils";
123
+ import { parseSLPKArchive } from "@loaders.gl/i3s";
124
+ import { parse3DTilesArchive } from "@loaders.gl/3d-tiles";
125
+ import { TileConversionError as TileConversionError3 } from "@loaders.gl/tile-converter/v5/core";
126
+ async function openTileConversionArchive(descriptor, maxInputBytes, signal, fetcher = fetch) {
127
+ signal.throwIfAborted();
128
+ if (!["slpk", "3tz"].includes(descriptor.format))
129
+ throw new TileConversionError3("INVALID_ARCHIVE_SOURCE", "Select SLPK or 3TZ archive input");
130
+ validateArchiveEntityTag(descriptor.identity?.etag);
131
+ const transport = createTileConversionResourceFetcher({
132
+ maxInputBytes,
133
+ signal,
134
+ fetcher: async (url, options) => {
135
+ const response = await fetcher(url, options);
136
+ if (response.status !== 206) {
137
+ await response.body?.cancel();
138
+ throw new Error("Remote conversion archives require HTTP 206 byte-range responses.");
139
+ }
140
+ try {
141
+ validateArchiveResponseValidator(response.headers);
142
+ } catch (error) {
143
+ await response.body?.cancel(error);
144
+ throw error;
145
+ }
146
+ return response;
147
+ }
148
+ });
149
+ let file;
150
+ let closePromise;
151
+ async function closeFile() {
152
+ if (file) {
153
+ closePromise ??= file.close();
154
+ await closePromise;
155
+ }
156
+ }
157
+ try {
158
+ if (typeof descriptor.input === "string") {
159
+ if (!["http:", "https:"].includes(new URL(descriptor.input).protocol))
160
+ throw new Error("Use an HTTP(S) archive URL.");
161
+ const remote = new HttpFile(descriptor.input, {
162
+ fetch: (url, options) => transport(url, options),
163
+ consistency: "strict",
164
+ ...descriptor.identity
165
+ });
166
+ file = remote;
167
+ await remote.open(signal);
168
+ } else file = new BlobFile(descriptor.input);
169
+ const ownedFile = file;
170
+ file = void 0;
171
+ return await createTileConversionArchiveTransport(
172
+ ownedFile,
173
+ descriptor.format,
174
+ maxInputBytes,
175
+ signal,
176
+ descriptor
177
+ );
178
+ } catch (error) {
179
+ await closeFile();
180
+ throw error;
181
+ }
182
+ }
183
+ async function createTileConversionArchiveTransport(file, format, maxInputBytes, signal, descriptor) {
184
+ const rootUrl = `https://conversion.invalid/input.${format}`;
185
+ let closed = false;
186
+ let releasePromise;
187
+ let closePromise;
188
+ const closeFile = () => {
189
+ closed = true;
190
+ closePromise ??= file.close();
191
+ return closePromise;
192
+ };
193
+ const checkOpen = () => {
194
+ signal.throwIfAborted();
195
+ if (closed)
196
+ throw new TileConversionError3("ARCHIVE_SOURCE_CLOSED", "Conversion archive is closed");
197
+ };
198
+ try {
199
+ checkOpen();
200
+ if (!["slpk", "3tz"].includes(format) || !Number.isSafeInteger(maxInputBytes) || maxInputBytes < 1)
201
+ throw new TileConversionError3(
202
+ "INVALID_ARCHIVE_SOURCE",
203
+ "Archive source requires a supported format and positive safe byte budget"
204
+ );
205
+ if (!Number.isSafeInteger(file.size) || file.size < 0 || file.size > maxInputBytes)
206
+ throw new Error("Conversion archive input must be at most 16 MiB.");
207
+ let readBytes = 0;
208
+ const boundedFile = {
209
+ handle: file.handle,
210
+ size: file.size,
211
+ bigsize: file.bigsize,
212
+ url: file.url,
213
+ ...file.stat ? { stat: () => file.stat() } : {},
214
+ close: closeFile,
215
+ /** Checks indexed reads before allocation and forwards operation cancellation. */
216
+ read: async (start, length) => {
217
+ checkOpen();
218
+ if (!Number.isSafeInteger(length) || length < 0 || length > maxInputBytes - readBytes)
219
+ throw new TileConversionError3(
220
+ "INPUT_RESOURCE_TOO_LARGE",
221
+ "Archive indexed reads exceed the input budget"
222
+ );
223
+ readBytes += length;
224
+ const bytes = await file.read(start, length, signal);
225
+ checkOpen();
226
+ return bytes;
227
+ }
228
+ };
229
+ const archive = format === "slpk" ? await parseSLPKArchive(boundedFile) : await parse3DTilesArchive(boundedFile);
230
+ const root = new URL(rootUrl);
231
+ return {
232
+ rootUrl: format === "3tz" ? `${rootUrl}/tileset.json` : rootUrl,
233
+ descriptor: descriptor ? {
234
+ ...descriptor,
235
+ ...file instanceof HttpFile ? { identity: file.getIdentitySnapshot() } : {}
236
+ } : void 0,
237
+ /** Resolves only resources belonging to this archive, without external network fallback. */
238
+ fetcher: (async (input, options) => {
239
+ checkOpen();
240
+ options?.signal?.throwIfAborted();
241
+ const url = new URL(input instanceof Request ? input.url : String(input));
242
+ if (url.origin !== root.origin || url.pathname !== root.pathname && !url.pathname.startsWith(`${root.pathname}/`))
243
+ throw new Error("Conversion dependencies must resolve inside the selected archive.");
244
+ const path = decodeURIComponent(
245
+ url.pathname.slice(root.pathname.length).replace(/^\//, "")
246
+ );
247
+ if (path.includes("\\") || path.split("/").some((segment) => segment === ".." || segment === "."))
248
+ throw new Error("Archive dependency path cannot escape its root.");
249
+ const bytes = format === "slpk" ? await archive.getFile(path, "http") : await archive.getFile(path);
250
+ checkOpen();
251
+ options?.signal?.throwIfAborted();
252
+ return new Response(bytes);
253
+ }),
254
+ /** Releases the readable file on success, failure or cancellation. */
255
+ close: async () => {
256
+ closed = true;
257
+ releasePromise ??= archive.close();
258
+ await releasePromise;
259
+ }
260
+ };
261
+ } catch (error) {
262
+ await closeFile();
263
+ throw error;
264
+ }
265
+ }
266
+ function validateArchiveEntityTag(entityTag) {
267
+ if (entityTag !== void 0 && entityTag !== null && !/^"[\x21\x23-\x7e\x80-\xff]*"$/.test(entityTag))
268
+ throw new TileConversionError3(
269
+ "ARCHIVE_VALIDATOR_UNSUPPORTED",
270
+ "Remote archives require a strong quoted ETag, or Last-Modified without an ETag; weak/malformed ETags are unsupported"
271
+ );
272
+ }
273
+ function validateArchiveResponseValidator(headers) {
274
+ const entityTag = headers.get("etag");
275
+ validateArchiveEntityTag(entityTag);
276
+ if (entityTag !== null) return;
277
+ const lastModified = headers.get("last-modified");
278
+ const responseDate = headers.get("date");
279
+ const modifiedTime = parseArchiveHttpDate(lastModified);
280
+ const responseTime = parseArchiveHttpDate(responseDate);
281
+ if (!Number.isFinite(modifiedTime) || !Number.isFinite(responseTime) || responseTime - modifiedTime < 6e4)
282
+ throw new TileConversionError3(
283
+ "ARCHIVE_VALIDATOR_UNSUPPORTED",
284
+ "Last-Modified archive identity requires a valid Date at least 60 seconds later; otherwise supply a strong ETag"
285
+ );
286
+ }
287
+ function parseArchiveHttpDate(value) {
288
+ const milliseconds = value === null ? NaN : Date.parse(value);
289
+ return Number.isFinite(milliseconds) && new Date(milliseconds).toUTCString() === value ? milliseconds : NaN;
290
+ }
291
+ var init_archive_source = __esm({
292
+ "src/v5/archive-source.ts"() {
293
+ "use strict";
294
+ init_resource_fetcher();
295
+ }
296
+ });
297
+
298
+ // src/v5/node-source.ts
299
+ import { open as open2, realpath } from "node:fs/promises";
300
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
301
+ import { fileURLToPath } from "node:url";
302
+ import { parse3DTilesArchive as parse3DTilesArchive2 } from "@loaders.gl/3d-tiles";
303
+ import {
304
+ createBrowserTilesetConversionSource,
305
+ TileConversionError as TileConversionError4
306
+ } from "@loaders.gl/tile-converter/v5/core";
307
+ async function createNodeTilesetConversionSource(options) {
308
+ const maxInputBytes = options.maxInputBytes ?? NODE_TILESET_LIMITS.maxInputBytes;
309
+ const maxInputResources = options.maxInputResources ?? NODE_TILESET_LIMITS.maxInputResources;
310
+ const maxTilesetDocuments = options.maxTilesetDocuments ?? 1;
311
+ if (!Number.isSafeInteger(maxInputBytes) || maxInputBytes < 1 || maxInputBytes > NODE_TILESET_LIMITS.maxInputBytes || !Number.isSafeInteger(maxInputResources) || maxInputResources < 1 || maxInputResources > NODE_TILESET_LIMITS.maxInputResources || !Number.isSafeInteger(maxTilesetDocuments) || maxTilesetDocuments < 1 || maxTilesetDocuments > NODE_TILESET_LIMITS.maxTilesetDocuments || !["3d-tiles", "3tz"].includes(options.format ?? "3d-tiles"))
312
+ throw new TileConversionError4(
313
+ "INVALID_NODE_SOURCE_OPTIONS",
314
+ "Node sources require positive byte/resource budgets within the initial profile limits"
315
+ );
316
+ options.signal?.throwIfAborted();
317
+ const controller = new AbortController();
318
+ const lifetimeSignal = options.signal ? AbortSignal.any([controller.signal, options.signal]) : controller.signal;
319
+ let archiveFile;
320
+ let closeArchive;
321
+ let consumedReadBytes = 0;
322
+ const reserveRead = (length) => {
323
+ if (length > maxInputBytes - consumedReadBytes)
324
+ throw new TileConversionError4(
325
+ "INPUT_RESOURCE_TOO_LARGE",
326
+ "Local reads exceed the aggregate input budget"
327
+ );
328
+ consumedReadBytes += length;
329
+ };
330
+ try {
331
+ const inputUrl = /^(?:https?|file):/i.test(options.input) ? new URL(options.input) : void 0;
332
+ if (!inputUrl && /^[a-z][a-z\d+.-]*:/i.test(options.input) && !/^[a-z]:[\\/]/i.test(options.input))
333
+ throw new TileConversionError4(
334
+ "UNSUPPORTED_NODE_SOURCE",
335
+ "Use a local path, file URL or HTTP(S) tileset URL"
336
+ );
337
+ const remote = inputUrl && inputUrl.protocol !== "file:";
338
+ let rootUrl;
339
+ let transport;
340
+ let virtualRoot;
341
+ if (remote && options.format === "3tz") {
342
+ const archive = await openTileConversionArchive(
343
+ { input: inputUrl.href, format: "3tz" },
344
+ maxInputBytes,
345
+ lifetimeSignal,
346
+ options.fetcher
347
+ );
348
+ rootUrl = archive.rootUrl;
349
+ transport = archive.fetcher;
350
+ closeArchive = archive.close;
351
+ } else if (remote) {
352
+ rootUrl = inputUrl.href;
353
+ transport = options.fetcher ?? fetch;
354
+ } else {
355
+ const inputPath = await realpath(inputUrl ? fileURLToPath(inputUrl) : resolve(options.input));
356
+ lifetimeSignal.throwIfAborted();
357
+ const directory = dirname(inputPath);
358
+ virtualRoot = new URL("https://conversion.invalid/input/");
359
+ rootUrl = new URL(
360
+ options.format === "3tz" ? "tileset.json" : encodeURIComponent(basename(inputPath)),
361
+ virtualRoot
362
+ ).href;
363
+ if (options.format === "3tz") {
364
+ archiveFile = await openLocalFile(inputPath, maxInputBytes, lifetimeSignal, reserveRead);
365
+ const archive = await parse3DTilesArchive2(archiveFile);
366
+ transport = async (input, requestOptions) => {
367
+ lifetimeSignal.throwIfAborted();
368
+ requestOptions?.signal?.throwIfAborted();
369
+ const path = getLocalResourcePath(input, virtualRoot);
370
+ const bytes = await archive.getFile(path);
371
+ lifetimeSignal.throwIfAborted();
372
+ requestOptions?.signal?.throwIfAborted();
373
+ return new Response(bytes);
374
+ };
375
+ } else {
376
+ transport = async (input, requestOptions) => {
377
+ const path = getLocalResourcePath(input, virtualRoot);
378
+ const resolvedPath = await realpath(resolve(directory, path));
379
+ const relativePath = relative(directory, resolvedPath);
380
+ if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath))
381
+ throw new TileConversionError4(
382
+ "EXTERNAL_RESOURCE_UNSUPPORTED",
383
+ "Local dependencies must remain inside the tileset directory"
384
+ );
385
+ const signal = requestOptions?.signal ? AbortSignal.any([lifetimeSignal, requestOptions.signal]) : lifetimeSignal;
386
+ const file = await openLocalFile(resolvedPath, maxInputBytes, signal, reserveRead);
387
+ try {
388
+ return new Response(await file.read());
389
+ } finally {
390
+ await file.close();
391
+ }
392
+ };
393
+ }
394
+ }
395
+ const resourceFetcher = createTileConversionResourceFetcher({
396
+ maxInputBytes,
397
+ signal: lifetimeSignal,
398
+ fetcher: transport
399
+ });
400
+ const source = createBrowserTilesetConversionSource({
401
+ input: rootUrl,
402
+ maxInputBytes,
403
+ maxInputResources,
404
+ maxTilesetDocuments,
405
+ inspectNestedTilesets: options.inspectNestedTilesets,
406
+ fetcher: resourceFetcher
407
+ });
408
+ return {
409
+ fetcher: resourceFetcher,
410
+ /** Inspects declarations without opening tile payloads or decoding coordinates. */
411
+ async inspect(signal) {
412
+ const inspection = await source.inspect(
413
+ signal ? AbortSignal.any([lifetimeSignal, signal]) : lifetimeSignal
414
+ );
415
+ if (virtualRoot)
416
+ for (const resource of inspection.resources)
417
+ getLocalResourcePath(
418
+ new URL(resource.uri, resource.tilesetUrl ?? inspection.rootUrl),
419
+ virtualRoot
420
+ );
421
+ return inspection;
422
+ },
423
+ /** Reads each declared placement with the same lifetime-wide input budget. */
424
+ read: (inspection, signal) => source.read(
425
+ inspection,
426
+ signal ? AbortSignal.any([lifetimeSignal, signal]) : lifetimeSignal
427
+ ),
428
+ /** Cancels future reads and releases the archive handle. */
429
+ async close() {
430
+ controller.abort(new TileConversionError4("NODE_SOURCE_CLOSED", "Node source is closed"));
431
+ const file = archiveFile;
432
+ archiveFile = void 0;
433
+ await closeArchive?.();
434
+ await file?.close();
435
+ }
436
+ };
437
+ } catch (error) {
438
+ await closeArchive?.();
439
+ await archiveFile?.close();
440
+ throw error;
441
+ }
442
+ }
443
+ function getLocalResourcePath(input, root) {
444
+ const url = new URL(input instanceof Request ? input.url : String(input));
445
+ if (url.origin !== root.origin || !url.pathname.startsWith(root.pathname))
446
+ throw new TileConversionError4(
447
+ "EXTERNAL_RESOURCE_UNSUPPORTED",
448
+ "Dependencies must resolve inside the selected local input"
449
+ );
450
+ const path = decodeURIComponent(url.pathname.slice(root.pathname.length));
451
+ if (!path || path.startsWith("/") || path.includes("\\") || path.split("/").some((segment) => segment === ".." || segment === "."))
452
+ throw new TileConversionError4(
453
+ "EXTERNAL_RESOURCE_UNSUPPORTED",
454
+ "Dependency paths cannot escape the selected input"
455
+ );
456
+ return path;
457
+ }
458
+ async function openLocalFile(path, maxInputBytes, signal, reserveRead) {
459
+ signal.throwIfAborted();
460
+ const handle = await open2(path, "r");
461
+ try {
462
+ signal.throwIfAborted();
463
+ const information = await handle.stat();
464
+ if (!information.isFile() || !Number.isSafeInteger(information.size) || information.size > maxInputBytes)
465
+ throw new TileConversionError4(
466
+ "INPUT_RESOURCE_TOO_LARGE",
467
+ "Local input must be a regular file within the byte budget"
468
+ );
469
+ const size = information.size;
470
+ return {
471
+ handle,
472
+ size,
473
+ bigsize: BigInt(size),
474
+ url: path,
475
+ /** Reads a bounded range, checking cancellation and unexpected EOF between native reads. */
476
+ async read(start = 0, length = size - Number(start), readSignal) {
477
+ const operationSignal = readSignal ? AbortSignal.any([signal, readSignal]) : signal;
478
+ operationSignal.throwIfAborted();
479
+ const offset = Number(start);
480
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length < 0 || offset > size - length)
481
+ throw new TileConversionError4(
482
+ "INVALID_INPUT_RANGE",
483
+ "Local file reads require a range inside the file"
484
+ );
485
+ reserveRead(length);
486
+ const bytes = new Uint8Array(length);
487
+ let readBytes = 0;
488
+ while (readBytes < length) {
489
+ operationSignal.throwIfAborted();
490
+ const result = await handle.read(
491
+ bytes,
492
+ readBytes,
493
+ length - readBytes,
494
+ offset + readBytes
495
+ );
496
+ if (result.bytesRead === 0)
497
+ throw new TileConversionError4(
498
+ "INPUT_FILE_TRUNCATED",
499
+ "Local input ended before its declared size"
500
+ );
501
+ readBytes += result.bytesRead;
502
+ }
503
+ operationSignal.throwIfAborted();
504
+ return bytes.buffer;
505
+ },
506
+ /** Releases the Node file handle. */
507
+ close: () => handle.close()
508
+ };
509
+ } catch (error) {
510
+ await handle.close();
511
+ throw error;
512
+ }
513
+ }
514
+ var NODE_TILESET_LIMITS;
515
+ var init_node_source = __esm({
516
+ "src/v5/node-source.ts"() {
517
+ "use strict";
518
+ init_resource_fetcher();
519
+ init_archive_source();
520
+ NODE_TILESET_LIMITS = {
521
+ /** Aggregate response bytes and, separately, indexed archive read bytes. */
522
+ maxInputBytes: 16 * 1024 * 1024,
523
+ /** Declared content placements in the explicit root document. */
524
+ maxInputResources: 1e3,
525
+ /** Maximum root/external document placements inspected or traversed. */
526
+ maxTilesetDocuments: 32
527
+ };
528
+ }
529
+ });
530
+
531
+ // src/v5/node-i3s-source.ts
532
+ var node_i3s_source_exports = {};
533
+ __export(node_i3s_source_exports, {
534
+ createNodeI3SConversionSource: () => createNodeI3SConversionSource
535
+ });
536
+ import { realpath as realpath2 } from "node:fs/promises";
537
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
538
+ import { resolve as resolve2 } from "node:path";
539
+ import { TileConversionError as TileConversionError5 } from "@loaders.gl/tile-converter/v5/core";
540
+ import {
541
+ createSelectedI3SMeshConversionSource,
542
+ inspectI3SMeshConversionSource,
543
+ createTileConversionArchiveTransport as createTileConversionArchiveTransport2,
544
+ openTileConversionArchive as openTileConversionArchive2
545
+ } from "@loaders.gl/tile-converter/v5/adapters";
546
+ async function createNodeI3SConversionSource(options) {
547
+ const maxInputBytes = options.maxInputBytes ?? NODE_TILESET_LIMITS.maxInputBytes;
548
+ const maxInputResources = options.maxInputResources ?? NODE_TILESET_LIMITS.maxInputResources;
549
+ if (!["i3s", "slpk"].includes(options.format) || !Number.isSafeInteger(maxInputBytes) || maxInputBytes < 1 || maxInputBytes > NODE_TILESET_LIMITS.maxInputBytes || !Number.isSafeInteger(maxInputResources) || maxInputResources < 1 || maxInputResources > NODE_TILESET_LIMITS.maxInputResources)
550
+ throw new TileConversionError5(
551
+ "INVALID_NODE_SOURCE_OPTIONS",
552
+ "I3S sources require positive budgets within the bounded input profile"
553
+ );
554
+ const controller = new AbortController();
555
+ const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
556
+ signal.throwIfAborted();
557
+ let release;
558
+ let closePromise;
559
+ const close = async () => {
560
+ controller.abort(new TileConversionError5("NODE_SOURCE_CLOSED", "Node source is closed"));
561
+ closePromise ??= release?.() ?? Promise.resolve();
562
+ await closePromise;
563
+ };
564
+ try {
565
+ const remote = /^https?:/i.test(options.input);
566
+ if (options.format === "i3s" && !remote)
567
+ throw new TileConversionError5(
568
+ "UNSUPPORTED_NODE_SOURCE",
569
+ "I3S input requires an HTTP(S) layer URL; use slpk for a local archive"
570
+ );
571
+ let rootUrl;
572
+ let transport;
573
+ if (options.format === "slpk") {
574
+ const archive = remote ? await openTileConversionArchive2(
575
+ { input: options.input, format: "slpk" },
576
+ maxInputBytes,
577
+ signal,
578
+ options.fetcher
579
+ ) : await createTileConversionArchiveTransport2(
580
+ await openLocalFile(
581
+ await realpath2(
582
+ /^file:/i.test(options.input) ? fileURLToPath2(options.input) : resolve2(options.input)
583
+ ),
584
+ maxInputBytes,
585
+ signal,
586
+ () => {
587
+ }
588
+ ),
589
+ "slpk",
590
+ maxInputBytes,
591
+ signal
592
+ );
593
+ release = archive.close;
594
+ rootUrl = archive.rootUrl;
595
+ transport = archive.fetcher;
596
+ } else {
597
+ rootUrl = new URL(options.input).href;
598
+ transport = options.fetcher ?? fetch;
599
+ }
600
+ const bounded = createTileConversionResourceFetcher({
601
+ maxInputBytes,
602
+ signal,
603
+ fetcher: transport
604
+ });
605
+ let metadataBytes = 0;
606
+ const root = new URL(rootUrl);
607
+ const fetcher = async (input, requestOptions) => {
608
+ const url = new URL(input instanceof Request ? input.url : String(input));
609
+ if (url.origin === root.origin && (url.pathname === root.pathname || url.pathname.startsWith(`${root.pathname}/`))) {
610
+ for (const [key, value] of root.searchParams)
611
+ if (!url.searchParams.has(key)) url.searchParams.set(key, value);
612
+ }
613
+ const response = await bounded(
614
+ input instanceof Request ? new Request(url, input) : url,
615
+ requestOptions
616
+ );
617
+ if (response.url && response.url !== url.href)
618
+ throw new TileConversionError5(
619
+ "I3S_REDIRECT_UNSUPPORTED",
620
+ "Use the final I3S resource URL; redirected resource bases are not qualified"
621
+ );
622
+ const bytes = await response.arrayBuffer();
623
+ metadataBytes += bytes.byteLength;
624
+ const result = new Response(bytes, { status: response.status, headers: response.headers });
625
+ Object.defineProperty(result, "url", { value: response.url });
626
+ return result;
627
+ };
628
+ const selectionOptions = {
629
+ rootUrl,
630
+ fetcher,
631
+ signal,
632
+ maxInputBytes,
633
+ maxInputResources,
634
+ dracoLibraryOptions: { decoderProfile: "full" }
635
+ };
636
+ return {
637
+ close,
638
+ /** Reads only layer and node metadata, under the same lifetime-wide budget as conversion. */
639
+ async inspect(operationSignal) {
640
+ signal.throwIfAborted();
641
+ operationSignal?.throwIfAborted();
642
+ const activeSignal = operationSignal ? AbortSignal.any([signal, operationSignal]) : signal;
643
+ const operationFetcher = (input, requestOptions) => fetcher(input, {
644
+ ...requestOptions,
645
+ signal: requestOptions?.signal ? AbortSignal.any([activeSignal, requestOptions.signal]) : activeSignal
646
+ });
647
+ const inspection = await inspectI3SMeshConversionSource({
648
+ ...selectionOptions,
649
+ fetcher: operationFetcher,
650
+ signal: activeSignal
651
+ });
652
+ return { ...inspection, metadataBytes };
653
+ },
654
+ /** Creates an original-coordinate selected mesh source using application-provided Draco assets. */
655
+ select: (inspection, resourceIds, dracoLibraryOptions, features) => {
656
+ signal.throwIfAborted();
657
+ return createSelectedI3SMeshConversionSource(inspection, {
658
+ ...selectionOptions,
659
+ resourceIds,
660
+ dracoLibraryOptions,
661
+ features
662
+ });
663
+ }
664
+ };
665
+ } catch (error) {
666
+ await close();
667
+ throw error;
668
+ }
669
+ }
670
+ var init_node_i3s_source = __esm({
671
+ "src/v5/node-i3s-source.ts"() {
672
+ "use strict";
673
+ init_resource_fetcher();
674
+ init_node_source();
675
+ }
676
+ });
677
+
678
+ // src/v5/node-conversion.ts
679
+ var node_conversion_exports = {};
680
+ __export(node_conversion_exports, {
681
+ convertNodeTileset: () => convertNodeTileset,
682
+ writeNodeTileConversionOutput: () => writeNodeTileConversionOutput
683
+ });
684
+ import { mkdir, open as open3, rm, link, mkdtemp } from "node:fs/promises";
685
+ import { dirname as dirname2, join, resolve as resolve3 } from "node:path";
686
+ import { convertTileset, TileConversionError as TileConversionError6 } from "@loaders.gl/tile-converter/v5/core";
687
+ import {
688
+ createSelectedMeshConversionSource,
689
+ measureMeshBytes,
690
+ SELECTED_MESH_LIMITS,
691
+ createMeshConversionCodec,
692
+ createI3SMeshConversionCodec,
693
+ createMeshTilesetSink,
694
+ createMeshHierarchyTilesetSink,
695
+ createI3SMeshSink,
696
+ encodeTileConversionArchiveInBatches
697
+ } from "@loaders.gl/tile-converter/v5/adapters";
698
+ async function convertNodeTileset(options) {
699
+ const signal = options.signal ?? new AbortController().signal;
700
+ const maxOutputBytes = options.maxOutputBytes ?? SELECTED_MESH_LIMITS.maxOutputBytes;
701
+ if (!["3d-tiles", "3tz", "slpk"].includes(options.outputFormat) || !Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > SELECTED_MESH_LIMITS.maxOutputBytes)
702
+ throw new TileConversionError6(
703
+ "INVALID_NODE_CONVERSION_OPTIONS",
704
+ "Select a supported output profile and a positive output budget at most 32 MiB"
705
+ );
706
+ const i3s = options.format === "i3s" || options.format === "slpk";
707
+ if (options.preserveHierarchy !== void 0 && typeof options.preserveHierarchy !== "boolean" || options.preserveHierarchy && (i3s || options.outputFormat === "slpk"))
708
+ throw new TileConversionError6(
709
+ "MESH_HIERARCHY_UNSUPPORTED",
710
+ "Hierarchy preservation requires 3D Tiles/3TZ input and output"
711
+ );
712
+ if (i3s && (options.outputFormat !== "slpk" && options.geometricError === void 0 || options.geometricError !== void 0 && (typeof options.geometricError !== "number" || !Number.isFinite(options.geometricError) || options.geometricError < 0)))
713
+ throw new TileConversionError6(
714
+ "MESH_GEOMETRIC_ERROR_INVALID",
715
+ "I3S to 3D Tiles requires an explicit nonnegative geometric error in meters"
716
+ );
717
+ if (i3s ? options.features !== void 0 : options.i3sFeatures !== void 0 || options.integratedMeshPolicy !== void 0 || options.geometricError !== void 0)
718
+ throw new TileConversionError6(
719
+ "INVALID_NODE_CONVERSION_OPTIONS",
720
+ "Feature and layer policies must match the selected input format"
721
+ );
722
+ const sourceOptions = {
723
+ ...options,
724
+ signal,
725
+ inspectNestedTilesets: true,
726
+ maxTilesetDocuments: options.maxTilesetDocuments ?? SELECTED_MESH_LIMITS.maxTilesetDocuments
727
+ };
728
+ const rawSource = i3s ? await createNodeI3SConversionSource({
729
+ ...sourceOptions,
730
+ format: options.format
731
+ }) : await createNodeTilesetConversionSource({
732
+ ...sourceOptions,
733
+ format: options.format
734
+ });
735
+ let files;
736
+ let report;
737
+ try {
738
+ const inspection = await rawSource.inspect(signal);
739
+ await import("@loaders.gl/polyfills");
740
+ const { getDracoLibraryOptions } = await import("@loaders.gl/draco/bundled");
741
+ const dracoLibraryOptions = getDracoLibraryOptions({
742
+ decoderProfile: "full",
743
+ useLocalLibraries: true
744
+ });
745
+ const selection = "select" in rawSource ? {
746
+ ...await rawSource.select(
747
+ inspection,
748
+ options.resourceIds,
749
+ dracoLibraryOptions,
750
+ options.i3sFeatures
751
+ ),
752
+ geometricError: (options.geometricError ?? 0) + SELECTED_MESH_LIMITS.maxPositionError
753
+ } : await createSelectedMeshConversionSource(
754
+ inspection,
755
+ {
756
+ resourceIds: options.resourceIds,
757
+ preserveHierarchy: options.preserveHierarchy,
758
+ features: options.features,
759
+ signal,
760
+ source: rawSource,
761
+ fetcher: rawSource.fetcher,
762
+ dracoLibraryOptions,
763
+ maxInputBytes: options.maxInputBytes
764
+ }
765
+ );
766
+ const { source, spatialContext, geometricError } = selection;
767
+ const common = {
768
+ source,
769
+ signal,
770
+ measureInputBytes: measureMeshBytes,
771
+ maxInputBytes: options.maxInputBytes ?? SELECTED_MESH_LIMITS.maxInputBytes,
772
+ maxOutputBytes,
773
+ maxInputResources: SELECTED_MESH_LIMITS.maxMeshResources,
774
+ maxOutputResources: SELECTED_MESH_LIMITS.maxMeshResources
775
+ };
776
+ const codecOptions = {
777
+ spatialContext,
778
+ autoOrigin: i3s,
779
+ integratedMeshPolicy: options.integratedMeshPolicy,
780
+ dracoLibraryOptions,
781
+ maxPositionError: SELECTED_MESH_LIMITS.maxPositionError
782
+ };
783
+ if (options.outputFormat === "slpk") {
784
+ const sink = createI3SMeshSink({
785
+ maxTotalBytes: maxOutputBytes,
786
+ maxMeshes: SELECTED_MESH_LIMITS.maxMeshResources,
787
+ maxResourceBytes: maxOutputBytes
788
+ });
789
+ report = await convertTileset({
790
+ ...common,
791
+ sink,
792
+ codec: createI3SMeshConversionCodec({ ...codecOptions, maxResourceBytes: maxOutputBytes }),
793
+ measureOutputBytes: (resource) => Object.values(resource.files).reduce((total, bytes) => total + bytes.byteLength, 0)
794
+ });
795
+ files = sink.getFiles();
796
+ } else {
797
+ const sink = options.preserveHierarchy ? createMeshHierarchyTilesetSink({
798
+ inspection,
799
+ resourceIds: options.resourceIds,
800
+ maxTotalBytes: maxOutputBytes,
801
+ maxPositionError: SELECTED_MESH_LIMITS.maxPositionError
802
+ }) : createMeshTilesetSink({
803
+ maxTotalBytes: maxOutputBytes,
804
+ maxMeshes: SELECTED_MESH_LIMITS.maxMeshResources,
805
+ geometricError
806
+ });
807
+ report = await convertTileset({
808
+ ...common,
809
+ sink,
810
+ codec: createMeshConversionCodec(codecOptions),
811
+ measureOutputBytes: (resource) => resource.glb.byteLength
812
+ });
813
+ files = sink.getFiles();
814
+ }
815
+ } finally {
816
+ await rawSource.close();
817
+ }
818
+ signal.throwIfAborted();
819
+ if (options.outputFormat !== "3d-tiles" && !options.output) {
820
+ for await (const chunk of encodeTileConversionArchiveInBatches(files, {
821
+ format: options.outputFormat,
822
+ maxArchiveBytes: maxOutputBytes,
823
+ signal
824
+ }))
825
+ void chunk;
826
+ }
827
+ if (options.output)
828
+ await writeNodeTileConversionOutput(
829
+ files,
830
+ options.output,
831
+ options.outputFormat,
832
+ maxOutputBytes,
833
+ signal
834
+ );
835
+ return report;
836
+ }
837
+ async function writeNodeTileConversionOutput(files, output, format, maxOutputBytes, signal) {
838
+ signal.throwIfAborted();
839
+ if (!["3d-tiles", "3tz", "slpk"].includes(format) || !Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1)
840
+ throw new TileConversionError6(
841
+ "INVALID_NODE_OUTPUT",
842
+ "Output requires a supported format and positive safe byte limit"
843
+ );
844
+ let bytes = 0;
845
+ const names = /* @__PURE__ */ new Set();
846
+ for (const file of files) {
847
+ const name = file.resourceId;
848
+ if (!name || name.includes("\\") || name.includes("\0") || name.includes(":") || name.split("/").some((segment) => !segment || segment === "." || segment === "..") || names.has(name))
849
+ throw new TileConversionError6(
850
+ "INVALID_NODE_OUTPUT",
851
+ "Output names must be unique confined relative resource paths"
852
+ );
853
+ names.add(name);
854
+ if (file.blob.size > maxOutputBytes - bytes)
855
+ throw new TileConversionError6(
856
+ "OUTPUT_BYTES_LIMIT_EXCEEDED",
857
+ "Output files exceed the byte budget"
858
+ );
859
+ bytes += file.blob.size;
860
+ }
861
+ const destination = resolve3(output);
862
+ if (format === "3d-tiles") {
863
+ await mkdir(destination);
864
+ try {
865
+ for (const file of files) {
866
+ signal.throwIfAborted();
867
+ const path = join(destination, file.resourceId);
868
+ await mkdir(dirname2(path), { recursive: true });
869
+ const handle = await open3(path, "wx");
870
+ try {
871
+ const reader = file.blob.stream().getReader();
872
+ try {
873
+ while (true) {
874
+ signal.throwIfAborted();
875
+ const { value, done } = await reader.read();
876
+ if (done) break;
877
+ await writeFileBytes(handle, value, signal);
878
+ }
879
+ } finally {
880
+ await reader.cancel();
881
+ reader.releaseLock();
882
+ }
883
+ } finally {
884
+ await handle.close();
885
+ }
886
+ }
887
+ signal.throwIfAborted();
888
+ } catch (error) {
889
+ await rm(destination, { recursive: true, force: true });
890
+ throw error;
891
+ }
892
+ } else {
893
+ const temporaryDirectory = await mkdtemp(join(dirname2(destination), ".tile-converter-"));
894
+ try {
895
+ const path = join(temporaryDirectory, "archive");
896
+ const handle = await open3(path, "wx");
897
+ try {
898
+ for await (const chunk of encodeTileConversionArchiveInBatches(files, {
899
+ format,
900
+ maxArchiveBytes: maxOutputBytes,
901
+ signal
902
+ }))
903
+ await writeFileBytes(handle, chunk, signal);
904
+ } finally {
905
+ await handle.close();
906
+ }
907
+ signal.throwIfAborted();
908
+ await link(path, destination);
909
+ } finally {
910
+ await rm(temporaryDirectory, { recursive: true, force: true });
911
+ }
912
+ }
913
+ }
914
+ async function writeFileBytes(handle, bytes, signal) {
915
+ let offset = 0;
916
+ while (offset < bytes.byteLength) {
917
+ signal.throwIfAborted();
918
+ const result = await handle.write(bytes, offset, bytes.byteLength - offset);
919
+ if (!result.bytesWritten)
920
+ throw new TileConversionError6("NODE_OUTPUT_WRITE_FAILED", "Output write made no progress");
921
+ offset += result.bytesWritten;
922
+ }
923
+ }
924
+ var init_node_conversion = __esm({
925
+ "src/v5/node-conversion.ts"() {
926
+ "use strict";
927
+ init_node_source();
928
+ init_node_i3s_source();
929
+ }
930
+ });
931
+
932
+ // src/v5/node-cli.ts
933
+ import { parseArgs } from "node:util";
934
+
935
+ // src/v5/node-feature-schema.ts
936
+ import { constants } from "node:fs";
937
+ import { open } from "node:fs/promises";
938
+ import { TileConversionError } from "@loaders.gl/tile-converter/v5/core";
939
+ var MAXIMUM_SCHEMA_BYTES = 64 * 1024;
940
+ var FIELD_TYPES = [
941
+ "int8",
942
+ "uint8",
943
+ "int16",
944
+ "uint16",
945
+ "int32",
946
+ "uint32",
947
+ "int64",
948
+ "uint64",
949
+ "float32",
950
+ "float64",
951
+ "utf8"
952
+ ];
953
+ async function readFeatureSchema(path, i3s, signal) {
954
+ signal?.throwIfAborted();
955
+ const file = await open(path, constants.O_RDONLY | constants.O_NONBLOCK);
956
+ let mapping;
957
+ try {
958
+ if (!(await file.stat()).isFile()) rejectSchema("Feature schema must be a regular local file");
959
+ const bytes = new Uint8Array(MAXIMUM_SCHEMA_BYTES + 1);
960
+ let length = 0;
961
+ while (length < bytes.length) {
962
+ signal?.throwIfAborted();
963
+ const { bytesRead } = await file.read(bytes, length, bytes.length - length, null);
964
+ if (!bytesRead) break;
965
+ length += bytesRead;
966
+ }
967
+ signal?.throwIfAborted();
968
+ if (length > MAXIMUM_SCHEMA_BYTES) rejectSchema("Feature schema exceeds 64 KiB");
969
+ try {
970
+ mapping = JSON.parse(
971
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, length))
972
+ );
973
+ } catch {
974
+ rejectSchema("Feature schema must contain valid UTF-8 JSON");
975
+ }
976
+ } finally {
977
+ await file.close();
978
+ }
979
+ const options = requireRecord(mapping);
980
+ rejectUnknownKeys(options, [
981
+ "schema",
982
+ "metadataClass",
983
+ "sourceFeatureIdProperty",
984
+ "featureIdField",
985
+ "batchSize",
986
+ "integer64Encoding",
987
+ ...i3s ? ["objectIdProperty", "maxAttributeBytes"] : []
988
+ ]);
989
+ requireName(options.metadataClass);
990
+ for (const key of ["sourceFeatureIdProperty", "featureIdField"]) {
991
+ if (options[key] !== void 0) requireName(options[key]);
992
+ }
993
+ if (options.batchSize !== void 0 && (!Number.isSafeInteger(options.batchSize) || Number(options.batchSize) < 1 || Number(options.batchSize) > 65536))
994
+ rejectSchema("batchSize must be an integer from 1 to 65536");
995
+ if (options.integer64Encoding !== void 0 && options.integer64Encoding !== "decimal-string")
996
+ rejectSchema("integer64Encoding must be decimal-string when supplied");
997
+ const schema = requireRecord(options.schema);
998
+ rejectUnknownKeys(schema, ["fields", "metadata"]);
999
+ validateMetadata(schema.metadata);
1000
+ if (!Array.isArray(schema.fields) || !schema.fields.length || schema.fields.length > 256)
1001
+ rejectSchema("schema.fields must declare between 1 and 256 fields");
1002
+ const names = /* @__PURE__ */ new Set();
1003
+ for (const value of schema.fields) {
1004
+ const field = requireRecord(value);
1005
+ rejectUnknownKeys(field, ["name", "type", "nullable", "metadata"]);
1006
+ requireName(field.name);
1007
+ if (names.has(field.name)) rejectSchema("Feature fields must have unique names");
1008
+ names.add(field.name);
1009
+ if (typeof field.type !== "string" || !FIELD_TYPES.includes(field.type) || field.nullable !== void 0 && typeof field.nullable !== "boolean")
1010
+ rejectSchema("Fields require a scalar numeric/utf8 type and an optional boolean nullable");
1011
+ validateMetadata(field.metadata);
1012
+ }
1013
+ const identifier = schema.fields.find(
1014
+ (value) => value.name === (options.featureIdField ?? "feature_id")
1015
+ );
1016
+ if (!identifier || identifier.nullable === true || !["utf8", ...FIELD_TYPES.filter((type) => /^(u?int)/.test(type))].includes(identifier.type))
1017
+ rejectSchema("Declare a nonnullable string or integer feature ID field");
1018
+ if (i3s) {
1019
+ requireName(options.objectIdProperty);
1020
+ if (!Number.isSafeInteger(options.maxAttributeBytes) || Number(options.maxAttributeBytes) < 1 || Number(options.maxAttributeBytes) > 16 * 1024 * 1024)
1021
+ rejectSchema("maxAttributeBytes must be a positive integer at most 16 MiB");
1022
+ return {
1023
+ i3sFeatures: options
1024
+ };
1025
+ }
1026
+ return { features: options };
1027
+ }
1028
+ function rejectUnknownKeys(record, allowed) {
1029
+ if (Object.keys(record).some((key) => !allowed.includes(key)))
1030
+ rejectSchema("Unknown feature-schema option");
1031
+ }
1032
+ function requireRecord(value) {
1033
+ if (!value || typeof value !== "object" || Array.isArray(value))
1034
+ rejectSchema("Feature mappings require JSON objects");
1035
+ return value;
1036
+ }
1037
+ function requireName(value) {
1038
+ if (typeof value !== "string" || !value.trim())
1039
+ rejectSchema("Feature names must be nonempty strings");
1040
+ }
1041
+ function validateMetadata(value) {
1042
+ if (value === void 0) return;
1043
+ if (Object.values(requireRecord(value)).some((entry) => typeof entry !== "string"))
1044
+ rejectSchema("Schema metadata values must be strings");
1045
+ }
1046
+ function rejectSchema(message) {
1047
+ throw new TileConversionError("CLI_FEATURE_SCHEMA_INVALID", message);
1048
+ }
1049
+
1050
+ // src/v5/node-cli.ts
1051
+ init_node_source();
1052
+ import { TileConversionError as TileConversionError7 } from "@loaders.gl/tile-converter/v5/core";
1053
+ var HELP = `Usage: tile-converter-v5 inspect|convert|validate <input path or URL> [options]
1054
+
1055
+ --input-format 3d-tiles|3tz|i3s|slpk Explicit input format (default: 3d-tiles)
1056
+ --geometric-error N Required source error in meters for I3S to 3D Tiles
1057
+ --integrated-mesh-policy as-3d-object Authorize I3S layer-type change
1058
+ --preserve-hierarchy Retain a complete explicit 3D Tiles tree (3D Tiles/3TZ only)
1059
+ --feature-schema PATH Local JSON feature class/schema mapping (at most 64 KiB)
1060
+ --resource ID Selected content placement; repeat for multiple selections
1061
+ --output-format 3d-tiles|3tz|slpk Target profile (default: 3tz)
1062
+ --output PATH New destination (required for convert, forbidden for validate)
1063
+ --max-input-bytes N Lower the 16 MiB input/read budget
1064
+ --max-resources N Lower the 1,000 content-declaration limit
1065
+ --max-documents N Lower the 32 metadata-document limit
1066
+ --max-output-bytes N Lower the 32 MiB output/archive budget
1067
+ --help Show this help
1068
+
1069
+ Inspect reads metadata only, following declared .json external tilesets.
1070
+ Convert/validate require explicit selections; I3S uses declared CRS and shared spatial preparation.
1071
+ Validate decodes, converts and packages the selected target profile without writing files.
1072
+ Output defaults to a partial flat mesh collection. --preserve-hierarchy requires every declared content.
1073
+ Remote archives require HTTP 206 ranges and a stable ETag or Last-Modified validator.
1074
+ I3S inspect lists leaf nodes; feature-bearing conversion requires --feature-schema.
1075
+ Hierarchy mode supports at most 32 document placements, 256 tiles and 64 total mesh primitive placements, including ordered 1.1 contents arrays.
1076
+ `;
1077
+ async function runTileConverterCLI(argumentsList, output, signal) {
1078
+ try {
1079
+ const { values, positionals } = parseArgs({
1080
+ args: argumentsList,
1081
+ allowPositionals: true,
1082
+ options: {
1083
+ help: { type: "boolean" },
1084
+ "preserve-hierarchy": { type: "boolean" },
1085
+ "input-format": { type: "string", default: "3d-tiles" },
1086
+ "max-input-bytes": { type: "string", default: String(NODE_TILESET_LIMITS.maxInputBytes) },
1087
+ "max-resources": { type: "string", default: String(NODE_TILESET_LIMITS.maxInputResources) },
1088
+ "max-documents": { type: "string", default: String(NODE_TILESET_LIMITS.maxTilesetDocuments) },
1089
+ "max-output-bytes": { type: "string", default: String(32 * 1024 * 1024) },
1090
+ "output-format": { type: "string", default: "3tz" },
1091
+ output: { type: "string" },
1092
+ resource: { type: "string", multiple: true },
1093
+ "feature-schema": { type: "string" },
1094
+ "geometric-error": { type: "string" },
1095
+ "integrated-mesh-policy": { type: "string" }
1096
+ }
1097
+ });
1098
+ if (values.help || !argumentsList.length) {
1099
+ output.stdout(HELP);
1100
+ return 0;
1101
+ }
1102
+ if (positionals.length !== 2 || !["inspect", "convert", "validate"].includes(positionals[0]) || !["3d-tiles", "3tz", "i3s", "slpk"].includes(values["input-format"]))
1103
+ throw new TileConversionError7(
1104
+ "CLI_USAGE_ERROR",
1105
+ "Use inspect, convert or validate with one input and --input-format 3d-tiles, 3tz, i3s or slpk"
1106
+ );
1107
+ const format = values["input-format"];
1108
+ const operation = positionals[0];
1109
+ const sourceOptions = {
1110
+ input: positionals[1],
1111
+ format,
1112
+ signal,
1113
+ maxInputBytes: parseLimit(values["max-input-bytes"]),
1114
+ maxInputResources: parseLimit(values["max-resources"]),
1115
+ maxTilesetDocuments: parseLimit(values["max-documents"]),
1116
+ inspectNestedTilesets: true
1117
+ };
1118
+ if (operation !== "inspect") {
1119
+ if (!values.resource?.length || !["3d-tiles", "3tz", "slpk"].includes(values["output-format"]) || (operation === "convert" ? !values.output : values.output !== void 0))
1120
+ throw new TileConversionError7(
1121
+ "CLI_USAGE_ERROR",
1122
+ "Convert/validate require --resource selections and a supported --output-format; only convert requires --output"
1123
+ );
1124
+ if (values["integrated-mesh-policy"] !== void 0 && values["integrated-mesh-policy"] !== "as-3d-object")
1125
+ throw new TileConversionError7(
1126
+ "CLI_USAGE_ERROR",
1127
+ "Use --integrated-mesh-policy as-3d-object"
1128
+ );
1129
+ const features = values["feature-schema"] === void 0 ? void 0 : await readFeatureSchema(
1130
+ values["feature-schema"],
1131
+ format === "i3s" || format === "slpk",
1132
+ signal
1133
+ );
1134
+ const { convertNodeTileset: convertNodeTileset2 } = await Promise.resolve().then(() => (init_node_conversion(), node_conversion_exports));
1135
+ const report = await convertNodeTileset2({
1136
+ ...sourceOptions,
1137
+ ...features,
1138
+ resourceIds: values.resource,
1139
+ preserveHierarchy: values["preserve-hierarchy"],
1140
+ outputFormat: values["output-format"],
1141
+ output: values.output,
1142
+ maxOutputBytes: parseLimit(values["max-output-bytes"]),
1143
+ geometricError: values["geometric-error"] === void 0 ? void 0 : parseGeometricError(values["geometric-error"]),
1144
+ integratedMeshPolicy: values["integrated-mesh-policy"]
1145
+ });
1146
+ output.stdout(
1147
+ `${JSON.stringify({ operation, input: positionals[1], inputFormat: format, outputFormat: values["output-format"], ...values.output ? { output: values.output } : {}, report }, null, 2)}
1148
+ `
1149
+ );
1150
+ return 0;
1151
+ }
1152
+ if (values["feature-schema"] !== void 0 || values["preserve-hierarchy"] !== void 0 || values["geometric-error"] !== void 0 || values["integrated-mesh-policy"] !== void 0 || values.resource || values.output || values["output-format"] !== "3tz" || values["max-output-bytes"] !== String(32 * 1024 * 1024))
1153
+ throw new TileConversionError7(
1154
+ "CLI_USAGE_ERROR",
1155
+ "Inspect does not accept conversion/output options"
1156
+ );
1157
+ const source = format === "i3s" || format === "slpk" ? await (await Promise.resolve().then(() => (init_node_i3s_source(), node_i3s_source_exports))).createNodeI3SConversionSource({
1158
+ ...sourceOptions,
1159
+ format
1160
+ }) : await createNodeTilesetConversionSource({ ...sourceOptions, format });
1161
+ const inspection = await source.inspect(signal).finally(() => source.close());
1162
+ output.stdout(
1163
+ `${JSON.stringify({ operation: "inspect", input: positionals[1], inputFormat: format, ...inspection }, null, 2)}
1164
+ `
1165
+ );
1166
+ return 0;
1167
+ } catch (error) {
1168
+ output.stderr(
1169
+ `${JSON.stringify({
1170
+ code: error instanceof Error && "code" in error ? error.code : "CONVERSION_FAILED",
1171
+ message: error instanceof Error ? error.message : String(error),
1172
+ diagnostics: error instanceof TileConversionError7 ? error.diagnostics : []
1173
+ })}
1174
+ `
1175
+ );
1176
+ return 1;
1177
+ }
1178
+ }
1179
+ function parseLimit(value) {
1180
+ if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value)))
1181
+ throw new TileConversionError7(
1182
+ "CLI_USAGE_ERROR",
1183
+ "Limits must be positive safe decimal integers"
1184
+ );
1185
+ return Number(value);
1186
+ }
1187
+ function parseGeometricError(value) {
1188
+ if (!/^(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(value) || !Number.isFinite(Number(value)))
1189
+ throw new TileConversionError7(
1190
+ "CLI_USAGE_ERROR",
1191
+ "Geometric error must be a finite nonnegative number in meters"
1192
+ );
1193
+ return Number(value);
1194
+ }
1195
+
1196
+ // src/v5/converter-cli.ts
1197
+ async function main() {
1198
+ const controller = new AbortController();
1199
+ const cancelOperation = () => controller.abort(new Error("Tile conversion operation interrupted"));
1200
+ process.on("SIGINT", cancelOperation);
1201
+ try {
1202
+ process.exitCode = await runTileConverterCLI(
1203
+ process.argv.slice(2),
1204
+ {
1205
+ stdout: (text) => process.stdout.write(text),
1206
+ stderr: (text) => process.stderr.write(text)
1207
+ },
1208
+ controller.signal
1209
+ );
1210
+ } finally {
1211
+ process.removeListener("SIGINT", cancelOperation);
1212
+ }
1213
+ }
1214
+ void main();