@loaders.gl/tile-converter 5.0.0-alpha.10 → 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 (107) hide show
  1. package/README.md +211 -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 +1933 -2232
  6. package/dist/index.js +1925 -2235
  7. package/dist/v5/adapters.cjs +3186 -208
  8. package/dist/v5/adapters.d.ts +29 -2
  9. package/dist/v5/adapters.js +3202 -196
  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 +30 -0
  13. package/dist/v5/browser.d.ts +26 -2
  14. package/dist/v5/browser.js +45 -1
  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 +515 -50
  19. package/dist/v5/core.d.ts +2 -0
  20. package/dist/v5/core.js +515 -50
  21. package/dist/v5/i3s-mesh-conversion.d.ts +14 -4
  22. package/dist/v5/i3s-mesh-sink.d.ts +25 -0
  23. package/dist/v5/i3s-mesh-source-features.d.ts +13 -0
  24. package/dist/v5/i3s-mesh-source-material.d.ts +5 -0
  25. package/dist/v5/i3s-mesh-source.d.ts +30 -0
  26. package/dist/v5/i3s-selection-source.d.ts +70 -0
  27. package/dist/v5/index.cjs +20 -0
  28. package/dist/v5/index.d.ts +18 -2
  29. package/dist/v5/index.js +31 -1
  30. package/dist/v5/mesh-conversion.d.ts +29 -2
  31. package/dist/v5/mesh-features.d.ts +9 -0
  32. package/dist/v5/mesh-hierarchy.d.ts +47 -0
  33. package/dist/v5/mesh-source-features.d.ts +2 -1
  34. package/dist/v5/mesh-source-schema.d.ts +12 -0
  35. package/dist/v5/mesh-source-texture.d.ts +2 -0
  36. package/dist/v5/mesh-source.d.ts +13 -3
  37. package/dist/v5/mesh-tileset-sink.d.ts +5 -0
  38. package/dist/v5/mesh.d.ts +6 -1
  39. package/dist/v5/node-cli.d.ts +9 -0
  40. package/dist/v5/node-conversion.d.ts +32 -0
  41. package/dist/v5/node-feature-schema.d.ts +3 -0
  42. package/dist/v5/node-i3s-source.d.ts +41 -0
  43. package/dist/v5/node-source.d.ts +47 -0
  44. package/dist/v5/node.cjs +900 -0
  45. package/dist/v5/node.d.ts +6 -0
  46. package/dist/v5/node.js +877 -0
  47. package/dist/v5/point-cloud-file.d.ts +83 -0
  48. package/dist/v5/point-cloud-potree.d.ts +47 -0
  49. package/dist/v5/point-cloud-source-encoder.d.ts +4 -7
  50. package/dist/v5/point-cloud-spatial.d.ts +22 -0
  51. package/dist/v5/point-cloud-tileset-sink.d.ts +28 -0
  52. package/dist/v5/point-cloud-tileset.d.ts +44 -0
  53. package/dist/v5/point-cloud.d.ts +26 -1
  54. package/dist/v5/resource-fetcher.d.ts +18 -0
  55. package/dist/v5/resource-manifest.d.ts +10 -1
  56. package/dist/v5/selected-mesh-source.d.ts +53 -0
  57. package/dist/v5/spatial-conversion.d.ts +3 -1
  58. package/package.json +33 -25
  59. package/src/index.ts +4 -1
  60. package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +2 -4
  61. package/src/v4/converter-cli.ts +8 -2
  62. package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +3 -51
  63. package/src/v4/i3s-converter/helpers/node-index-document.ts +5 -2
  64. package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +1 -1
  65. package/src/v4/i3s-converter/json-templates/store.ts +2 -1
  66. package/src/v5/adapters.ts +69 -2
  67. package/src/v5/archive-source.ts +235 -0
  68. package/src/v5/browser-tileset-source.ts +418 -48
  69. package/src/v5/browser.ts +81 -2
  70. package/src/v5/conversion-api.ts +69 -9
  71. package/src/v5/converter-cli.ts +28 -0
  72. package/src/v5/core.ts +6 -0
  73. package/src/v5/i3s-mesh-conversion.ts +97 -19
  74. package/src/v5/i3s-mesh-sink.ts +351 -0
  75. package/src/v5/i3s-mesh-source-features.ts +279 -0
  76. package/src/v5/i3s-mesh-source-material.ts +157 -0
  77. package/src/v5/i3s-mesh-source.ts +228 -0
  78. package/src/v5/i3s-selection-source.ts +298 -0
  79. package/src/v5/index.ts +60 -2
  80. package/src/v5/mesh-conversion.ts +61 -11
  81. package/src/v5/mesh-draco.ts +47 -3
  82. package/src/v5/mesh-features.ts +319 -0
  83. package/src/v5/mesh-hierarchy.ts +648 -0
  84. package/src/v5/mesh-source-features.ts +73 -10
  85. package/src/v5/mesh-source-schema.ts +134 -0
  86. package/src/v5/mesh-source-texture.ts +47 -0
  87. package/src/v5/mesh-source.ts +99 -25
  88. package/src/v5/mesh-tileset-sink.ts +1 -1
  89. package/src/v5/mesh.ts +16 -7
  90. package/src/v5/node-cli.ts +197 -0
  91. package/src/v5/node-conversion.ts +345 -0
  92. package/src/v5/node-feature-schema.ts +155 -0
  93. package/src/v5/node-i3s-source.ts +181 -0
  94. package/src/v5/node-source.ts +328 -0
  95. package/src/v5/node.ts +16 -0
  96. package/src/v5/point-cloud-conversion.ts +9 -1
  97. package/src/v5/point-cloud-file.ts +306 -0
  98. package/src/v5/point-cloud-potree.ts +130 -0
  99. package/src/v5/point-cloud-source-encoder.ts +31 -20
  100. package/src/v5/point-cloud-spatial.ts +280 -0
  101. package/src/v5/point-cloud-tileset-sink.ts +234 -0
  102. package/src/v5/point-cloud-tileset.ts +137 -0
  103. package/src/v5/point-cloud.ts +159 -32
  104. package/src/v5/resource-fetcher.ts +113 -0
  105. package/src/v5/resource-manifest.ts +40 -5
  106. package/src/v5/selected-mesh-source.ts +384 -0
  107. package/src/v5/spatial-conversion.ts +161 -6
