@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,235 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {BlobFile, HttpFile} from '@loaders.gl/loader-utils';
6
+ import type {HttpFileIdentity, ReadableFile} from '@loaders.gl/loader-utils';
7
+ import {parseSLPKArchive} from '@loaders.gl/i3s';
8
+ import {parse3DTilesArchive} from '@loaders.gl/3d-tiles';
9
+ import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
10
+ import {createTileConversionResourceFetcher} from './resource-fetcher.js';
11
+
12
+ /** Cloneable archive input; a remote identity is retained across inspection and conversion. */
13
+ export interface TileConversionArchiveInput {
14
+ /** Immutable local Blob or HTTP(S) archive URL, including any query credentials. */
15
+ readonly input: Blob | string;
16
+ /** Explicit archive format, including for URLs without a filename extension. */
17
+ readonly format: 'slpk' | '3tz';
18
+ /** Pinned remote length and validator; local File contents are immutable. */
19
+ readonly identity?: HttpFileIdentity;
20
+ }
21
+
22
+ /** Opens an indexed archive with bounded reads and confines dependency URLs to its virtual root. */
23
+ export async function openTileConversionArchive(
24
+ descriptor: TileConversionArchiveInput,
25
+ maxInputBytes: number,
26
+ signal: AbortSignal,
27
+ fetcher: typeof fetch = fetch
28
+ ) {
29
+ signal.throwIfAborted();
30
+ if (!['slpk', '3tz'].includes(descriptor.format))
31
+ throw new TileConversionError('INVALID_ARCHIVE_SOURCE', 'Select SLPK or 3TZ archive input');
32
+ validateArchiveEntityTag(descriptor.identity?.etag);
33
+ const transport = createTileConversionResourceFetcher({
34
+ maxInputBytes,
35
+ signal,
36
+ fetcher: async (url, options) => {
37
+ const response = await fetcher(url, options);
38
+ if (response.status !== 206) {
39
+ await response.body?.cancel();
40
+ throw new Error('Remote conversion archives require HTTP 206 byte-range responses.');
41
+ }
42
+ try {
43
+ validateArchiveResponseValidator(response.headers);
44
+ } catch (error) {
45
+ await response.body?.cancel(error);
46
+ throw error;
47
+ }
48
+ return response;
49
+ }
50
+ });
51
+ let file: ReadableFile | undefined;
52
+ let closePromise: Promise<void> | undefined;
53
+ /** Shares transport release between initialization failure and the owned archive. */
54
+ async function closeFile(): Promise<void> {
55
+ if (file) {
56
+ closePromise ??= file.close();
57
+ await closePromise;
58
+ }
59
+ }
60
+ try {
61
+ if (typeof descriptor.input === 'string') {
62
+ if (!['http:', 'https:'].includes(new URL(descriptor.input).protocol))
63
+ throw new Error('Use an HTTP(S) archive URL.');
64
+ const remote = new HttpFile(descriptor.input, {
65
+ fetch: (url, options) => transport(url, options),
66
+ consistency: 'strict',
67
+ ...descriptor.identity
68
+ });
69
+ file = remote;
70
+ await remote.open(signal);
71
+ } else file = new BlobFile(descriptor.input);
72
+ const ownedFile = file;
73
+ file = undefined;
74
+ return await createTileConversionArchiveTransport(
75
+ ownedFile,
76
+ descriptor.format,
77
+ maxInputBytes,
78
+ signal,
79
+ descriptor
80
+ );
81
+ } catch (error) {
82
+ await closeFile();
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ /** Owns an indexed readable file and confines entry/dependency requests to its virtual root. */
88
+ export async function createTileConversionArchiveTransport(
89
+ file: ReadableFile,
90
+ format: 'slpk' | '3tz',
91
+ maxInputBytes: number,
92
+ signal: AbortSignal,
93
+ descriptor?: TileConversionArchiveInput
94
+ ) {
95
+ const rootUrl = `https://conversion.invalid/input.${format}`;
96
+ let closed = false;
97
+ let releasePromise: Promise<void> | undefined;
98
+ let closePromise: Promise<void> | undefined;
99
+ /** Invalidates cached reads as well as physical reads, and releases the file exactly once. */
100
+ const closeFile = (): Promise<void> => {
101
+ closed = true;
102
+ closePromise ??= file.close();
103
+ return closePromise;
104
+ };
105
+ /** Checks operation cancellation and the owned archive lifetime before cached or physical reads. */
106
+ const checkOpen = (): void => {
107
+ signal.throwIfAborted();
108
+ if (closed)
109
+ throw new TileConversionError('ARCHIVE_SOURCE_CLOSED', 'Conversion archive is closed');
110
+ };
111
+ try {
112
+ checkOpen();
113
+ if (
114
+ !['slpk', '3tz'].includes(format) ||
115
+ !Number.isSafeInteger(maxInputBytes) ||
116
+ maxInputBytes < 1
117
+ )
118
+ throw new TileConversionError(
119
+ 'INVALID_ARCHIVE_SOURCE',
120
+ 'Archive source requires a supported format and positive safe byte budget'
121
+ );
122
+ if (!Number.isSafeInteger(file.size) || file.size < 0 || file.size > maxInputBytes)
123
+ throw new Error('Conversion archive input must be at most 16 MiB.');
124
+ let readBytes = 0;
125
+ const boundedFile: ReadableFile = {
126
+ handle: file.handle,
127
+ size: file.size,
128
+ bigsize: file.bigsize,
129
+ url: file.url,
130
+ ...(file.stat ? {stat: () => file.stat!()} : {}),
131
+ close: closeFile,
132
+ /** Checks indexed reads before allocation and forwards operation cancellation. */
133
+ read: async (start, length) => {
134
+ checkOpen();
135
+ if (!Number.isSafeInteger(length) || length! < 0 || length! > maxInputBytes - readBytes)
136
+ throw new TileConversionError(
137
+ 'INPUT_RESOURCE_TOO_LARGE',
138
+ 'Archive indexed reads exceed the input budget'
139
+ );
140
+ readBytes += length!;
141
+ const bytes = await file!.read(start, length, signal);
142
+ checkOpen();
143
+ return bytes;
144
+ }
145
+ };
146
+ const archive =
147
+ format === 'slpk'
148
+ ? await parseSLPKArchive(boundedFile)
149
+ : await parse3DTilesArchive(boundedFile);
150
+ const root = new URL(rootUrl);
151
+ return {
152
+ rootUrl: format === '3tz' ? `${rootUrl}/tileset.json` : rootUrl,
153
+ descriptor: descriptor
154
+ ? {
155
+ ...descriptor,
156
+ ...(file instanceof HttpFile ? {identity: file.getIdentitySnapshot()!} : {})
157
+ }
158
+ : undefined,
159
+ /** Resolves only resources belonging to this archive, without external network fallback. */
160
+ fetcher: (async (input, options) => {
161
+ checkOpen();
162
+ options?.signal?.throwIfAborted();
163
+ const url = new URL(input instanceof Request ? input.url : String(input));
164
+ if (
165
+ url.origin !== root.origin ||
166
+ (url.pathname !== root.pathname && !url.pathname.startsWith(`${root.pathname}/`))
167
+ )
168
+ throw new Error('Conversion dependencies must resolve inside the selected archive.');
169
+ const path = decodeURIComponent(
170
+ url.pathname.slice(root.pathname.length).replace(/^\//, '')
171
+ );
172
+ if (
173
+ path.includes('\\') ||
174
+ path.split('/').some(segment => segment === '..' || segment === '.')
175
+ )
176
+ throw new Error('Archive dependency path cannot escape its root.');
177
+ const bytes =
178
+ format === 'slpk' ? await archive.getFile(path, 'http') : await archive.getFile(path);
179
+ checkOpen();
180
+ options?.signal?.throwIfAborted();
181
+ return new Response(bytes);
182
+ }) satisfies typeof fetch,
183
+ /** Releases the readable file on success, failure or cancellation. */
184
+ close: async () => {
185
+ closed = true;
186
+ releasePromise ??= archive.close();
187
+ await releasePromise;
188
+ }
189
+ };
190
+ } catch (error) {
191
+ await closeFile();
192
+ throw error;
193
+ }
194
+ }
195
+
196
+ /** Rejects weak/malformed entity tags rather than downgrading the archive identity contract. */
197
+ function validateArchiveEntityTag(entityTag: string | null | undefined): void {
198
+ if (
199
+ entityTag !== undefined &&
200
+ entityTag !== null &&
201
+ !/^"[\x21\x23-\x7e\x80-\xff]*"$/.test(entityTag)
202
+ )
203
+ throw new TileConversionError(
204
+ 'ARCHIVE_VALIDATOR_UNSUPPORTED',
205
+ 'Remote archives require a strong quoted ETag, or Last-Modified without an ETag; weak/malformed ETags are unsupported'
206
+ );
207
+ }
208
+
209
+ /** Qualifies Last-Modified fallback conservatively using server dates at least 60 seconds apart. */
210
+ function validateArchiveResponseValidator(headers: Headers): void {
211
+ const entityTag = headers.get('etag');
212
+ validateArchiveEntityTag(entityTag);
213
+ if (entityTag !== null) return;
214
+ const lastModified = headers.get('last-modified');
215
+ const responseDate = headers.get('date');
216
+ const modifiedTime = parseArchiveHttpDate(lastModified);
217
+ const responseTime = parseArchiveHttpDate(responseDate);
218
+ if (
219
+ !Number.isFinite(modifiedTime) ||
220
+ !Number.isFinite(responseTime) ||
221
+ responseTime - modifiedTime < 60_000
222
+ )
223
+ throw new TileConversionError(
224
+ 'ARCHIVE_VALIDATOR_UNSUPPORTED',
225
+ 'Last-Modified archive identity requires a valid Date at least 60 seconds later; otherwise supply a strong ETag'
226
+ );
227
+ }
228
+
229
+ /** Accepts canonical HTTP server dates without Date.parse's permissive non-HTTP extensions. */
230
+ function parseArchiveHttpDate(value: string | null): number {
231
+ const milliseconds = value === null ? NaN : Date.parse(value);
232
+ return Number.isFinite(milliseconds) && new Date(milliseconds).toUTCString() === value
233
+ ? milliseconds
234
+ : NaN;
235
+ }