@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.
- package/README.md +211 -2
- package/bin/converter-v5.js +2 -0
- package/dist/converter.min.cjs +162 -163
- package/dist/i3s-server/bin/i3s-server.min.cjs +73 -73
- package/dist/index.cjs +1933 -2232
- package/dist/index.js +1925 -2235
- package/dist/v5/adapters.cjs +3186 -208
- package/dist/v5/adapters.d.ts +29 -2
- package/dist/v5/adapters.js +3202 -196
- package/dist/v5/archive-source.d.ts +36 -0
- package/dist/v5/browser-tileset-source.d.ts +55 -11
- package/dist/v5/browser.cjs +30 -0
- package/dist/v5/browser.d.ts +26 -2
- package/dist/v5/browser.js +45 -1
- package/dist/v5/conversion-api.d.ts +9 -0
- package/dist/v5/converter-cli.d.ts +1 -0
- package/dist/v5/converter-cli.js +1214 -0
- package/dist/v5/core.cjs +515 -50
- package/dist/v5/core.d.ts +2 -0
- package/dist/v5/core.js +515 -50
- package/dist/v5/i3s-mesh-conversion.d.ts +14 -4
- package/dist/v5/i3s-mesh-sink.d.ts +25 -0
- package/dist/v5/i3s-mesh-source-features.d.ts +13 -0
- package/dist/v5/i3s-mesh-source-material.d.ts +5 -0
- package/dist/v5/i3s-mesh-source.d.ts +30 -0
- package/dist/v5/i3s-selection-source.d.ts +70 -0
- package/dist/v5/index.cjs +20 -0
- package/dist/v5/index.d.ts +18 -2
- package/dist/v5/index.js +31 -1
- package/dist/v5/mesh-conversion.d.ts +29 -2
- package/dist/v5/mesh-features.d.ts +9 -0
- package/dist/v5/mesh-hierarchy.d.ts +47 -0
- package/dist/v5/mesh-source-features.d.ts +2 -1
- package/dist/v5/mesh-source-schema.d.ts +12 -0
- package/dist/v5/mesh-source-texture.d.ts +2 -0
- package/dist/v5/mesh-source.d.ts +13 -3
- package/dist/v5/mesh-tileset-sink.d.ts +5 -0
- package/dist/v5/mesh.d.ts +6 -1
- package/dist/v5/node-cli.d.ts +9 -0
- package/dist/v5/node-conversion.d.ts +32 -0
- package/dist/v5/node-feature-schema.d.ts +3 -0
- package/dist/v5/node-i3s-source.d.ts +41 -0
- package/dist/v5/node-source.d.ts +47 -0
- package/dist/v5/node.cjs +900 -0
- package/dist/v5/node.d.ts +6 -0
- package/dist/v5/node.js +877 -0
- package/dist/v5/point-cloud-file.d.ts +83 -0
- package/dist/v5/point-cloud-potree.d.ts +47 -0
- package/dist/v5/point-cloud-source-encoder.d.ts +4 -7
- package/dist/v5/point-cloud-spatial.d.ts +22 -0
- package/dist/v5/point-cloud-tileset-sink.d.ts +28 -0
- package/dist/v5/point-cloud-tileset.d.ts +44 -0
- package/dist/v5/point-cloud.d.ts +26 -1
- package/dist/v5/resource-fetcher.d.ts +18 -0
- package/dist/v5/resource-manifest.d.ts +10 -1
- package/dist/v5/selected-mesh-source.d.ts +53 -0
- package/dist/v5/spatial-conversion.d.ts +3 -1
- package/package.json +33 -25
- package/src/index.ts +4 -1
- package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +2 -4
- package/src/v4/converter-cli.ts +8 -2
- package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +3 -51
- package/src/v4/i3s-converter/helpers/node-index-document.ts +5 -2
- package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +1 -1
- package/src/v4/i3s-converter/json-templates/store.ts +2 -1
- package/src/v5/adapters.ts +69 -2
- package/src/v5/archive-source.ts +235 -0
- package/src/v5/browser-tileset-source.ts +418 -48
- package/src/v5/browser.ts +81 -2
- package/src/v5/conversion-api.ts +69 -9
- package/src/v5/converter-cli.ts +28 -0
- package/src/v5/core.ts +6 -0
- package/src/v5/i3s-mesh-conversion.ts +97 -19
- package/src/v5/i3s-mesh-sink.ts +351 -0
- package/src/v5/i3s-mesh-source-features.ts +279 -0
- package/src/v5/i3s-mesh-source-material.ts +157 -0
- package/src/v5/i3s-mesh-source.ts +228 -0
- package/src/v5/i3s-selection-source.ts +298 -0
- package/src/v5/index.ts +60 -2
- package/src/v5/mesh-conversion.ts +61 -11
- package/src/v5/mesh-draco.ts +47 -3
- package/src/v5/mesh-features.ts +319 -0
- package/src/v5/mesh-hierarchy.ts +648 -0
- package/src/v5/mesh-source-features.ts +73 -10
- package/src/v5/mesh-source-schema.ts +134 -0
- package/src/v5/mesh-source-texture.ts +47 -0
- package/src/v5/mesh-source.ts +99 -25
- package/src/v5/mesh-tileset-sink.ts +1 -1
- package/src/v5/mesh.ts +16 -7
- package/src/v5/node-cli.ts +197 -0
- package/src/v5/node-conversion.ts +345 -0
- package/src/v5/node-feature-schema.ts +155 -0
- package/src/v5/node-i3s-source.ts +181 -0
- package/src/v5/node-source.ts +328 -0
- package/src/v5/node.ts +16 -0
- package/src/v5/point-cloud-conversion.ts +9 -1
- package/src/v5/point-cloud-file.ts +306 -0
- package/src/v5/point-cloud-potree.ts +130 -0
- package/src/v5/point-cloud-source-encoder.ts +31 -20
- package/src/v5/point-cloud-spatial.ts +280 -0
- package/src/v5/point-cloud-tileset-sink.ts +234 -0
- package/src/v5/point-cloud-tileset.ts +137 -0
- package/src/v5/point-cloud.ts +159 -32
- package/src/v5/resource-fetcher.ts +113 -0
- package/src/v5/resource-manifest.ts +40 -5
- package/src/v5/selected-mesh-source.ts +384 -0
- 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(
|
|
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;
|