@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
@@ -0,0 +1,328 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {open, realpath} from 'node:fs/promises';
6
+ import {basename, dirname, isAbsolute, relative, resolve, sep} from 'node:path';
7
+ import {fileURLToPath} from 'node:url';
8
+ import type {ReadableFile} from '@loaders.gl/core/loader-utils';
9
+ import {parse3DTilesArchive} from '@loaders.gl/tiles-3d/3d-tiles';
10
+ import {
11
+ createBrowserTilesetConversionSource,
12
+ TileConversionError
13
+ } from '@loaders.gl/tile-converter/v5/core';
14
+ import type {
15
+ BrowserTilesetConversionInspection,
16
+ BrowserTilesetConversionInputResource,
17
+ TileConversionSource
18
+ } from '@loaders.gl/tile-converter/v5/core';
19
+ import {createTileConversionResourceFetcher} from './resource-fetcher.js';
20
+ import {openTileConversionArchive} from './archive-source.js';
21
+
22
+ /** Maximum budgets for the initial Node source profile; callers may lower them. */
23
+ export const NODE_TILESET_LIMITS = {
24
+ /** Aggregate response bytes and, separately, indexed archive read bytes. */
25
+ maxInputBytes: 16 * 1024 * 1024,
26
+ /** Declared content placements in the explicit root document. */
27
+ maxInputResources: 1000,
28
+ /** Maximum root/external document placements inspected or traversed. */
29
+ maxTilesetDocuments: 32
30
+ } as const;
31
+
32
+ /** Platform I/O options for bounded explicit 3D Tiles inspection and raw content reads. */
33
+ export interface NodeTilesetConversionSourceOptions {
34
+ /** Local tileset JSON/3TZ path, file URL, or HTTP(S) tileset JSON URL. */
35
+ readonly input: string;
36
+ /** Explicit JSON or indexed archive input format. */
37
+ readonly format?: '3d-tiles' | '3tz';
38
+ /** Positive response/indexed-read budget, at most 16 MiB. */
39
+ readonly maxInputBytes?: number;
40
+ /** Positive content-placement budget, at most 1,000. */
41
+ readonly maxInputResources?: number;
42
+ /** Positive document budget, at most 32; defaults to root-only traversal. */
43
+ readonly maxTilesetDocuments?: number;
44
+ /** Follow declared .json external documents during metadata-only inspection. */
45
+ readonly inspectNestedTilesets?: boolean;
46
+ /** Cancellation for opening, inspection and subsequent reads. */
47
+ readonly signal?: AbortSignal;
48
+ /** HTTP(S) transport; local dependencies never fall back to it. */
49
+ readonly fetcher?: typeof fetch;
50
+ }
51
+
52
+ /** A dedicated Node source whose owner must close it after inspection or conversion. */
53
+ export interface NodeTilesetConversionSource
54
+ extends TileConversionSource<
55
+ BrowserTilesetConversionInspection,
56
+ BrowserTilesetConversionInputResource
57
+ > {
58
+ /** Aborts further I/O and releases the indexed archive handle; safe to call repeatedly. */
59
+ close(): Promise<void>;
60
+ /** Confined, lifetime-budgeted transport for content-level buffers and images. */
61
+ readonly fetcher: typeof fetch;
62
+ }
63
+
64
+ /**
65
+ * Opens bounded Node I/O and delegates explicit traversal to the portable source.
66
+ * Local resources stay below the root document's real directory, including symlinks;
67
+ * archive resources stay inside the archive. Inspection reads metadata only. Reading yields
68
+ * raw content for an application codec, without CRS inference or content-format qualification.
69
+ * The source is sequential and owns one lifetime-wide response/read budget. Close it in finally.
70
+ */
71
+ export async function createNodeTilesetConversionSource(
72
+ options: NodeTilesetConversionSourceOptions
73
+ ): Promise<NodeTilesetConversionSource> {
74
+ const maxInputBytes = options.maxInputBytes ?? NODE_TILESET_LIMITS.maxInputBytes;
75
+ const maxInputResources = options.maxInputResources ?? NODE_TILESET_LIMITS.maxInputResources;
76
+ const maxTilesetDocuments = options.maxTilesetDocuments ?? 1;
77
+ if (
78
+ !Number.isSafeInteger(maxInputBytes) ||
79
+ maxInputBytes < 1 ||
80
+ maxInputBytes > NODE_TILESET_LIMITS.maxInputBytes ||
81
+ !Number.isSafeInteger(maxInputResources) ||
82
+ maxInputResources < 1 ||
83
+ maxInputResources > NODE_TILESET_LIMITS.maxInputResources ||
84
+ !Number.isSafeInteger(maxTilesetDocuments) ||
85
+ maxTilesetDocuments < 1 ||
86
+ maxTilesetDocuments > NODE_TILESET_LIMITS.maxTilesetDocuments ||
87
+ !['3d-tiles', '3tz'].includes(options.format ?? '3d-tiles')
88
+ )
89
+ throw new TileConversionError(
90
+ 'INVALID_NODE_SOURCE_OPTIONS',
91
+ 'Node sources require positive byte/resource budgets within the initial profile limits'
92
+ );
93
+ options.signal?.throwIfAborted();
94
+ const controller = new AbortController();
95
+ const lifetimeSignal = options.signal
96
+ ? AbortSignal.any([controller.signal, options.signal])
97
+ : controller.signal;
98
+ let archiveFile: ReadableFile | undefined;
99
+ let closeArchive: (() => Promise<void>) | undefined;
100
+ let consumedReadBytes = 0;
101
+ /** Reserves physical file bytes before allocation, including repeated indexed reads. */
102
+ const reserveRead = (length: number): void => {
103
+ if (length > maxInputBytes - consumedReadBytes)
104
+ throw new TileConversionError(
105
+ 'INPUT_RESOURCE_TOO_LARGE',
106
+ 'Local reads exceed the aggregate input budget'
107
+ );
108
+ consumedReadBytes += length;
109
+ };
110
+ try {
111
+ const inputUrl = /^(?:https?|file):/i.test(options.input) ? new URL(options.input) : undefined;
112
+ if (
113
+ !inputUrl &&
114
+ /^[a-z][a-z\d+.-]*:/i.test(options.input) &&
115
+ !/^[a-z]:[\\/]/i.test(options.input)
116
+ )
117
+ throw new TileConversionError(
118
+ 'UNSUPPORTED_NODE_SOURCE',
119
+ 'Use a local path, file URL or HTTP(S) tileset URL'
120
+ );
121
+ const remote = inputUrl && inputUrl.protocol !== 'file:';
122
+ let rootUrl: string;
123
+ let transport: typeof fetch;
124
+ let virtualRoot: URL | undefined;
125
+ if (remote && options.format === '3tz') {
126
+ const archive = await openTileConversionArchive(
127
+ {input: inputUrl.href, format: '3tz'},
128
+ maxInputBytes,
129
+ lifetimeSignal,
130
+ options.fetcher
131
+ );
132
+ rootUrl = archive.rootUrl;
133
+ transport = archive.fetcher;
134
+ closeArchive = archive.close;
135
+ } else if (remote) {
136
+ rootUrl = inputUrl.href;
137
+ transport = options.fetcher ?? fetch;
138
+ } else {
139
+ const inputPath = await realpath(inputUrl ? fileURLToPath(inputUrl) : resolve(options.input));
140
+ lifetimeSignal.throwIfAborted();
141
+ const directory = dirname(inputPath);
142
+ virtualRoot = new URL('https://conversion.invalid/input/');
143
+ rootUrl = new URL(
144
+ options.format === '3tz' ? 'tileset.json' : encodeURIComponent(basename(inputPath)),
145
+ virtualRoot
146
+ ).href;
147
+ if (options.format === '3tz') {
148
+ archiveFile = await openLocalFile(inputPath, maxInputBytes, lifetimeSignal, reserveRead);
149
+ const archive = await parse3DTilesArchive(archiveFile);
150
+ transport = async (input, requestOptions) => {
151
+ lifetimeSignal.throwIfAborted();
152
+ requestOptions?.signal?.throwIfAborted();
153
+ const path = getLocalResourcePath(input, virtualRoot!);
154
+ const bytes = await archive.getFile(path);
155
+ lifetimeSignal.throwIfAborted();
156
+ requestOptions?.signal?.throwIfAborted();
157
+ return new Response(bytes);
158
+ };
159
+ } else {
160
+ transport = async (input, requestOptions) => {
161
+ const path = getLocalResourcePath(input, virtualRoot!);
162
+ const resolvedPath = await realpath(resolve(directory, path));
163
+ const relativePath = relative(directory, resolvedPath);
164
+ if (
165
+ relativePath === '..' ||
166
+ relativePath.startsWith(`..${sep}`) ||
167
+ isAbsolute(relativePath)
168
+ )
169
+ throw new TileConversionError(
170
+ 'EXTERNAL_RESOURCE_UNSUPPORTED',
171
+ 'Local dependencies must remain inside the tileset directory'
172
+ );
173
+ const signal = requestOptions?.signal
174
+ ? AbortSignal.any([lifetimeSignal, requestOptions.signal])
175
+ : lifetimeSignal;
176
+ const file = await openLocalFile(resolvedPath, maxInputBytes, signal, reserveRead);
177
+ try {
178
+ return new Response(await file.read());
179
+ } finally {
180
+ await file.close();
181
+ }
182
+ };
183
+ }
184
+ }
185
+ const resourceFetcher = createTileConversionResourceFetcher({
186
+ maxInputBytes,
187
+ signal: lifetimeSignal,
188
+ fetcher: transport
189
+ });
190
+ const source = createBrowserTilesetConversionSource({
191
+ input: rootUrl,
192
+ maxInputBytes,
193
+ maxInputResources,
194
+ maxTilesetDocuments,
195
+ inspectNestedTilesets: options.inspectNestedTilesets,
196
+ fetcher: resourceFetcher
197
+ });
198
+ return {
199
+ fetcher: resourceFetcher,
200
+ /** Inspects declarations without opening tile payloads or decoding coordinates. */
201
+ async inspect(signal) {
202
+ const inspection = await source.inspect(
203
+ signal ? AbortSignal.any([lifetimeSignal, signal]) : lifetimeSignal
204
+ );
205
+ if (virtualRoot)
206
+ for (const resource of inspection.resources)
207
+ getLocalResourcePath(
208
+ new URL(resource.uri, resource.tilesetUrl ?? inspection.rootUrl),
209
+ virtualRoot
210
+ );
211
+ return inspection;
212
+ },
213
+ /** Reads each declared placement with the same lifetime-wide input budget. */
214
+ read: (inspection, signal) =>
215
+ source.read(
216
+ inspection,
217
+ signal ? AbortSignal.any([lifetimeSignal, signal]) : lifetimeSignal
218
+ ),
219
+ /** Cancels future reads and releases the archive handle. */
220
+ async close() {
221
+ controller.abort(new TileConversionError('NODE_SOURCE_CLOSED', 'Node source is closed'));
222
+ const file = archiveFile;
223
+ archiveFile = undefined;
224
+ await closeArchive?.();
225
+ await file?.close();
226
+ }
227
+ };
228
+ } catch (error) {
229
+ await closeArchive?.();
230
+ await archiveFile?.close();
231
+ throw error;
232
+ }
233
+ }
234
+
235
+ /** Resolves a canonical in-source URL and rejects external or encoded escaping paths. */
236
+ function getLocalResourcePath(input: RequestInfo | URL, root: URL): string {
237
+ const url = new URL(input instanceof Request ? input.url : String(input));
238
+ if (url.origin !== root.origin || !url.pathname.startsWith(root.pathname))
239
+ throw new TileConversionError(
240
+ 'EXTERNAL_RESOURCE_UNSUPPORTED',
241
+ 'Dependencies must resolve inside the selected local input'
242
+ );
243
+ const path = decodeURIComponent(url.pathname.slice(root.pathname.length));
244
+ if (
245
+ !path ||
246
+ path.startsWith('/') ||
247
+ path.includes('\\') ||
248
+ path.split('/').some(segment => segment === '..' || segment === '.')
249
+ )
250
+ throw new TileConversionError(
251
+ 'EXTERNAL_RESOURCE_UNSUPPORTED',
252
+ 'Dependency paths cannot escape the selected input'
253
+ );
254
+ return path;
255
+ }
256
+
257
+ /** Opens a real Node file and provides checked typed-array reads without Node Buffer. */
258
+ export async function openLocalFile(
259
+ path: string,
260
+ maxInputBytes: number,
261
+ signal: AbortSignal,
262
+ reserveRead: (length: number) => void
263
+ ): Promise<ReadableFile> {
264
+ signal.throwIfAborted();
265
+ const handle = await open(path, 'r');
266
+ try {
267
+ signal.throwIfAborted();
268
+ const information = await handle.stat();
269
+ if (
270
+ !information.isFile() ||
271
+ !Number.isSafeInteger(information.size) ||
272
+ information.size > maxInputBytes
273
+ )
274
+ throw new TileConversionError(
275
+ 'INPUT_RESOURCE_TOO_LARGE',
276
+ 'Local input must be a regular file within the byte budget'
277
+ );
278
+ const size = information.size;
279
+ return {
280
+ handle,
281
+ size,
282
+ bigsize: BigInt(size),
283
+ url: path,
284
+ /** Reads a bounded range, checking cancellation and unexpected EOF between native reads. */
285
+ async read(start = 0, length = size - Number(start), readSignal) {
286
+ const operationSignal = readSignal ? AbortSignal.any([signal, readSignal]) : signal;
287
+ operationSignal.throwIfAborted();
288
+ const offset = Number(start);
289
+ if (
290
+ !Number.isSafeInteger(offset) ||
291
+ offset < 0 ||
292
+ !Number.isSafeInteger(length) ||
293
+ length < 0 ||
294
+ offset > size - length
295
+ )
296
+ throw new TileConversionError(
297
+ 'INVALID_INPUT_RANGE',
298
+ 'Local file reads require a range inside the file'
299
+ );
300
+ reserveRead(length);
301
+ const bytes = new Uint8Array(length);
302
+ let readBytes = 0;
303
+ while (readBytes < length) {
304
+ operationSignal.throwIfAborted();
305
+ const result = await handle.read(
306
+ bytes,
307
+ readBytes,
308
+ length - readBytes,
309
+ offset + readBytes
310
+ );
311
+ if (result.bytesRead === 0)
312
+ throw new TileConversionError(
313
+ 'INPUT_FILE_TRUNCATED',
314
+ 'Local input ended before its declared size'
315
+ );
316
+ readBytes += result.bytesRead;
317
+ }
318
+ operationSignal.throwIfAborted();
319
+ return bytes.buffer;
320
+ },
321
+ /** Releases the Node file handle. */
322
+ close: () => handle.close()
323
+ };
324
+ } catch (error) {
325
+ await handle.close();
326
+ throw error;
327
+ }
328
+ }
package/src/v5/node.ts ADDED
@@ -0,0 +1,16 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ // Intentionally Node-only. Portable entrypoints do not import this adapter.
6
+ export {createNodeTilesetConversionSource, NODE_TILESET_LIMITS} from './node-source.js';
7
+ export type {
8
+ NodeTilesetConversionSource,
9
+ NodeTilesetConversionSourceOptions
10
+ } from './node-source.js';
11
+
12
+ export {convertNodeTileset, writeNodeTileConversionOutput} from './node-conversion.js';
13
+ export type {NodeTileConversionOptions, NodeTileConversionOutputFormat} from './node-conversion.js';
14
+
15
+ export {createNodeI3SConversionSource} from './node-i3s-source.js';
16
+ export type {NodeI3SConversionSourceOptions} from './node-i3s-source.js';
@@ -0,0 +1,113 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {Mesh, MeshArrowTable} from '@loaders.gl/core/schema';
6
+ import {convertTableToMesh} from '@loaders.gl/core/schema-utils';
7
+ import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
8
+
9
+ /** Explicit source-name mapping and authorized semantic losses. */
10
+ export interface PointCloudAttributeMapping {
11
+ /** Destination attribute name to source attribute name. Values and normalization remain unchanged. */
12
+ readonly attributes: Readonly<Record<string, string>>;
13
+ /** Source attributes intentionally omitted, each with a nonempty reason. */
14
+ readonly drop?: Readonly<Record<string, string>>;
15
+ }
16
+
17
+ /** Decoded mapped rows and a record of every authorized omission. */
18
+ export interface MappedPointCloud {
19
+ /** Mesh with renamed attributes sharing immutable source values. */
20
+ readonly mesh: Mesh;
21
+ /** Explicit source-name/reason pairs, sorted deterministically. */
22
+ readonly droppedAttributes: Readonly<Record<string, string>>;
23
+ }
24
+
25
+ /** Maps decoded point attributes without implicit aliases, casts, projection or silent omissions. */
26
+ export function mapPointCloudAttributes(
27
+ input: Mesh | MeshArrowTable,
28
+ mapping: PointCloudAttributeMapping
29
+ ): MappedPointCloud {
30
+ const mesh = getPointCloudMesh(input);
31
+ const attributes: Mesh['attributes'] = Object.create(null);
32
+ const selected = new Set<string>();
33
+ for (const [target, source] of Object.entries(mapping.attributes)) {
34
+ if (
35
+ !target ||
36
+ ['__proto__', 'constructor', 'prototype'].includes(target) ||
37
+ !Object.hasOwn(mesh.attributes, source)
38
+ )
39
+ throw new TileConversionError(
40
+ 'POINT_CLOUD_MAPPING_INVALID',
41
+ `Invalid point attribute mapping ${target}: ${source}`
42
+ );
43
+ if (Object.hasOwn(mapping.drop || {}, source))
44
+ throw new TileConversionError(
45
+ 'POINT_CLOUD_MAPPING_INVALID',
46
+ `Point attribute ${source} is both selected and dropped`
47
+ );
48
+ attributes[target] = {...mesh.attributes[source]};
49
+ selected.add(source);
50
+ }
51
+ const droppedAttributes: Record<string, string> = Object.create(null);
52
+ for (const [source, reason] of Object.entries(mapping.drop || {}).sort(([left], [right]) =>
53
+ left < right ? -1 : left > right ? 1 : 0
54
+ )) {
55
+ if (!Object.hasOwn(mesh.attributes, source) || typeof reason !== 'string' || !reason.trim())
56
+ throw new TileConversionError(
57
+ 'POINT_CLOUD_MAPPING_INVALID',
58
+ `Invalid loss declaration for ${source}`
59
+ );
60
+ droppedAttributes[source] = reason;
61
+ }
62
+ const unmapped = Object.keys(mesh.attributes).filter(
63
+ name => !selected.has(name) && !Object.hasOwn(droppedAttributes, name)
64
+ );
65
+ if (unmapped.length || !attributes.POSITION)
66
+ throw new TileConversionError(
67
+ 'POINT_CLOUD_MAPPING_REQUIRED',
68
+ `Map POSITION and explicitly retain or drop every attribute: ${unmapped.join(', ')}`
69
+ );
70
+ return {
71
+ mesh: {...mesh, attributes, schema: {fields: [], metadata: {...mesh.schema?.metadata}}},
72
+ droppedAttributes
73
+ };
74
+ }
75
+
76
+ /** Converts decoded point input after optional retained-buffer accounting and null validation. */
77
+ export function getPointCloudMesh(input: Mesh | MeshArrowTable, maxInputBytes = Infinity): Mesh {
78
+ const buffers = new Set<ArrayBufferLike>();
79
+ let bytes = 0;
80
+ /** Counts distinct retained buffers rather than narrow views. */
81
+ const countBuffer = (buffer: ArrayBufferLike): void => {
82
+ if (!buffers.has(buffer)) {
83
+ buffers.add(buffer);
84
+ bytes += buffer.byteLength;
85
+ }
86
+ if (bytes > maxInputBytes)
87
+ throw new TileConversionError('POINT_CLOUD_INPUT_BUDGET', 'Point input byte budget exceeded');
88
+ };
89
+ if ('shape' in input) {
90
+ /** Validates nullable nested Arrow columns before conversion. */
91
+ type ArrowColumn = MeshArrowTable['data']['batches'][number]['data']['children'][number];
92
+ const checkColumn = (column: ArrowColumn): void => {
93
+ if (column.nullCount)
94
+ throw new TileConversionError(
95
+ 'POINT_CLOUD_NULL_UNSUPPORTED',
96
+ 'Null point values require an explicit preprocessing step'
97
+ );
98
+ for (const buffer of Object.values(column.buffers)) if (buffer) countBuffer(buffer.buffer);
99
+ for (const view of column.variadicBuffers) countBuffer(view.buffer);
100
+ for (const child of column.children) checkColumn(child);
101
+ for (const dictionary of column.dictionary?.data || []) checkColumn(dictionary);
102
+ };
103
+ for (const batch of input.data.batches) checkColumn(batch.data);
104
+ } else
105
+ for (const attribute of Object.values(input.attributes)) countBuffer(attribute.value.buffer);
106
+ const mesh = 'shape' in input ? convertTableToMesh(input) : input;
107
+ if (mesh.topology !== 'point-list' || mesh.indices)
108
+ throw new TileConversionError(
109
+ 'POINT_CLOUD_INPUT_INVALID',
110
+ 'Decoded unindexed point-list input is required'
111
+ );
112
+ return mesh;
113
+ }
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import type {PointCloudTilesetSource} from '@loaders.gl/tiles';
5
+ import type {PointCloudTilesetSource} from '@loaders.gl/tiles-3d';
6
6
  import {convertTileset} from '@loaders.gl/tile-converter/v5/core';
7
7
  import type {
8
8
  TileConversionProgress,