@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.
- package/LICENSE +1 -1
- package/README.md +180 -4
- package/bin/converter-v5.js +2 -0
- package/dist/converter.min.cjs +172 -182
- package/dist/i3s-server/bin/i3s-server.min.cjs +113 -158
- package/dist/index.cjs +18832 -19228
- package/dist/index.js +19566 -19968
- package/dist/v5/adapters.cjs +2002 -230
- package/dist/v5/adapters.d.ts +20 -0
- package/dist/v5/adapters.js +2005 -214
- package/dist/v5/archive-source.d.ts +36 -0
- package/dist/v5/browser-tileset-source.d.ts +55 -11
- package/dist/v5/browser.cjs +18 -0
- package/dist/v5/browser.d.ts +15 -1
- package/dist/v5/browser.js +26 -0
- package/dist/v5/conversion-api.d.ts +34 -0
- package/dist/v5/converter-cli.d.ts +1 -0
- package/dist/v5/converter-cli.js +1230 -0
- package/dist/v5/core.cjs +547 -77
- package/dist/v5/core.d.ts +3 -1
- package/dist/v5/core.js +540 -70
- package/dist/v5/feature-arrow.d.ts +1 -1
- package/dist/v5/i3s-mesh-conversion.d.ts +5 -1
- package/dist/v5/i3s-mesh-source-features.d.ts +2 -2
- package/dist/v5/i3s-mesh-source-material.d.ts +1 -1
- package/dist/v5/i3s-mesh-source.d.ts +2 -2
- package/dist/v5/i3s-selection-source.d.ts +70 -0
- package/dist/v5/index.cjs +15 -0
- package/dist/v5/index.d.ts +13 -1
- package/dist/v5/index.js +22 -0
- package/dist/v5/mesh-conversion.d.ts +21 -3
- package/dist/v5/mesh-draco.d.ts +2 -2
- package/dist/v5/mesh-features.d.ts +2 -2
- package/dist/v5/mesh-hierarchy.d.ts +47 -0
- package/dist/v5/mesh-source-features.d.ts +6 -5
- package/dist/v5/mesh-source-schema.d.ts +12 -0
- package/dist/v5/mesh-source-texture.d.ts +3 -3
- package/dist/v5/mesh-source.d.ts +10 -7
- package/dist/v5/mesh-tileset-sink.d.ts +5 -0
- package/dist/v5/mesh.d.ts +1 -1
- package/dist/v5/node-cli.d.ts +9 -0
- package/dist/v5/node-conversion.d.ts +34 -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 +913 -0
- package/dist/v5/node.d.ts +6 -0
- package/dist/v5/node.js +890 -0
- package/dist/v5/point-cloud-attributes.d.ts +19 -0
- package/dist/v5/point-cloud-conversion.d.ts +1 -1
- package/dist/v5/point-cloud-copc.d.ts +44 -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 +1 -1
- package/dist/v5/point-cloud-source.d.ts +1 -1
- package/dist/v5/point-cloud-tileset.d.ts +44 -0
- package/dist/v5/point-cloud.d.ts +1 -1
- 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 +1 -1
- package/dist/v5/tileset-source.d.ts +1 -1
- package/package.json +25 -32
- package/src/index.ts +4 -1
- package/src/v4/3d-tiles-converter/3d-tiles-converter.ts +11 -12
- package/src/v4/3d-tiles-converter/helpers/3d-tiles-content-converter.ts +7 -3
- package/src/v4/3d-tiles-converter/helpers/i3s-obb-to-3d-tiles-obb.ts +2 -2
- package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +4 -6
- package/src/v4/3d-tiles-converter/helpers/texture-atlas.ts +1 -1
- package/src/v4/3d-tiles-converter/helpers/validate-i3s-spatial-reference.ts +2 -2
- package/src/v4/3d-tiles-converter/json-templates/tileset.ts +58 -39
- package/src/v4/converter-cli.ts +10 -4
- package/src/v4/deps-installer/deps-installer.ts +3 -3
- package/src/v4/i3s-converter/helpers/attribute-metadata-info.ts +1 -1
- package/src/v4/i3s-converter/helpers/batch-ids-extensions.ts +9 -10
- package/src/v4/i3s-converter/helpers/conversion-gltf.ts +108 -0
- package/src/v4/i3s-converter/helpers/coordinate-converter.ts +6 -5
- package/src/v4/i3s-converter/helpers/create-scene-server-path.ts +5 -6
- package/src/v4/i3s-converter/helpers/feature-attributes.ts +4 -4
- package/src/v4/i3s-converter/helpers/geometry-attributes.ts +1 -1
- package/src/v4/i3s-converter/helpers/geometry-converter.ts +69 -77
- package/src/v4/i3s-converter/helpers/gltf-attributes.ts +58 -74
- package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +6 -54
- package/src/v4/i3s-converter/helpers/node-debug.ts +7 -3
- package/src/v4/i3s-converter/helpers/node-index-document.ts +11 -9
- package/src/v4/i3s-converter/helpers/node-pages.ts +3 -4
- package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +2 -2
- package/src/v4/i3s-converter/helpers/tileset-traversal.ts +1 -1
- package/src/v4/i3s-converter/i3s-converter.ts +29 -31
- package/src/v4/i3s-converter/json-templates/geometry-definitions.ts +71 -66
- package/src/v4/i3s-converter/json-templates/layers.ts +97 -132
- package/src/v4/i3s-converter/json-templates/metadata.ts +31 -21
- package/src/v4/i3s-converter/json-templates/node.ts +60 -74
- package/src/v4/i3s-converter/json-templates/scene-server.ts +36 -27
- package/src/v4/i3s-converter/json-templates/shared-resources.ts +62 -123
- package/src/v4/i3s-converter/json-templates/store.ts +39 -57
- package/src/v4/i3s-converter/types.ts +4 -4
- package/src/v4/i3s-server/controllers/slpk-controller.ts +3 -3
- package/src/v4/i3s-server/utils/create-scene-server.ts +3 -3
- package/src/v4/lib/utils/builder-input.ts +8 -0
- package/src/v4/lib/utils/conversion-dump.ts +5 -1
- package/src/v4/lib/utils/file-utils.ts +1 -1
- package/src/v4/lib/utils/hash-string.ts +21 -0
- package/src/v4/lib/utils/lod-conversion-utils.ts +3 -3
- package/src/v4/pgm-loader.ts +2 -2
- package/src/v5/adapters.ts +53 -0
- package/src/v5/archive-source.ts +235 -0
- package/src/v5/browser-tileset-source.ts +418 -48
- package/src/v5/browser.ts +46 -0
- package/src/v5/conversion-api.ts +184 -17
- package/src/v5/conversion-archive.ts +2 -2
- package/src/v5/converter-cli.ts +28 -0
- package/src/v5/core.ts +7 -0
- package/src/v5/feature-arrow.ts +2 -2
- package/src/v5/i3s-mesh-conversion.ts +41 -3
- package/src/v5/i3s-mesh-sink.ts +1 -1
- package/src/v5/i3s-mesh-source-features.ts +3 -3
- package/src/v5/i3s-mesh-source-material.ts +18 -11
- package/src/v5/i3s-mesh-source.ts +3 -2
- package/src/v5/i3s-selection-source.ts +298 -0
- package/src/v5/index.ts +41 -0
- package/src/v5/mesh-conversion.ts +24 -4
- package/src/v5/mesh-draco.ts +2 -2
- package/src/v5/mesh-features.ts +3 -3
- package/src/v5/mesh-hierarchy.ts +648 -0
- package/src/v5/mesh-source-features.ts +165 -51
- package/src/v5/mesh-source-schema.ts +140 -0
- package/src/v5/mesh-source-texture.ts +49 -19
- package/src/v5/mesh-source.ts +80 -49
- package/src/v5/mesh-tileset-archive.ts +1 -1
- package/src/v5/mesh-tileset-sink.ts +2 -2
- package/src/v5/mesh.ts +1 -1
- package/src/v5/node-cli.ts +201 -0
- package/src/v5/node-conversion.ts +362 -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-attributes.ts +113 -0
- package/src/v5/point-cloud-conversion.ts +1 -1
- package/src/v5/point-cloud-copc.ts +289 -0
- package/src/v5/point-cloud-file.ts +306 -0
- package/src/v5/point-cloud-potree.ts +134 -0
- package/src/v5/point-cloud-source-encoder.ts +1 -1
- package/src/v5/point-cloud-source.ts +1 -1
- package/src/v5/point-cloud-spatial.ts +2 -2
- package/src/v5/point-cloud-tileset.ts +137 -0
- package/src/v5/point-cloud.ts +3 -3
- package/src/v5/resource-fetcher.ts +4 -1
- package/src/v5/resource-manifest.ts +40 -5
- package/src/v5/selected-mesh-source.ts +384 -0
- package/src/v5/spatial-conversion.ts +90 -19
- 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,
|