@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.
- package/README.md +178 -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 +1749 -2152
- package/dist/index.js +1748 -2162
- package/dist/v5/adapters.cjs +1550 -128
- package/dist/v5/adapters.d.ts +16 -0
- package/dist/v5/adapters.js +1558 -117
- 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 +14 -0
- package/dist/v5/browser.js +26 -0
- 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 +471 -60
- package/dist/v5/core.d.ts +2 -0
- package/dist/v5/core.js +471 -60
- package/dist/v5/i3s-mesh-conversion.d.ts +4 -0
- package/dist/v5/i3s-selection-source.d.ts +70 -0
- package/dist/v5/index.cjs +10 -0
- package/dist/v5/index.d.ts +8 -0
- package/dist/v5/index.js +14 -0
- package/dist/v5/mesh-conversion.d.ts +18 -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.d.ts +5 -2
- package/dist/v5/mesh-tileset-sink.d.ts +5 -0
- 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-tileset.d.ts +44 -0
- package/dist/v5/resource-manifest.d.ts +10 -1
- package/dist/v5/selected-mesh-source.d.ts +53 -0
- 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 +48 -0
- package/src/v5/archive-source.ts +235 -0
- package/src/v5/browser-tileset-source.ts +418 -48
- package/src/v5/browser.ts +45 -0
- 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 +40 -2
- package/src/v5/i3s-mesh-sink.ts +1 -1
- package/src/v5/i3s-mesh-source.ts +1 -0
- package/src/v5/i3s-selection-source.ts +298 -0
- package/src/v5/index.ts +26 -0
- package/src/v5/mesh-conversion.ts +20 -0
- package/src/v5/mesh-hierarchy.ts +648 -0
- package/src/v5/mesh-source-features.ts +39 -7
- package/src/v5/mesh-source-schema.ts +134 -0
- package/src/v5/mesh-source.ts +16 -4
- package/src/v5/mesh-tileset-sink.ts +1 -1
- 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-file.ts +306 -0
- package/src/v5/point-cloud-potree.ts +130 -0
- package/src/v5/point-cloud-tileset.ts +137 -0
- 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 +88 -17
package/src/v5/conversion-api.ts
CHANGED
|
@@ -111,6 +111,14 @@ export interface ConvertTilesetOptions<TInspection, TInput, TOutput> {
|
|
|
111
111
|
readonly maxInputResourceBytes?: number;
|
|
112
112
|
/** Reject any output resource larger than this configured byte limit. */
|
|
113
113
|
readonly maxOutputResourceBytes?: number;
|
|
114
|
+
/** Maximum measured input bytes across this run; does not bound source allocations. */
|
|
115
|
+
readonly maxInputBytes?: number;
|
|
116
|
+
/** Maximum measured output bytes across this run, including checkpointed resources. */
|
|
117
|
+
readonly maxOutputBytes?: number;
|
|
118
|
+
/** Maximum input resources consumed by this run, including zero-byte resources. */
|
|
119
|
+
readonly maxInputResources?: number;
|
|
120
|
+
/** Maximum outputs processed by this run, including zero-byte and checkpointed resources. */
|
|
121
|
+
readonly maxOutputResources?: number;
|
|
114
122
|
/** Cancel inspection, conversion, validation, or writes. */
|
|
115
123
|
readonly signal?: AbortSignal;
|
|
116
124
|
/** Receive progress updates after each resource transition. */
|
|
@@ -155,6 +163,7 @@ export async function inspectTileset<TInspection, TInput>(
|
|
|
155
163
|
* This does not limit source decoding allocations or content retained by a source.
|
|
156
164
|
* Writes are awaited one resource at a time, keeping queued output bytes bounded by the
|
|
157
165
|
* largest emitted resource and the configured `maxOutputResourceBytes` limit.
|
|
166
|
+
* Cumulative byte/count limits bound measured work in this run, not total allocation memory.
|
|
158
167
|
*/
|
|
159
168
|
export async function convertTileset<TInspection, TInput, TOutput>(
|
|
160
169
|
options: ConvertTilesetOptions<TInspection, TInput, TOutput>
|
|
@@ -167,6 +176,10 @@ export async function convertTileset<TInspection, TInput, TOutput>(
|
|
|
167
176
|
measureOutputBytes,
|
|
168
177
|
maxInputResourceBytes = Number.POSITIVE_INFINITY,
|
|
169
178
|
maxOutputResourceBytes = Number.POSITIVE_INFINITY,
|
|
179
|
+
maxInputBytes = Number.POSITIVE_INFINITY,
|
|
180
|
+
maxOutputBytes = Number.POSITIVE_INFINITY,
|
|
181
|
+
maxInputResources = Number.POSITIVE_INFINITY,
|
|
182
|
+
maxOutputResources = Number.POSITIVE_INFINITY,
|
|
170
183
|
signal,
|
|
171
184
|
onProgress
|
|
172
185
|
} = options;
|
|
@@ -181,19 +194,26 @@ export async function convertTileset<TInspection, TInput, TOutput>(
|
|
|
181
194
|
};
|
|
182
195
|
|
|
183
196
|
try {
|
|
184
|
-
|
|
185
|
-
maxInputResourceBytes
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
197
|
+
for (const [name, limit, code] of [
|
|
198
|
+
['maxInputResourceBytes', maxInputResourceBytes, 'INVALID_INPUT_RESOURCE_LIMIT'],
|
|
199
|
+
['maxOutputResourceBytes', maxOutputResourceBytes, 'INVALID_OUTPUT_RESOURCE_LIMIT'],
|
|
200
|
+
['maxInputBytes', maxInputBytes, 'INVALID_INPUT_BYTES_LIMIT'],
|
|
201
|
+
['maxOutputBytes', maxOutputBytes, 'INVALID_OUTPUT_BYTES_LIMIT'],
|
|
202
|
+
['maxInputResources', maxInputResources, 'INVALID_INPUT_COUNT_LIMIT'],
|
|
203
|
+
['maxOutputResources', maxOutputResources, 'INVALID_OUTPUT_COUNT_LIMIT']
|
|
204
|
+
] as const) {
|
|
205
|
+
if (limit !== Infinity && (!Number.isSafeInteger(limit) || limit < 0)) {
|
|
206
|
+
throw new TileConversionError(
|
|
207
|
+
code,
|
|
208
|
+
name + ' must be a nonnegative safe integer or Infinity'
|
|
209
|
+
);
|
|
210
|
+
}
|
|
192
211
|
}
|
|
193
212
|
reportProgress('inspect');
|
|
194
213
|
const inspection = await inspectTileset(source, signal);
|
|
195
214
|
for await (const inputResource of source.read(inspection, signal)) {
|
|
196
215
|
throwIfAborted(signal);
|
|
216
|
+
checkConversionTotal(inputResources, 1, maxInputResources, 'INPUT_COUNT_LIMIT_EXCEEDED');
|
|
197
217
|
const inputResourceBytes = measureInputBytes(inputResource);
|
|
198
218
|
if (!Number.isSafeInteger(inputResourceBytes) || inputResourceBytes < 0) {
|
|
199
219
|
throw new TileConversionError(
|
|
@@ -207,23 +227,41 @@ export async function convertTileset<TInspection, TInput, TOutput>(
|
|
|
207
227
|
`Input resource is ${inputResourceBytes} bytes, exceeding the configured limit of ${maxInputResourceBytes} bytes`
|
|
208
228
|
);
|
|
209
229
|
}
|
|
230
|
+
checkConversionTotal(
|
|
231
|
+
inputBytes,
|
|
232
|
+
inputResourceBytes,
|
|
233
|
+
maxInputBytes,
|
|
234
|
+
'INPUT_BYTES_LIMIT_EXCEEDED'
|
|
235
|
+
);
|
|
210
236
|
inputResources++;
|
|
211
237
|
inputBytes += inputResourceBytes;
|
|
212
238
|
reportProgress('convert');
|
|
213
239
|
const convertedResources = await codec.convert(inputResource, inspection, signal);
|
|
214
240
|
for await (const outputResource of convertedResources) {
|
|
215
241
|
throwIfAborted(signal);
|
|
242
|
+
checkConversionTotal(outputResources, 1, maxOutputResources, 'OUTPUT_COUNT_LIMIT_EXCEEDED');
|
|
216
243
|
reportProgress('validate');
|
|
217
244
|
const resourceDiagnostics = (await codec.validateOutput?.(outputResource, signal)) || [];
|
|
218
245
|
diagnostics.push(...resourceDiagnostics);
|
|
219
246
|
const outputResourceBytes = measureOutputBytes(outputResource);
|
|
220
|
-
|
|
247
|
+
if (!Number.isSafeInteger(outputResourceBytes) || outputResourceBytes < 0) {
|
|
248
|
+
throw new TileConversionError(
|
|
249
|
+
'INVALID_OUTPUT_RESOURCE_SIZE',
|
|
250
|
+
'measureOutputBytes must return a nonnegative safe integer'
|
|
251
|
+
);
|
|
252
|
+
}
|
|
221
253
|
if (outputResourceBytes > maxOutputResourceBytes) {
|
|
222
254
|
throw new TileConversionError(
|
|
223
255
|
'OUTPUT_RESOURCE_TOO_LARGE',
|
|
224
256
|
`Output resource is ${outputResourceBytes} bytes, exceeding the configured limit of ${maxOutputResourceBytes} bytes`
|
|
225
257
|
);
|
|
226
258
|
}
|
|
259
|
+
checkConversionTotal(
|
|
260
|
+
outputBytes,
|
|
261
|
+
outputResourceBytes,
|
|
262
|
+
maxOutputBytes,
|
|
263
|
+
'OUTPUT_BYTES_LIMIT_EXCEEDED'
|
|
264
|
+
);
|
|
227
265
|
if (resourceDiagnostics.some(diagnostic => diagnostic.severity === 'error')) {
|
|
228
266
|
throw new TileConversionError(
|
|
229
267
|
'OUTPUT_VALIDATION_FAILED',
|
|
@@ -234,6 +272,7 @@ export async function convertTileset<TInspection, TInput, TOutput>(
|
|
|
234
272
|
throwIfAborted(signal);
|
|
235
273
|
await sink.write(outputResource, signal);
|
|
236
274
|
throwIfAborted(signal);
|
|
275
|
+
largestOutputResourceBytes = Math.max(largestOutputResourceBytes, outputResourceBytes);
|
|
237
276
|
outputResources++;
|
|
238
277
|
outputBytes += outputResourceBytes;
|
|
239
278
|
reportProgress('convert');
|
|
@@ -285,3 +324,24 @@ function throwIfAborted(signal?: AbortSignal): void {
|
|
|
285
324
|
throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');
|
|
286
325
|
}
|
|
287
326
|
}
|
|
327
|
+
|
|
328
|
+
/** Checks a cumulative budget without overflowing safe integer accounting. */
|
|
329
|
+
function checkConversionTotal(
|
|
330
|
+
current: number,
|
|
331
|
+
increment: number,
|
|
332
|
+
limit: number,
|
|
333
|
+
code: string
|
|
334
|
+
): void {
|
|
335
|
+
if (increment > limit - current) {
|
|
336
|
+
throw new TileConversionError(
|
|
337
|
+
code,
|
|
338
|
+
`Conversion exceeds cumulative limit ${limit}: ${current} processed, next resource adds ${increment}`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
if (increment > Number.MAX_SAFE_INTEGER - current) {
|
|
342
|
+
throw new TileConversionError(
|
|
343
|
+
'RESOURCE_TOTAL_OVERFLOW',
|
|
344
|
+
'Conversion totals must remain safe integers'
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {runTileConverterCLI} from './node-cli.js';
|
|
6
|
+
|
|
7
|
+
/** Runs the Node process adapter and maps SIGINT into cooperative I/O cancellation. */
|
|
8
|
+
async function main(): Promise<void> {
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
/** Cancels the active operation; promise cleanup closes source and output handles. */
|
|
11
|
+
const cancelOperation = (): void =>
|
|
12
|
+
controller.abort(new Error('Tile conversion operation interrupted'));
|
|
13
|
+
process.on('SIGINT', cancelOperation);
|
|
14
|
+
try {
|
|
15
|
+
process.exitCode = await runTileConverterCLI(
|
|
16
|
+
process.argv.slice(2),
|
|
17
|
+
{
|
|
18
|
+
stdout: text => process.stdout.write(text),
|
|
19
|
+
stderr: text => process.stderr.write(text)
|
|
20
|
+
},
|
|
21
|
+
controller.signal
|
|
22
|
+
);
|
|
23
|
+
} finally {
|
|
24
|
+
process.removeListener('SIGINT', cancelOperation);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
void main();
|
package/src/v5/core.ts
CHANGED
|
@@ -59,3 +59,9 @@ export type {
|
|
|
59
59
|
BrowserTilesetResourceDescriptor,
|
|
60
60
|
BrowserTilesetConversionInputResource
|
|
61
61
|
} from './browser-tileset-source.js';
|
|
62
|
+
|
|
63
|
+
export {selectBrowserTilesetContent} from './browser-tileset-source.js';
|
|
64
|
+
export type {
|
|
65
|
+
BrowserTilesetDocumentPlacement,
|
|
66
|
+
BrowserTilesetContentSelection
|
|
67
|
+
} from './browser-tileset-source.js';
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
// Copyright (c) vis.gl contributors
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
encodeI3SMeshLayer,
|
|
7
|
+
encodeI3SMeshLayerWithDraco,
|
|
8
|
+
preloadI3SMeshCompression
|
|
9
|
+
} from '@loaders.gl/i3s';
|
|
6
10
|
import type {
|
|
7
11
|
EncodedI3SMeshLayer,
|
|
8
12
|
I3SMeshWriterOptions,
|
|
@@ -23,6 +27,8 @@ import type {BrowserTileConversionFile} from '@loaders.gl/tile-converter/v5/core
|
|
|
23
27
|
export interface I3SMeshConversionResource extends EncodedI3SMeshLayer {
|
|
24
28
|
/** Source placement identifier used for diagnostics. */
|
|
25
29
|
readonly id: string;
|
|
30
|
+
/** Original I3S layer type when authoring from an I3S source. */
|
|
31
|
+
readonly sourceLayerType?: '3DObject' | 'IntegratedMesh';
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
/** Spatial preparation and required authoring limits for the I3S codec. */
|
|
@@ -34,6 +40,8 @@ export interface I3SMeshConversionCodecOptions
|
|
|
34
40
|
readonly normalReferenceFrame?: 'earth-centered' | 'vertex-reference-frame';
|
|
35
41
|
/** Lossless Edge Breaker geometry by default; false retains raw I3S geometry. */
|
|
36
42
|
readonly draco?: boolean;
|
|
43
|
+
/** Explicitly authorize IntegratedMesh input to become 3DObject output; omission rejects it. */
|
|
44
|
+
readonly integratedMeshPolicy?: 'as-3d-object';
|
|
37
45
|
/** Full Draco encoder/decoder runtime URLs or injected modules. */
|
|
38
46
|
readonly dracoLibraryOptions?: I3SDracoLibraryOptions;
|
|
39
47
|
}
|
|
@@ -42,6 +50,15 @@ export interface I3SMeshConversionCodecOptions
|
|
|
42
50
|
export function createI3SMeshConversionCodec<TInspection = unknown>(
|
|
43
51
|
options: I3SMeshConversionCodecOptions
|
|
44
52
|
): TileConversionCodec<TInspection, MeshSourceResource, I3SMeshConversionResource> {
|
|
53
|
+
if (
|
|
54
|
+
options.integratedMeshPolicy !== undefined &&
|
|
55
|
+
options.integratedMeshPolicy !== 'as-3d-object'
|
|
56
|
+
) {
|
|
57
|
+
throw new TileConversionError(
|
|
58
|
+
'I3S_LAYER_TYPE_POLICY_INVALID',
|
|
59
|
+
'Unknown IntegratedMesh conversion policy'
|
|
60
|
+
);
|
|
61
|
+
}
|
|
45
62
|
const reference = options.spatialContext.spatialReference;
|
|
46
63
|
const height =
|
|
47
64
|
reference.targetHeightReference === 'native'
|
|
@@ -65,6 +82,15 @@ export function createI3SMeshConversionCodec<TInspection = unknown>(
|
|
|
65
82
|
/** Places a source mesh in ECEF and authors one layer without modifying source arrays. */
|
|
66
83
|
async *convert(resource, _inspection, signal) {
|
|
67
84
|
signal?.throwIfAborted();
|
|
85
|
+
if (
|
|
86
|
+
resource.sourceLayerType === 'IntegratedMesh' &&
|
|
87
|
+
options.integratedMeshPolicy !== 'as-3d-object'
|
|
88
|
+
) {
|
|
89
|
+
throw new TileConversionError(
|
|
90
|
+
'I3S_LAYER_TYPE_CHANGE_REQUIRED',
|
|
91
|
+
'IntegratedMesh to 3DObject conversion requires integratedMeshPolicy=as-3d-object'
|
|
92
|
+
);
|
|
93
|
+
}
|
|
68
94
|
const mesh = validateMeshGeometry(resource.mesh, true);
|
|
69
95
|
const positions = mesh.attributes.POSITION.value;
|
|
70
96
|
const context = options.spatialContext;
|
|
@@ -86,6 +112,7 @@ export function createI3SMeshConversionCodec<TInspection = unknown>(
|
|
|
86
112
|
if (mesh.attributes.NORMAL) mesh.attributes.NORMAL = {value: transformed.normals!, size: 3};
|
|
87
113
|
let layer: EncodedI3SMeshLayer;
|
|
88
114
|
try {
|
|
115
|
+
await preloadI3SMeshCompression();
|
|
89
116
|
const writerOptions = {
|
|
90
117
|
...options,
|
|
91
118
|
material: resource.material,
|
|
@@ -105,11 +132,22 @@ export function createI3SMeshConversionCodec<TInspection = unknown>(
|
|
|
105
132
|
signal?.throwIfAborted();
|
|
106
133
|
objectIdOffset +=
|
|
107
134
|
resource.features?.batches.reduce((total, batch) => total + batch.data.numRows, 0) ?? 1;
|
|
108
|
-
yield {id: resource.id, ...layer};
|
|
135
|
+
yield {id: resource.id, ...layer, sourceLayerType: resource.sourceLayerType};
|
|
109
136
|
},
|
|
110
137
|
/** Reports measured precision and explicitly authorized integer representation changes. */
|
|
111
138
|
async validateOutput(resource) {
|
|
112
139
|
return [
|
|
140
|
+
...(resource.sourceLayerType === 'IntegratedMesh'
|
|
141
|
+
? [
|
|
142
|
+
{
|
|
143
|
+
code: 'I3S_LAYER_TYPE_CHANGED',
|
|
144
|
+
message:
|
|
145
|
+
'Explicitly authored IntegratedMesh geometry as I3S 1.7 3DObject; integrated surface layer semantics and source LOD are not preserved',
|
|
146
|
+
severity: 'warning' as const,
|
|
147
|
+
resourceId: resource.id
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
: []),
|
|
113
151
|
...(resource.maximumPositionError > 0
|
|
114
152
|
? [
|
|
115
153
|
{
|
package/src/v5/i3s-mesh-sink.ts
CHANGED
|
@@ -113,7 +113,7 @@ export function createI3SMeshSink(options: I3SMeshSinkOptions): I3SMeshSink {
|
|
|
113
113
|
'I3S collections require 1–64 meshes and a positive resource byte limit'
|
|
114
114
|
);
|
|
115
115
|
const memory = createBoundedMemoryTileConversionSink(options);
|
|
116
|
-
const compressor = new GZipCompressor({useNative: false
|
|
116
|
+
const compressor = new GZipCompressor({useNative: false});
|
|
117
117
|
const decompressor = new GZipDecompressor({useNative: false});
|
|
118
118
|
const leaves: {node: MeshNode; legacy: Record<string, unknown>}[] = [];
|
|
119
119
|
const identifiers = new Set<string>();
|
|
@@ -212,6 +212,7 @@ export function createI3SMeshTilesetConversionSource(
|
|
|
212
212
|
yield {
|
|
213
213
|
id: `${item.tile.id}/${entry.index}`,
|
|
214
214
|
origin: [0, 0, 0],
|
|
215
|
+
sourceLayerType: metadata.tileset.layerType,
|
|
215
216
|
mesh,
|
|
216
217
|
normalReferenceFrame:
|
|
217
218
|
content.normalReferenceFrame === 'vertex-reference-frame'
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {coreApi, parse} from '@loaders.gl/core';
|
|
6
|
+
import {I3SLoader} from '@loaders.gl/i3s';
|
|
7
|
+
import type {I3STileHeader, SceneLayer3D} from '@loaders.gl/i3s';
|
|
8
|
+
import {I3SSource, Tileset3D} from '@loaders.gl/tiles';
|
|
9
|
+
import type {TilesetSourceResolver} from '@loaders.gl/tiles';
|
|
10
|
+
import {
|
|
11
|
+
createI3SConversionSpatialContext,
|
|
12
|
+
TileConversionError
|
|
13
|
+
} from '@loaders.gl/tile-converter/v5/core';
|
|
14
|
+
import {createI3SMeshTilesetConversionSource} from './i3s-mesh-source.js';
|
|
15
|
+
import type {I3SMeshSourceFeatureOptions} from './i3s-mesh-source.js';
|
|
16
|
+
import {SELECTED_MESH_LIMITS, measureMeshBytes} from './selected-mesh-source.js';
|
|
17
|
+
import type {SelectedMeshSourceOptions} from './selected-mesh-source.js';
|
|
18
|
+
|
|
19
|
+
/** Inspected leaf mesh identity and immutable normalized header. */
|
|
20
|
+
export interface I3SMeshConversionResourceDescriptor {
|
|
21
|
+
/** Unique declared node identity. */
|
|
22
|
+
readonly resourceId: string;
|
|
23
|
+
/** Source-managed geometry URL. */
|
|
24
|
+
readonly uri: string;
|
|
25
|
+
/** Normalized header without runtime objects. */
|
|
26
|
+
readonly header: I3STileHeader;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Metadata-only I3S selection, independent of browser or Node transport ownership. */
|
|
30
|
+
export interface I3SMeshConversionInspection {
|
|
31
|
+
/** Discriminant for I3S mesh input. */
|
|
32
|
+
readonly kind: 'i3s';
|
|
33
|
+
/** Absolute layer or archive virtual root URL. */
|
|
34
|
+
readonly rootUrl: string;
|
|
35
|
+
/** Original layer declarations. */
|
|
36
|
+
readonly layer: SceneLayer3D;
|
|
37
|
+
/** Ordered leaf mesh declarations. */
|
|
38
|
+
readonly resources: readonly I3SMeshConversionResourceDescriptor[];
|
|
39
|
+
/** Previously consumed transport bytes, supplied by the owner. */
|
|
40
|
+
readonly metadataBytes: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Platform-injected transport and decoder policy for bounded I3S selection. */
|
|
44
|
+
export interface I3SMeshSelectionOptions {
|
|
45
|
+
/** Root layer or confined archive root URL. */
|
|
46
|
+
readonly rootUrl: string;
|
|
47
|
+
/** Lifetime-budgeted transport shared between metadata and payload reads. */
|
|
48
|
+
readonly fetcher: typeof fetch;
|
|
49
|
+
/** Operation cancellation. */
|
|
50
|
+
readonly signal: AbortSignal;
|
|
51
|
+
/** Bundled or application-owned Draco assets. */
|
|
52
|
+
readonly dracoLibraryOptions: SelectedMeshSourceOptions['dracoLibraryOptions'];
|
|
53
|
+
/** Maximum decoded mesh bytes; callers may lower the 16 MiB profile. */
|
|
54
|
+
readonly maxInputBytes?: number;
|
|
55
|
+
/** Maximum inspected nodes; callers may lower the 1,000 node profile. */
|
|
56
|
+
readonly maxInputResources?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Creates a dedicated source-coordinate runtime with transport and all decoder assets injected. */
|
|
60
|
+
function createRuntime(
|
|
61
|
+
transport: I3SMeshSelectionOptions,
|
|
62
|
+
layer?: SceneLayer3D,
|
|
63
|
+
header?: I3STileHeader
|
|
64
|
+
): Tileset3D {
|
|
65
|
+
const libraries = transport.dracoLibraryOptions;
|
|
66
|
+
const sourceUrl = new URL(transport.rootUrl);
|
|
67
|
+
sourceUrl.search = '';
|
|
68
|
+
sourceUrl.hash = '';
|
|
69
|
+
const options = {
|
|
70
|
+
modules: libraries.modules,
|
|
71
|
+
core: {
|
|
72
|
+
worker: false,
|
|
73
|
+
fetch: transport.fetcher,
|
|
74
|
+
CDN: libraries.CDN,
|
|
75
|
+
useLocalLibraries: libraries.useLocalLibraries
|
|
76
|
+
},
|
|
77
|
+
draco: {decoderProfile: libraries.decoderProfile},
|
|
78
|
+
// The injected transport owns credential scope; runtime query parameters apply to every URL.
|
|
79
|
+
i3s: {geometryMode: 'source' as const, decodeTextures: false, useCompressedTextures: false}
|
|
80
|
+
};
|
|
81
|
+
/** Parses a source-managed resource with the same transport available to nested dependencies. */
|
|
82
|
+
const loadResource: TilesetSourceResolver['loadResource'] = async (url, loader, loadOptions) => {
|
|
83
|
+
const response = await transport.fetcher(url);
|
|
84
|
+
const contextUrl = new URL(url);
|
|
85
|
+
contextUrl.search = '';
|
|
86
|
+
contextUrl.hash = '';
|
|
87
|
+
return parse(await response.arrayBuffer(), loader, loadOptions, {
|
|
88
|
+
url: contextUrl.href,
|
|
89
|
+
fetch: transport.fetcher,
|
|
90
|
+
coreApi,
|
|
91
|
+
_parse: parse
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
const source = new I3SSource(
|
|
95
|
+
header && layer
|
|
96
|
+
? {
|
|
97
|
+
...layer,
|
|
98
|
+
root: {...header, children: []},
|
|
99
|
+
type: 'I3S',
|
|
100
|
+
url: sourceUrl.href,
|
|
101
|
+
basePath: sourceUrl.href,
|
|
102
|
+
loader: I3SLoader,
|
|
103
|
+
coreApi
|
|
104
|
+
}
|
|
105
|
+
: {
|
|
106
|
+
url: sourceUrl.href,
|
|
107
|
+
loader: I3SLoader,
|
|
108
|
+
coreApi,
|
|
109
|
+
resolver: {loadRoot: loadResource, loadResource}
|
|
110
|
+
},
|
|
111
|
+
options
|
|
112
|
+
);
|
|
113
|
+
// Preloaded selection still uses this resolver for content; never let core perform unbounded I/O.
|
|
114
|
+
source.coreApi = {...coreApi, load: loadResource as typeof coreApi.load};
|
|
115
|
+
return new Tileset3D(source, {i3s: options.i3s});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Inspects supported layer/leaf declarations without requesting geometry, textures or attributes. */
|
|
119
|
+
export async function inspectI3SMeshConversionSource(
|
|
120
|
+
options: I3SMeshSelectionOptions
|
|
121
|
+
): Promise<I3SMeshConversionInspection> {
|
|
122
|
+
const {signal} = options;
|
|
123
|
+
validateSelectionOptions(options);
|
|
124
|
+
signal.throwIfAborted();
|
|
125
|
+
const transport = options;
|
|
126
|
+
let runtime: Tileset3D | undefined;
|
|
127
|
+
try {
|
|
128
|
+
const response = await transport.fetcher(transport.rootUrl);
|
|
129
|
+
const layer = (await response.json()) as SceneLayer3D;
|
|
130
|
+
if (!['3DObject', 'IntegratedMesh'].includes(layer.layerType))
|
|
131
|
+
throw new Error('Conversion accepts I3S 3DObject or IntegratedMesh layers.');
|
|
132
|
+
// Reuse the inspected root; all further metadata requests are still charged.
|
|
133
|
+
const rootFetcher = transport.fetcher;
|
|
134
|
+
const cachedFetcher: typeof fetch = (url, options) => {
|
|
135
|
+
const requestUrl = new URL(url instanceof Request ? url.url : String(url));
|
|
136
|
+
const root = new URL(transport.rootUrl);
|
|
137
|
+
return requestUrl.origin === root.origin && requestUrl.pathname === root.pathname
|
|
138
|
+
? Promise.resolve(new Response(JSON.stringify(layer)))
|
|
139
|
+
: rootFetcher(url, options);
|
|
140
|
+
};
|
|
141
|
+
runtime = createRuntime({...transport, fetcher: cachedFetcher});
|
|
142
|
+
await runtime.tilesetInitializationPromise;
|
|
143
|
+
await createI3SMeshTilesetConversionSource(runtime).inspect(signal);
|
|
144
|
+
const stack = [runtime.root!];
|
|
145
|
+
const identifiers = new Set<string>();
|
|
146
|
+
const resources: I3SMeshConversionResourceDescriptor[] = [];
|
|
147
|
+
while (stack.length) {
|
|
148
|
+
signal.throwIfAborted();
|
|
149
|
+
const tile = stack.pop()!;
|
|
150
|
+
if (
|
|
151
|
+
!['string', 'number'].includes(typeof tile.header.id) ||
|
|
152
|
+
String(tile.header.id).length === 0
|
|
153
|
+
)
|
|
154
|
+
throw new Error('I3S nodes require a declared identity.');
|
|
155
|
+
const identifier = String(tile.header.id);
|
|
156
|
+
if (identifiers.has(identifier))
|
|
157
|
+
throw new Error('Repeated I3S node identity is unsupported.');
|
|
158
|
+
identifiers.add(identifier);
|
|
159
|
+
if (
|
|
160
|
+
identifiers.size + stack.length + (tile.header.children?.length ?? 0) >
|
|
161
|
+
(options.maxInputResources ?? SELECTED_MESH_LIMITS.maxInputResources)
|
|
162
|
+
)
|
|
163
|
+
throw new Error('I3S inspection exceeds the 1,000 node limit.');
|
|
164
|
+
await runtime.source.loadTileChildrenForTraversal!(tile, signal);
|
|
165
|
+
if (!tile.children.length && tile.contentUrl)
|
|
166
|
+
resources.push({
|
|
167
|
+
resourceId: identifier,
|
|
168
|
+
uri: tile.contentUrl,
|
|
169
|
+
header: structuredClone(tile.header)
|
|
170
|
+
});
|
|
171
|
+
for (const child of [...tile.children].reverse()) stack.push(child);
|
|
172
|
+
}
|
|
173
|
+
signal.throwIfAborted();
|
|
174
|
+
if (!resources.length) throw new Error('No supported leaf mesh nodes are declared.');
|
|
175
|
+
return {
|
|
176
|
+
kind: 'i3s',
|
|
177
|
+
rootUrl: transport.rootUrl,
|
|
178
|
+
layer,
|
|
179
|
+
resources,
|
|
180
|
+
metadataBytes: 0
|
|
181
|
+
};
|
|
182
|
+
} finally {
|
|
183
|
+
runtime?.destroy();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Converts explicitly selected I3S leaf meshes into a bounded flat collection in either format. */
|
|
188
|
+
export async function createSelectedI3SMeshConversionSource(
|
|
189
|
+
inspection: I3SMeshConversionInspection,
|
|
190
|
+
options: I3SMeshSelectionOptions & {
|
|
191
|
+
/** Explicit distinct inspected leaf-node identities. */
|
|
192
|
+
readonly resourceIds: readonly string[];
|
|
193
|
+
/** Explicit attribute mapping for feature-bearing meshes. */
|
|
194
|
+
readonly features?: I3SMeshSourceFeatureOptions;
|
|
195
|
+
}
|
|
196
|
+
) {
|
|
197
|
+
const {resourceIds, signal, features} = options;
|
|
198
|
+
validateSelectionOptions(options);
|
|
199
|
+
signal.throwIfAborted();
|
|
200
|
+
if (inspection.rootUrl !== options.rootUrl)
|
|
201
|
+
throw new TileConversionError(
|
|
202
|
+
'INVALID_I3S_SELECTION',
|
|
203
|
+
'Selection transport must match the inspected layer'
|
|
204
|
+
);
|
|
205
|
+
if (
|
|
206
|
+
!resourceIds.length ||
|
|
207
|
+
resourceIds.length > SELECTED_MESH_LIMITS.maxSelectedResources ||
|
|
208
|
+
new Set(resourceIds).size !== resourceIds.length ||
|
|
209
|
+
resourceIds.some(
|
|
210
|
+
identifier => !inspection.resources.some(resource => resource.resourceId === identifier)
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
throw new Error('Select between 1 and 64 distinct leaf mesh nodes.');
|
|
214
|
+
const selected = inspection.resources.filter(resource =>
|
|
215
|
+
resourceIds.includes(resource.resourceId)
|
|
216
|
+
);
|
|
217
|
+
if (
|
|
218
|
+
selected.some(
|
|
219
|
+
resource =>
|
|
220
|
+
resource.header.children?.length ||
|
|
221
|
+
!resource.header.contentUrl ||
|
|
222
|
+
resource.uri !== resource.header.contentUrl
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
throw new Error('Only inspected leaf mesh headers can be converted.');
|
|
226
|
+
const transport = options;
|
|
227
|
+
let decodedBytes = 0;
|
|
228
|
+
let meshCount = 0;
|
|
229
|
+
{
|
|
230
|
+
const qualification = createRuntime(transport, inspection.layer, selected[0].header);
|
|
231
|
+
let spatialContext: ReturnType<typeof createI3SConversionSpatialContext>;
|
|
232
|
+
try {
|
|
233
|
+
await qualification.tilesetInitializationPromise;
|
|
234
|
+
const metadata = await createI3SMeshTilesetConversionSource(qualification).inspect(signal);
|
|
235
|
+
spatialContext = createI3SConversionSpatialContext(metadata.spatialReference!, {
|
|
236
|
+
targetCrs: 'EPSG:4978'
|
|
237
|
+
});
|
|
238
|
+
} finally {
|
|
239
|
+
qualification.destroy();
|
|
240
|
+
}
|
|
241
|
+
const source = {
|
|
242
|
+
/** Reuses only immutable inspected declarations. */
|
|
243
|
+
inspect: async () => inspection,
|
|
244
|
+
/** Releases each dedicated node runtime before moving on or closing early. */
|
|
245
|
+
async *read() {
|
|
246
|
+
for (const descriptor of selected) {
|
|
247
|
+
const runtime = createRuntime(transport, inspection.layer, descriptor.header);
|
|
248
|
+
try {
|
|
249
|
+
await runtime.tilesetInitializationPromise;
|
|
250
|
+
const meshSource = createI3SMeshTilesetConversionSource(runtime, {
|
|
251
|
+
unloadContent: true,
|
|
252
|
+
features,
|
|
253
|
+
readExternalResource: async uri =>
|
|
254
|
+
new Uint8Array(
|
|
255
|
+
await (await transport.fetcher(runtime.source.getTileUrl(uri))).arrayBuffer()
|
|
256
|
+
)
|
|
257
|
+
});
|
|
258
|
+
const metadata = await meshSource.inspect(signal);
|
|
259
|
+
for await (const resource of meshSource.read(metadata, signal)) {
|
|
260
|
+
decodedBytes += measureMeshBytes(resource);
|
|
261
|
+
if (
|
|
262
|
+
++meshCount > SELECTED_MESH_LIMITS.maxMeshResources ||
|
|
263
|
+
decodedBytes > (options.maxInputBytes ?? SELECTED_MESH_LIMITS.maxInputBytes)
|
|
264
|
+
)
|
|
265
|
+
throw new TileConversionError(
|
|
266
|
+
'INPUT_RESOURCE_TOO_LARGE',
|
|
267
|
+
'Selected I3S decoded resources exceed the input/mesh budget'
|
|
268
|
+
);
|
|
269
|
+
yield {...resource, id: descriptor.resourceId};
|
|
270
|
+
}
|
|
271
|
+
} finally {
|
|
272
|
+
runtime.destroy();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
return {source, spatialContext};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Keeps public selected-source limits within the same bounded profile as platform adapters. */
|
|
282
|
+
function validateSelectionOptions(options: I3SMeshSelectionOptions): void {
|
|
283
|
+
for (const [value, maximum] of [
|
|
284
|
+
[
|
|
285
|
+
options.maxInputBytes ?? SELECTED_MESH_LIMITS.maxInputBytes,
|
|
286
|
+
SELECTED_MESH_LIMITS.maxInputBytes
|
|
287
|
+
],
|
|
288
|
+
[
|
|
289
|
+
options.maxInputResources ?? SELECTED_MESH_LIMITS.maxInputResources,
|
|
290
|
+
SELECTED_MESH_LIMITS.maxInputResources
|
|
291
|
+
]
|
|
292
|
+
])
|
|
293
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum)
|
|
294
|
+
throw new TileConversionError(
|
|
295
|
+
'INVALID_I3S_SELECTION_LIMIT',
|
|
296
|
+
'I3S selection limits must be positive safe integers within the bounded profile'
|
|
297
|
+
);
|
|
298
|
+
}
|
package/src/v5/index.ts
CHANGED
|
@@ -129,3 +129,29 @@ export type {
|
|
|
129
129
|
I3SMeshTilesetSourceOptions,
|
|
130
130
|
I3SMeshSourceFeatureOptions
|
|
131
131
|
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
132
|
+
|
|
133
|
+
export {convertPointCloudToTileset} from '@loaders.gl/tile-converter/v5/adapters';
|
|
134
|
+
export type {
|
|
135
|
+
ConvertPointCloudToTilesetOptions,
|
|
136
|
+
ConvertedPointCloudTileset
|
|
137
|
+
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
138
|
+
|
|
139
|
+
export {
|
|
140
|
+
encodePotreeArchive,
|
|
141
|
+
openPointCloudFile,
|
|
142
|
+
extractMeshVertices
|
|
143
|
+
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
144
|
+
export type {
|
|
145
|
+
EncodePotreeArchiveOptions,
|
|
146
|
+
OpenPointCloudFileOptions,
|
|
147
|
+
OpenedPointCloudFile,
|
|
148
|
+
ExtractMeshVerticesOptions
|
|
149
|
+
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
150
|
+
|
|
151
|
+
export {createMeshSourceFeatureOptions} from '@loaders.gl/tile-converter/v5/adapters';
|
|
152
|
+
export type {MeshSourceSchemaOptions} from '@loaders.gl/tile-converter/v5/adapters';
|
|
153
|
+
export {convertPointCloudToPotree} from '@loaders.gl/tile-converter/v5/adapters';
|
|
154
|
+
export type {
|
|
155
|
+
ConvertPointCloudToPotreeOptions,
|
|
156
|
+
ConvertedPointCloudPotree
|
|
157
|
+
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
@@ -21,6 +21,15 @@ import {encodeDracoMeshTile} from './mesh-draco.js';
|
|
|
21
21
|
export interface MeshConversionInput {
|
|
22
22
|
/** Caller-owned resource identifier, forwarded to the output sink. */
|
|
23
23
|
readonly id: string;
|
|
24
|
+
/** Inspected content placement, used by the hierarchy sink independently of primitive IDs. */
|
|
25
|
+
readonly contentResourceId?: string;
|
|
26
|
+
/** Ordered primitive within a content; last marks complete extraction for hierarchy authoring. */
|
|
27
|
+
readonly contentPrimitive?: {
|
|
28
|
+
/** Zero-based primitive placement index in scene traversal order. */
|
|
29
|
+
readonly index: number;
|
|
30
|
+
/** Whether this is the final primitive placement in the content. */
|
|
31
|
+
readonly last: boolean;
|
|
32
|
+
};
|
|
24
33
|
/** Triangle geometry with absolute packed Float32 or Float64 source positions. */
|
|
25
34
|
readonly mesh: MeshGeometry;
|
|
26
35
|
/** Original I3S vector basis; overrides the codec default when supplied by a source. */
|
|
@@ -37,6 +46,15 @@ export interface MeshConversionInput {
|
|
|
37
46
|
export interface EncodedMeshConversionResource {
|
|
38
47
|
/** Caller-owned resource identifier. */
|
|
39
48
|
readonly id: string;
|
|
49
|
+
/** Inspected content placement, retained by the codec for hierarchy authoring. */
|
|
50
|
+
readonly contentResourceId?: string;
|
|
51
|
+
/** Ordered primitive within a content; last marks complete extraction for hierarchy authoring. */
|
|
52
|
+
readonly contentPrimitive?: {
|
|
53
|
+
/** Zero-based primitive placement index in scene traversal order. */
|
|
54
|
+
readonly index: number;
|
|
55
|
+
/** Whether this is the final primitive placement in the content. */
|
|
56
|
+
readonly last: boolean;
|
|
57
|
+
};
|
|
40
58
|
/** Self-contained GLB with local positions and transformed normals. */
|
|
41
59
|
readonly glb: ArrayBuffer;
|
|
42
60
|
/** Target-frame translation to add to the encoded positions. */
|
|
@@ -205,6 +223,8 @@ export function createMeshConversionCodec<TInspection = unknown>(
|
|
|
205
223
|
}
|
|
206
224
|
yield {
|
|
207
225
|
id: resource.id,
|
|
226
|
+
contentResourceId: resource.contentResourceId,
|
|
227
|
+
contentPrimitive: resource.contentPrimitive,
|
|
208
228
|
glb: encoded.glb,
|
|
209
229
|
origin,
|
|
210
230
|
boundingBox: [minimum, maximum],
|