@@ -0,0 +1,181 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {realpath} from 'node:fs/promises';
6
+ import {fileURLToPath} from 'node:url';
7
+ import {resolve} from 'node:path';
8
+ import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
9
+ import {
10
+ createSelectedI3SMeshConversionSource,
11
+ inspectI3SMeshConversionSource,
12
+ createTileConversionArchiveTransport,
13
+ openTileConversionArchive
14
+ } from '@loaders.gl/tile-converter/v5/adapters';
15
+ import type {
16
+ I3SMeshSelectionOptions,
17
+ I3SMeshSourceFeatureOptions
18
+ } from '@loaders.gl/tile-converter/v5/adapters';
19
+ import {createTileConversionResourceFetcher} from './resource-fetcher.js';
20
+ import {NODE_TILESET_LIMITS, openLocalFile} from './node-source.js';
21
+ import type {NodeTilesetConversionSourceOptions} from './node-source.js';
22
+
23
+ /** Bounded I3S layer URL or indexed SLPK input with application-owned transport. */
24
+ export interface NodeI3SConversionSourceOptions
25
+ extends Omit<NodeTilesetConversionSourceOptions, 'format' | 'inspectNestedTilesets'> {
26
+ /** I3S HTTP(S) layer or local/HTTP(S) SLPK archive. */
27
+ readonly format: 'i3s' | 'slpk';
28
+ }
29
+
30
+ /** Opens one lifetime-budgeted I3S source; its owner must close it in finally. */
31
+ export async function createNodeI3SConversionSource(options: NodeI3SConversionSourceOptions) {
32
+ const maxInputBytes = options.maxInputBytes ?? NODE_TILESET_LIMITS.maxInputBytes;
33
+ const maxInputResources = options.maxInputResources ?? NODE_TILESET_LIMITS.maxInputResources;
34
+ if (
35
+ !['i3s', 'slpk'].includes(options.format) ||
36
+ !Number.isSafeInteger(maxInputBytes) ||
37
+ maxInputBytes < 1 ||
38
+ maxInputBytes > NODE_TILESET_LIMITS.maxInputBytes ||
39
+ !Number.isSafeInteger(maxInputResources) ||
40
+ maxInputResources < 1 ||
41
+ maxInputResources > NODE_TILESET_LIMITS.maxInputResources
42
+ )
43
+ throw new TileConversionError(
44
+ 'INVALID_NODE_SOURCE_OPTIONS',
45
+ 'I3S sources require positive budgets within the bounded input profile'
46
+ );
47
+ const controller = new AbortController();
48
+ const signal = options.signal
49
+ ? AbortSignal.any([options.signal, controller.signal])
50
+ : controller.signal;
51
+ signal.throwIfAborted();
52
+ let release: (() => Promise<void>) | undefined;
53
+ let closePromise: Promise<void> | undefined;
54
+ /** Cancels cached/runtime work and releases owned indexed readers exactly once. */
55
+ const close = async (): Promise<void> => {
56
+ controller.abort(new TileConversionError('NODE_SOURCE_CLOSED', 'Node source is closed'));
57
+ closePromise ??= release?.() ?? Promise.resolve();
58
+ await closePromise;
59
+ };
60
+ try {
61
+ const remote = /^https?:/i.test(options.input);
62
+ if (options.format === 'i3s' && !remote)
63
+ throw new TileConversionError(
64
+ 'UNSUPPORTED_NODE_SOURCE',
65
+ 'I3S input requires an HTTP(S) layer URL; use slpk for a local archive'
66
+ );
67
+ let rootUrl: string;
68
+ let transport: typeof fetch;
69
+ if (options.format === 'slpk') {
70
+ const archive = remote
71
+ ? await openTileConversionArchive(
72
+ {input: options.input, format: 'slpk'},
73
+ maxInputBytes,
74
+ signal,
75
+ options.fetcher
76
+ )
77
+ : await createTileConversionArchiveTransport(
78
+ await openLocalFile(
79
+ await realpath(
80
+ /^file:/i.test(options.input)
81
+ ? fileURLToPath(options.input)
82
+ : resolve(options.input)
83
+ ),
84
+ maxInputBytes,
85
+ signal,
86
+ () => {}
87
+ ),
88
+ 'slpk',
89
+ maxInputBytes,
90
+ signal
91
+ );
92
+ release = archive.close;
93
+ rootUrl = archive.rootUrl;
94
+ transport = archive.fetcher;
95
+ } else {
96
+ rootUrl = new URL(options.input).href;
97
+ transport = options.fetcher ?? fetch;
98
+ }
99
+ const bounded = createTileConversionResourceFetcher({
100
+ maxInputBytes,
101
+ signal,
102
+ fetcher: transport
103
+ });
104
+ let metadataBytes = 0;
105
+ const root = new URL(rootUrl);
106
+ /** Inherits query credentials only within the selected layer's origin and path. */
107
+ const fetcher: typeof fetch = async (input, requestOptions) => {
108
+ const url = new URL(input instanceof Request ? input.url : String(input));
109
+ if (
110
+ url.origin === root.origin &&
111
+ (url.pathname === root.pathname || url.pathname.startsWith(`${root.pathname}/`))
112
+ )
113
+ for (const [key, value] of root.searchParams)
114
+ if (!url.searchParams.has(key)) url.searchParams.set(key, value);
115
+ const response = await bounded(
116
+ input instanceof Request ? new Request(url, input) : url,
117
+ requestOptions
118
+ );
119
+ if (response.url && response.url !== url.href)
120
+ throw new TileConversionError(
121
+ 'I3S_REDIRECT_UNSUPPORTED',
122
+ 'Use the final I3S resource URL; redirected resource bases are not qualified'
123
+ );
124
+ const bytes = await response.arrayBuffer();
125
+ metadataBytes += bytes.byteLength;
126
+ const result = new Response(bytes, {status: response.status, headers: response.headers});
127
+ Object.defineProperty(result, 'url', {value: response.url});
128
+ return result;
129
+ };
130
+ // Metadata inspection needs no decoder transport initialization. Conversion injects bundled assets.
131
+ const selectionOptions: I3SMeshSelectionOptions = {
132
+ rootUrl,
133
+ fetcher,
134
+ signal,
135
+ maxInputBytes,
136
+ maxInputResources,
137
+ dracoLibraryOptions: {decoderProfile: 'full' as const}
138
+ };
139
+ return {
140
+ close,
141
+ /** Reads only layer and node metadata, under the same lifetime-wide budget as conversion. */
142
+ async inspect(operationSignal?: AbortSignal) {
143
+ signal.throwIfAborted();
144
+ operationSignal?.throwIfAborted();
145
+ const activeSignal = operationSignal ? AbortSignal.any([signal, operationSignal]) : signal;
146
+ /** Forwards borrowed operation cancellation without closing the owning source. */
147
+ const operationFetcher: typeof fetch = (input, requestOptions) =>
148
+ fetcher(input, {
149
+ ...requestOptions,
150
+ signal: requestOptions?.signal
151
+ ? AbortSignal.any([activeSignal, requestOptions.signal])
152
+ : activeSignal
153
+ });
154
+ const inspection = await inspectI3SMeshConversionSource({
155
+ ...selectionOptions,
156
+ fetcher: operationFetcher,
157
+ signal: activeSignal
158
+ });
159
+ return {...inspection, metadataBytes};
160
+ },
161
+ /** Creates an original-coordinate selected mesh source using application-provided Draco assets. */
162
+ select: (
163
+ inspection: Awaited<ReturnType<typeof inspectI3SMeshConversionSource>>,
164
+ resourceIds: readonly string[],
165
+ dracoLibraryOptions: I3SMeshSelectionOptions['dracoLibraryOptions'],
166
+ features?: I3SMeshSourceFeatureOptions
167
+ ) => {
168
+ signal.throwIfAborted();
169
+ return createSelectedI3SMeshConversionSource(inspection, {
170
+ ...selectionOptions,
171
+ resourceIds,
172
+ dracoLibraryOptions,
173
+ features
174
+ });
175
+ }
176
+ };
177
+ } catch (error) {
178
+ await close();
179
+ throw error;
180
+ }
181
+ }
@@ -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/loader-utils';
9
+ import {parse3DTilesArchive} from '@loaders.gl/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';
@@ -11,6 +11,7 @@ import type {
11
11
  } from '@loaders.gl/tile-converter/v5/core';
12
12
  import {traversePointCloudSource} from '@loaders.gl/tile-converter/v5/core';
13
13
  import type {PointCloudSourceTile} from '@loaders.gl/tile-converter/v5/core';
14
+ import {transformPointCloudSourceTile} from './point-cloud-spatial.js';
14
15
  import {encodePointCloudSourceTile} from './point-cloud-source-encoder.js';
15
16
  import type {
16
17
  EncodedPointCloudSourceTile,
@@ -59,7 +60,14 @@ export function convertPointCloudSource(
59
60
  traversePointCloudSource(source, {maxDepth: options.maxDepth, signal})
60
61
  },
61
62
  codec: {
62
- async *convert(sourceTile) {
63
+ async *convert(inputTile, _inspection, signal) {
64
+ const sourceTile = options.spatialContext
65
+ ? await transformPointCloudSourceTile(inputTile, {
66
+ ...options,
67
+ spatialContext: options.spatialContext,
68
+ signal
69
+ })
70
+ : inputTile;
63
71
  const encodingOptions = sourceTile.content
64
72
  ? options.getTileEncodingOptions?.(sourceTile)
65
73
  : undefined;