@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
package/dist/v5/core.js
CHANGED
|
@@ -27,9 +27,23 @@ async function convertTileset(options) {
|
|
|
27
27
|
measureOutputBytes,
|
|
28
28
|
maxInputResourceBytes = Number.POSITIVE_INFINITY,
|
|
29
29
|
maxOutputResourceBytes = Number.POSITIVE_INFINITY,
|
|
30
|
+
maxInputBytes = Number.POSITIVE_INFINITY,
|
|
31
|
+
maxOutputBytes = Number.POSITIVE_INFINITY,
|
|
32
|
+
maxInputResources = Number.POSITIVE_INFINITY,
|
|
33
|
+
maxOutputResources = Number.POSITIVE_INFINITY,
|
|
30
34
|
signal,
|
|
31
35
|
onProgress
|
|
32
36
|
} = options;
|
|
37
|
+
const started = options.profile ? performance.now() : 0;
|
|
38
|
+
const timings = options.profile ? {
|
|
39
|
+
inspectMilliseconds: 0,
|
|
40
|
+
readMilliseconds: 0,
|
|
41
|
+
convertMilliseconds: 0,
|
|
42
|
+
validateMilliseconds: 0,
|
|
43
|
+
writeMilliseconds: 0,
|
|
44
|
+
finalizeMilliseconds: 0,
|
|
45
|
+
totalMilliseconds: 0
|
|
46
|
+
} : void 0;
|
|
33
47
|
let inputResources = 0;
|
|
34
48
|
let outputResources = 0;
|
|
35
49
|
let inputBytes = 0;
|
|
@@ -40,16 +54,34 @@ async function convertTileset(options) {
|
|
|
40
54
|
onProgress?.({ phase, inputResources, outputResources, inputBytes, outputBytes });
|
|
41
55
|
};
|
|
42
56
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
for (const [name, limit, code] of [
|
|
58
|
+
["maxInputResourceBytes", maxInputResourceBytes, "INVALID_INPUT_RESOURCE_LIMIT"],
|
|
59
|
+
["maxOutputResourceBytes", maxOutputResourceBytes, "INVALID_OUTPUT_RESOURCE_LIMIT"],
|
|
60
|
+
["maxInputBytes", maxInputBytes, "INVALID_INPUT_BYTES_LIMIT"],
|
|
61
|
+
["maxOutputBytes", maxOutputBytes, "INVALID_OUTPUT_BYTES_LIMIT"],
|
|
62
|
+
["maxInputResources", maxInputResources, "INVALID_INPUT_COUNT_LIMIT"],
|
|
63
|
+
["maxOutputResources", maxOutputResources, "INVALID_OUTPUT_COUNT_LIMIT"]
|
|
64
|
+
]) {
|
|
65
|
+
if (limit !== Infinity && (!Number.isSafeInteger(limit) || limit < 0)) {
|
|
66
|
+
throw new TileConversionError(
|
|
67
|
+
code,
|
|
68
|
+
name + " must be a nonnegative safe integer or Infinity"
|
|
69
|
+
);
|
|
70
|
+
}
|
|
48
71
|
}
|
|
49
72
|
reportProgress("inspect");
|
|
50
|
-
const inspection = await
|
|
51
|
-
|
|
73
|
+
const inspection = await measureConversionOperation(
|
|
74
|
+
timings,
|
|
75
|
+
"inspectMilliseconds",
|
|
76
|
+
() => inspectTileset(source, signal)
|
|
77
|
+
);
|
|
78
|
+
for await (const inputResource of measureConversionIterable(
|
|
79
|
+
source.read(inspection, signal),
|
|
80
|
+
timings,
|
|
81
|
+
"readMilliseconds"
|
|
82
|
+
)) {
|
|
52
83
|
throwIfAborted(signal);
|
|
84
|
+
checkConversionTotal(inputResources, 1, maxInputResources, "INPUT_COUNT_LIMIT_EXCEEDED");
|
|
53
85
|
const inputResourceBytes = measureInputBytes(inputResource);
|
|
54
86
|
if (!Number.isSafeInteger(inputResourceBytes) || inputResourceBytes < 0) {
|
|
55
87
|
throw new TileConversionError(
|
|
@@ -63,23 +95,53 @@ async function convertTileset(options) {
|
|
|
63
95
|
`Input resource is ${inputResourceBytes} bytes, exceeding the configured limit of ${maxInputResourceBytes} bytes`
|
|
64
96
|
);
|
|
65
97
|
}
|
|
98
|
+
checkConversionTotal(
|
|
99
|
+
inputBytes,
|
|
100
|
+
inputResourceBytes,
|
|
101
|
+
maxInputBytes,
|
|
102
|
+
"INPUT_BYTES_LIMIT_EXCEEDED"
|
|
103
|
+
);
|
|
66
104
|
inputResources++;
|
|
67
105
|
inputBytes += inputResourceBytes;
|
|
68
106
|
reportProgress("convert");
|
|
69
|
-
const convertedResources = await
|
|
70
|
-
|
|
107
|
+
const convertedResources = await measureConversionOperation(
|
|
108
|
+
timings,
|
|
109
|
+
"convertMilliseconds",
|
|
110
|
+
() => codec.convert(inputResource, inspection, signal)
|
|
111
|
+
);
|
|
112
|
+
for await (const outputResource of measureConversionIterable(
|
|
113
|
+
convertedResources,
|
|
114
|
+
timings,
|
|
115
|
+
"convertMilliseconds"
|
|
116
|
+
)) {
|
|
71
117
|
throwIfAborted(signal);
|
|
118
|
+
checkConversionTotal(outputResources, 1, maxOutputResources, "OUTPUT_COUNT_LIMIT_EXCEEDED");
|
|
72
119
|
reportProgress("validate");
|
|
73
|
-
const resourceDiagnostics = await
|
|
120
|
+
const resourceDiagnostics = await measureConversionOperation(
|
|
121
|
+
timings,
|
|
122
|
+
"validateMilliseconds",
|
|
123
|
+
() => codec.validateOutput?.(outputResource, signal)
|
|
124
|
+
) || [];
|
|
74
125
|
diagnostics.push(...resourceDiagnostics);
|
|
75
126
|
const outputResourceBytes = measureOutputBytes(outputResource);
|
|
76
|
-
|
|
127
|
+
if (!Number.isSafeInteger(outputResourceBytes) || outputResourceBytes < 0) {
|
|
128
|
+
throw new TileConversionError(
|
|
129
|
+
"INVALID_OUTPUT_RESOURCE_SIZE",
|
|
130
|
+
"measureOutputBytes must return a nonnegative safe integer"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
77
133
|
if (outputResourceBytes > maxOutputResourceBytes) {
|
|
78
134
|
throw new TileConversionError(
|
|
79
135
|
"OUTPUT_RESOURCE_TOO_LARGE",
|
|
80
136
|
`Output resource is ${outputResourceBytes} bytes, exceeding the configured limit of ${maxOutputResourceBytes} bytes`
|
|
81
137
|
);
|
|
82
138
|
}
|
|
139
|
+
checkConversionTotal(
|
|
140
|
+
outputBytes,
|
|
141
|
+
outputResourceBytes,
|
|
142
|
+
maxOutputBytes,
|
|
143
|
+
"OUTPUT_BYTES_LIMIT_EXCEEDED"
|
|
144
|
+
);
|
|
83
145
|
if (resourceDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
84
146
|
throw new TileConversionError(
|
|
85
147
|
"OUTPUT_VALIDATION_FAILED",
|
|
@@ -88,8 +150,13 @@ async function convertTileset(options) {
|
|
|
88
150
|
);
|
|
89
151
|
}
|
|
90
152
|
throwIfAborted(signal);
|
|
91
|
-
await
|
|
153
|
+
await measureConversionOperation(
|
|
154
|
+
timings,
|
|
155
|
+
"writeMilliseconds",
|
|
156
|
+
() => sink.write(outputResource, signal)
|
|
157
|
+
);
|
|
92
158
|
throwIfAborted(signal);
|
|
159
|
+
largestOutputResourceBytes = Math.max(largestOutputResourceBytes, outputResourceBytes);
|
|
93
160
|
outputResources++;
|
|
94
161
|
outputBytes += outputResourceBytes;
|
|
95
162
|
reportProgress("convert");
|
|
@@ -106,8 +173,9 @@ async function convertTileset(options) {
|
|
|
106
173
|
};
|
|
107
174
|
throwIfAborted(signal);
|
|
108
175
|
reportProgress("finalize");
|
|
109
|
-
await sink.finalize(report);
|
|
110
|
-
|
|
176
|
+
await measureConversionOperation(timings, "finalizeMilliseconds", () => sink.finalize(report));
|
|
177
|
+
if (timings) timings.totalMilliseconds = Math.max(0, performance.now() - started);
|
|
178
|
+
return timings ? { ...report, timings } : report;
|
|
111
179
|
} catch (error) {
|
|
112
180
|
try {
|
|
113
181
|
await sink.abort(error);
|
|
@@ -134,6 +202,44 @@ function throwIfAborted(signal) {
|
|
|
134
202
|
throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
135
203
|
}
|
|
136
204
|
}
|
|
205
|
+
function checkConversionTotal(current, increment, limit, code) {
|
|
206
|
+
if (increment > limit - current) {
|
|
207
|
+
throw new TileConversionError(
|
|
208
|
+
code,
|
|
209
|
+
`Conversion exceeds cumulative limit ${limit}: ${current} processed, next resource adds ${increment}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (increment > Number.MAX_SAFE_INTEGER - current) {
|
|
213
|
+
throw new TileConversionError(
|
|
214
|
+
"RESOURCE_TOTAL_OVERFLOW",
|
|
215
|
+
"Conversion totals must remain safe integers"
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function measureConversionOperation(timings, stage, operation) {
|
|
220
|
+
if (!timings) return operation();
|
|
221
|
+
const started = performance.now();
|
|
222
|
+
return (async () => {
|
|
223
|
+
try {
|
|
224
|
+
return await operation();
|
|
225
|
+
} finally {
|
|
226
|
+
timings[stage] += Math.max(0, performance.now() - started);
|
|
227
|
+
}
|
|
228
|
+
})();
|
|
229
|
+
}
|
|
230
|
+
function measureConversionIterable(iterable, timings, stage) {
|
|
231
|
+
if (!timings) return iterable;
|
|
232
|
+
return {
|
|
233
|
+
[Symbol.asyncIterator]() {
|
|
234
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
235
|
+
return {
|
|
236
|
+
next: async () => measureConversionOperation(timings, stage, () => iterator.next()),
|
|
237
|
+
...iterator.return ? { return: iterator.return.bind(iterator) } : {},
|
|
238
|
+
...iterator.throw ? { throw: iterator.throw.bind(iterator) } : {}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
137
243
|
|
|
138
244
|
// src/v5/browser-source.ts
|
|
139
245
|
function createBrowserTileConversionSource(options) {
|
|
@@ -271,7 +377,7 @@ import {
|
|
|
271
377
|
I3SSpatialTransformer,
|
|
272
378
|
markTilesetSpatialReferenceTransformed,
|
|
273
379
|
Tiles3DSpatialTransformer
|
|
274
|
-
} from "@loaders.gl/tiles";
|
|
380
|
+
} from "@loaders.gl/tiles-3d";
|
|
275
381
|
import { Matrix4 } from "@math.gl/core";
|
|
276
382
|
function createTiles3DConversionSpatialContext(discovered, options = {}) {
|
|
277
383
|
const spatialReference = createConversionSpatialReference(discovered, options);
|
|
@@ -291,8 +397,8 @@ function createTiles3DConversionSpatialContext(discovered, options = {}) {
|
|
|
291
397
|
const outputSpatialReference = transformer ? markTilesetSpatialReferenceTransformed(spatialReference) : spatialReference;
|
|
292
398
|
return {
|
|
293
399
|
spatialReference: outputSpatialReference,
|
|
294
|
-
transformPositions: (positions) => transformer ? transformer.transformPositions(positions) :
|
|
295
|
-
transformNormals: (normals, positions) => transformer ? transformer.transformNormals(normals, positions) :
|
|
400
|
+
transformPositions: (positions) => transformer ? transformer.transformPositions(positions) : copyNativePositions(positions),
|
|
401
|
+
transformNormals: (normals, positions) => transformer ? transformer.transformNormals(normals, positions) : copyNativeNormals(normals, positions),
|
|
296
402
|
transformBoundingVolume: (volume, sourceTransform) => transformer ? transformer.transformBoundingVolume(volume, sourceTransform) : cloneBoundingVolume(volume)
|
|
297
403
|
};
|
|
298
404
|
}
|
|
@@ -314,16 +420,9 @@ function createI3SConversionSpatialContext(discovered, options = {}) {
|
|
|
314
420
|
const outputSpatialReference = transformer ? transformer.spatialReference : spatialReference;
|
|
315
421
|
let nativeNormalTransformer;
|
|
316
422
|
const transformNativeNormals = (normals, positions, normalReferenceFrame) => {
|
|
423
|
+
validateNormalReferenceFrame(normalReferenceFrame);
|
|
424
|
+
const targetNormals = copyNativeNormals(normals, positions);
|
|
317
425
|
if (!spatialReference.sourceCrs || ["local", "cartesian"].includes(spatialReference.coordinateFrame)) {
|
|
318
|
-
const targetNormals = Float32Array.from(normals);
|
|
319
|
-
for (let index = 0; index < normals.length; index += 3) {
|
|
320
|
-
const length = Math.hypot(normals[index], normals[index + 1], normals[index + 2]);
|
|
321
|
-
if (length) {
|
|
322
|
-
for (let axis = 0; axis < 3; axis++) {
|
|
323
|
-
targetNormals[index + axis] = normals[index + axis] / length;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
426
|
return targetNormals;
|
|
328
427
|
}
|
|
329
428
|
nativeNormalTransformer ||= createSpatialTransformer(
|
|
@@ -337,7 +436,7 @@ function createI3SConversionSpatialContext(discovered, options = {}) {
|
|
|
337
436
|
options
|
|
338
437
|
)
|
|
339
438
|
);
|
|
340
|
-
return nativeNormalTransformer.transformNormals(
|
|
439
|
+
return nativeNormalTransformer.transformNormals(targetNormals, positions, normalReferenceFrame);
|
|
341
440
|
};
|
|
342
441
|
return {
|
|
343
442
|
spatialReference: outputSpatialReference,
|
|
@@ -358,7 +457,7 @@ function createI3SConversionSpatialContext(discovered, options = {}) {
|
|
|
358
457
|
};
|
|
359
458
|
},
|
|
360
459
|
transformPositionsAsync: (positions, sourceOrigin) => transformer ? transformer.transformPositionsAsync(positions, sourceOrigin) : Promise.resolve(createNativeI3SPositions(positions, sourceOrigin, spatialReference)),
|
|
361
|
-
transformNormals: (normals, positions, normalReferenceFrame) => transformer ? transformer.transformNormals(normals, positions, normalReferenceFrame) :
|
|
460
|
+
transformNormals: (normals, positions, normalReferenceFrame) => transformer ? transformer.transformNormals(normals, positions, normalReferenceFrame) : transformNativeNormals(normals, positions, normalReferenceFrame),
|
|
362
461
|
transformBoundingVolume: (bounds) => transformer ? transformer.transformBoundingVolume(bounds) : cloneI3SBounds(bounds),
|
|
363
462
|
transformBoundingVolumeAsync: (bounds) => transformer ? transformer.transformBoundingVolumeAsync(bounds) : Promise.resolve(cloneI3SBounds(bounds))
|
|
364
463
|
};
|
|
@@ -442,21 +541,30 @@ function cloneI3SBounds(bounds) {
|
|
|
442
541
|
};
|
|
443
542
|
}
|
|
444
543
|
function createNativeI3SPositions(sourcePositions, sourceOrigin, spatialReference) {
|
|
445
|
-
|
|
446
|
-
|
|
544
|
+
const sourcePositions64 = copyNativePositions(sourcePositions);
|
|
545
|
+
if (sourceOrigin.length !== 3 || Array.from(sourceOrigin).some((value) => !Number.isFinite(value))) {
|
|
546
|
+
throw new TileConversionError(
|
|
547
|
+
"SPATIAL_INPUT_INVALID",
|
|
548
|
+
"I3S origins require three finite coordinates"
|
|
549
|
+
);
|
|
447
550
|
}
|
|
448
551
|
const origin = [
|
|
449
552
|
Number(sourceOrigin[0]),
|
|
450
553
|
Number(sourceOrigin[1]),
|
|
451
554
|
Number(sourceOrigin[2])
|
|
452
555
|
];
|
|
453
|
-
const sourcePositions64 = Float64Array.from(sourcePositions);
|
|
454
556
|
const positions = new Float32Array(sourcePositions.length);
|
|
455
557
|
for (let index = 0; index < sourcePositions.length; index += 3) {
|
|
456
558
|
positions[index] = Number(sourcePositions[index]) - origin[0];
|
|
457
559
|
positions[index + 1] = Number(sourcePositions[index + 1]) - origin[1];
|
|
458
560
|
positions[index + 2] = Number(sourcePositions[index + 2]) - origin[2];
|
|
459
561
|
}
|
|
562
|
+
if (positions.some((value) => !Number.isFinite(value))) {
|
|
563
|
+
throw new TileConversionError(
|
|
564
|
+
"SPATIAL_INPUT_INVALID",
|
|
565
|
+
"Native I3S offsets exceed float32 range"
|
|
566
|
+
);
|
|
567
|
+
}
|
|
460
568
|
const isGeographic = spatialReference.coordinateFrame === "geographic";
|
|
461
569
|
const modelMatrix = isGeographic ? new Matrix4() : new Matrix4().translate(origin);
|
|
462
570
|
return {
|
|
@@ -468,9 +576,64 @@ function createNativeI3SPositions(sourcePositions, sourceOrigin, spatialReferenc
|
|
|
468
576
|
modelMatrix
|
|
469
577
|
};
|
|
470
578
|
}
|
|
579
|
+
function copyNativePositions(positions) {
|
|
580
|
+
validateNativePositions(positions);
|
|
581
|
+
return Float64Array.from(positions);
|
|
582
|
+
}
|
|
583
|
+
function validateNativePositions(positions) {
|
|
584
|
+
if (positions.length % 3) {
|
|
585
|
+
throw new TileConversionError(
|
|
586
|
+
"SPATIAL_INPUT_INVALID",
|
|
587
|
+
"Positions require complete finite xyz coordinates"
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
for (let index = 0; index < positions.length; index++) {
|
|
591
|
+
if (!Number.isFinite(positions[index])) {
|
|
592
|
+
throw new TileConversionError(
|
|
593
|
+
"SPATIAL_INPUT_INVALID",
|
|
594
|
+
"Positions require complete finite xyz coordinates"
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function copyNativeNormals(normals, positions) {
|
|
600
|
+
validateNativePositions(positions);
|
|
601
|
+
if (normals.length !== positions.length) {
|
|
602
|
+
throw new TileConversionError(
|
|
603
|
+
"SPATIAL_INPUT_INVALID",
|
|
604
|
+
"Normals require finite xyz values matching positions"
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const output = new Float32Array(normals.length);
|
|
608
|
+
for (let index = 0; index < normals.length; index += 3) {
|
|
609
|
+
const scale = Math.max(
|
|
610
|
+
Math.abs(normals[index]),
|
|
611
|
+
Math.abs(normals[index + 1]),
|
|
612
|
+
Math.abs(normals[index + 2])
|
|
613
|
+
);
|
|
614
|
+
if (!Number.isFinite(scale)) {
|
|
615
|
+
throw new TileConversionError(
|
|
616
|
+
"SPATIAL_INPUT_INVALID",
|
|
617
|
+
"Normals require finite xyz values matching positions"
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
const length = scale ? Math.hypot(normals[index] / scale, normals[index + 1] / scale, normals[index + 2] / scale) : 0;
|
|
621
|
+
for (let axis = 0; axis < 3; axis++)
|
|
622
|
+
output[index + axis] = length ? normals[index + axis] / scale / length : 0;
|
|
623
|
+
}
|
|
624
|
+
return output;
|
|
625
|
+
}
|
|
626
|
+
function validateNormalReferenceFrame(frame) {
|
|
627
|
+
if (frame !== void 0 && !["earth-centered", "vertex-reference-frame"].includes(frame)) {
|
|
628
|
+
throw new TileConversionError(
|
|
629
|
+
"SPATIAL_INPUT_INVALID",
|
|
630
|
+
"Unsupported I3S normal reference frame"
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
471
634
|
|
|
472
635
|
// src/v5/tileset-source.ts
|
|
473
|
-
import { traverseTilesetContents } from "@loaders.gl/tiles";
|
|
636
|
+
import { traverseTilesetContents } from "@loaders.gl/tiles-3d";
|
|
474
637
|
function createTilesetConversionSource(tileset, options = {}) {
|
|
475
638
|
return {
|
|
476
639
|
async inspect(signal) {
|
|
@@ -492,10 +655,17 @@ function createTilesetConversionSource(tileset, options = {}) {
|
|
|
492
655
|
// src/v5/resource-manifest.ts
|
|
493
656
|
var TILE_RESOURCE_MANIFEST_VERSION = 1;
|
|
494
657
|
async function createManifestBackedTileConversionSink(options) {
|
|
658
|
+
const maxResources = options.maxResources ?? Infinity;
|
|
659
|
+
if (maxResources !== Infinity && (!Number.isSafeInteger(maxResources) || maxResources < 0)) {
|
|
660
|
+
throw new TileConversionError(
|
|
661
|
+
"INVALID_MANIFEST_RESOURCE_LIMIT",
|
|
662
|
+
"maxResources must be a nonnegative safe integer or Infinity"
|
|
663
|
+
);
|
|
664
|
+
}
|
|
495
665
|
const priorManifest = await options.manifestStore.load();
|
|
496
666
|
const resources = /* @__PURE__ */ new Map();
|
|
497
667
|
if (priorManifest) {
|
|
498
|
-
validateManifest(priorManifest);
|
|
668
|
+
validateManifest(priorManifest, maxResources);
|
|
499
669
|
for (const entry of priorManifest.resources) {
|
|
500
670
|
resources.set(entry.resourceId, entry);
|
|
501
671
|
}
|
|
@@ -521,6 +691,7 @@ async function createManifestBackedTileConversionSink(options) {
|
|
|
521
691
|
if (writeFailed) {
|
|
522
692
|
throw writeFailure;
|
|
523
693
|
}
|
|
694
|
+
signal?.throwIfAborted();
|
|
524
695
|
const entry = createManifestEntry(options, resource);
|
|
525
696
|
const priorEntry = resources.get(entry.resourceId);
|
|
526
697
|
if (priorEntry) {
|
|
@@ -530,7 +701,14 @@ async function createManifestBackedTileConversionSink(options) {
|
|
|
530
701
|
`Resource "${entry.resourceId}" changed since the saved manifest`
|
|
531
702
|
);
|
|
532
703
|
}
|
|
533
|
-
|
|
704
|
+
const verified = options.verifyResource ? await options.verifyResource({ ...priorEntry }, signal) : true;
|
|
705
|
+
signal?.throwIfAborted();
|
|
706
|
+
if (verified === true) return;
|
|
707
|
+
} else if (resources.size >= maxResources) {
|
|
708
|
+
throw new TileConversionError(
|
|
709
|
+
"MANIFEST_RESOURCE_LIMIT_EXCEEDED",
|
|
710
|
+
"Resource checkpoint exceeds maxResources"
|
|
711
|
+
);
|
|
534
712
|
}
|
|
535
713
|
await options.sink.write(resource, signal);
|
|
536
714
|
resources.set(entry.resourceId, entry);
|
|
@@ -579,11 +757,17 @@ function createManifestEntry(options, resource) {
|
|
|
579
757
|
}
|
|
580
758
|
return { resourceId, byteLength, fingerprint };
|
|
581
759
|
}
|
|
582
|
-
function validateManifest(manifest) {
|
|
760
|
+
function validateManifest(manifest, maxResources) {
|
|
583
761
|
if (typeof manifest !== "object" || manifest === null || manifest.version !== TILE_RESOURCE_MANIFEST_VERSION || typeof manifest.complete !== "boolean" || !Array.isArray(manifest.resources)) {
|
|
584
762
|
throw new TileConversionError(
|
|
585
763
|
"UNSUPPORTED_RESOURCE_MANIFEST",
|
|
586
|
-
`Unsupported or malformed resource manifest version: ${String(manifest
|
|
764
|
+
`Unsupported or malformed resource manifest version: ${String(manifest?.version)}`
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
if (manifest.resources.length > maxResources) {
|
|
768
|
+
throw new TileConversionError(
|
|
769
|
+
"MANIFEST_RESOURCE_LIMIT_EXCEEDED",
|
|
770
|
+
"Loaded manifest exceeds maxResources"
|
|
587
771
|
);
|
|
588
772
|
}
|
|
589
773
|
const resourceIds = /* @__PURE__ */ new Set();
|
|
@@ -675,50 +859,67 @@ function measureBlobPart(part) {
|
|
|
675
859
|
|
|
676
860
|
// src/v5/browser-tileset-source.ts
|
|
677
861
|
function createBrowserTilesetConversionSource(options) {
|
|
678
|
-
|
|
862
|
+
const maxTilesetDocuments = options.maxTilesetDocuments ?? 1;
|
|
863
|
+
if (options.inspectNestedTilesets !== void 0 && typeof options.inspectNestedTilesets !== "boolean" || !Number.isSafeInteger(maxTilesetDocuments) || maxTilesetDocuments < 1 || !Number.isSafeInteger(options.maxInputBytes) || options.maxInputBytes < 1 || !Number.isSafeInteger(options.maxInputResources) || options.maxInputResources < 1) {
|
|
679
864
|
throw new TileConversionError(
|
|
680
865
|
"INVALID_BROWSER_SOURCE_OPTIONS",
|
|
681
|
-
"Browser tileset sources require positive byte and
|
|
866
|
+
"Browser tileset sources require positive byte, declaration and document limits"
|
|
682
867
|
);
|
|
683
868
|
}
|
|
684
869
|
const fetcher = options.fetcher ?? fetch;
|
|
685
870
|
return {
|
|
686
871
|
async inspect(signal) {
|
|
687
872
|
throwIfAborted4(signal);
|
|
688
|
-
const
|
|
689
|
-
const response = await fetcher(
|
|
873
|
+
const requestedUrl = new URL(options.input).href;
|
|
874
|
+
const response = await fetcher(requestedUrl, { signal });
|
|
875
|
+
const rootUrl = response.url || requestedUrl;
|
|
690
876
|
if (!response.ok) {
|
|
877
|
+
await response.body?.cancel();
|
|
691
878
|
throw new TileConversionError(
|
|
692
879
|
"BROWSER_SOURCE_FETCH_FAILED",
|
|
693
880
|
`Could not fetch browser tileset (${response.status} ${response.statusText})`
|
|
694
881
|
);
|
|
695
882
|
}
|
|
696
883
|
const data = await readResponseBytes2(response, options.maxInputBytes, signal);
|
|
697
|
-
|
|
698
|
-
try {
|
|
699
|
-
tileset = JSON.parse(new TextDecoder().decode(data));
|
|
700
|
-
} catch {
|
|
701
|
-
throw new TileConversionError("INVALID_BROWSER_TILESET", "Tileset input is not valid JSON");
|
|
702
|
-
}
|
|
703
|
-
if (!isRecord(tileset) || !["1.0", "1.1"].includes(tileset.asset?.version || "") || !isRecord(tileset.root)) {
|
|
704
|
-
throw new TileConversionError(
|
|
705
|
-
"UNSUPPORTED_BROWSER_TILESET",
|
|
706
|
-
"Browser tileset traversal supports explicit 3D Tiles 1.0 and 1.1 documents"
|
|
707
|
-
);
|
|
708
|
-
}
|
|
884
|
+
const tileset = parseTilesetDocument(data);
|
|
709
885
|
const resources = collectResources(tileset.root, options.maxInputResources);
|
|
710
|
-
|
|
886
|
+
const inspection = { rootUrl, tileset, resources, tilesetBytes: data.byteLength };
|
|
887
|
+
return options.inspectNestedTilesets ? expandInspection(inspection, options, maxTilesetDocuments, fetcher, signal) : inspection;
|
|
711
888
|
},
|
|
712
889
|
async *read(inspection, signal) {
|
|
713
890
|
let inputBytes = inspection.tilesetBytes;
|
|
714
|
-
|
|
891
|
+
let declarations = inspection.resources.length;
|
|
892
|
+
let documents = 1;
|
|
893
|
+
const stack = [
|
|
894
|
+
{
|
|
895
|
+
resources: inspection.resources,
|
|
896
|
+
index: 0,
|
|
897
|
+
tileset: inspection.tileset,
|
|
898
|
+
url: inspection.rootUrl,
|
|
899
|
+
ancestors: /* @__PURE__ */ new Set([getDocumentKey(inspection.rootUrl), getDocumentKey(options.input)])
|
|
900
|
+
}
|
|
901
|
+
];
|
|
902
|
+
while (stack.length) {
|
|
715
903
|
throwIfAborted4(signal);
|
|
716
|
-
const
|
|
904
|
+
const frame = stack[stack.length - 1];
|
|
905
|
+
if (frame.index === frame.resources.length) {
|
|
906
|
+
stack.pop();
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
const descriptor = frame.resources[frame.index++];
|
|
910
|
+
const resourceUrl = resolveResourceUrl(descriptor.uri, descriptor.tilesetUrl ?? frame.url);
|
|
911
|
+
if (frame.ancestors.has(getDocumentKey(resourceUrl))) {
|
|
912
|
+
throw new TileConversionError(
|
|
913
|
+
"BROWSER_TILESET_CYCLE",
|
|
914
|
+
"External tileset references an active ancestor document"
|
|
915
|
+
);
|
|
916
|
+
}
|
|
717
917
|
const response = await fetcher(resourceUrl, { signal });
|
|
718
918
|
if (!response.ok) {
|
|
919
|
+
await response.body?.cancel();
|
|
719
920
|
throw new TileConversionError(
|
|
720
921
|
"BROWSER_SOURCE_FETCH_FAILED",
|
|
721
|
-
|
|
922
|
+
"Could not fetch browser tile content (" + response.status + " " + response.statusText + ")",
|
|
722
923
|
[
|
|
723
924
|
{
|
|
724
925
|
code: "BROWSER_SOURCE_FETCH_FAILED",
|
|
@@ -729,28 +930,280 @@ function createBrowserTilesetConversionSource(options) {
|
|
|
729
930
|
]
|
|
730
931
|
);
|
|
731
932
|
}
|
|
732
|
-
const
|
|
733
|
-
|
|
933
|
+
const documentUrl = response.url || resourceUrl;
|
|
934
|
+
if (frame.ancestors.has(getDocumentKey(documentUrl))) {
|
|
935
|
+
await response.body?.cancel();
|
|
936
|
+
throw new TileConversionError(
|
|
937
|
+
"BROWSER_TILESET_CYCLE",
|
|
938
|
+
"Redirected content references an active ancestor document"
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
const data = await readResponseBytes2(response, options.maxInputBytes - inputBytes, signal);
|
|
734
942
|
inputBytes += data.byteLength;
|
|
735
943
|
const contentType = response.headers.get("content-type") ?? void 0;
|
|
736
944
|
if (isJsonContent(data, contentType)) {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
945
|
+
if (inspection.documents)
|
|
946
|
+
throw new TileConversionError(
|
|
947
|
+
"UNSUPPORTED_BROWSER_TILESET_CONTENT",
|
|
948
|
+
"Expanded inspection selections must be binary content; declare external metadata with a .json URI"
|
|
949
|
+
);
|
|
950
|
+
if (maxTilesetDocuments === 1) {
|
|
951
|
+
throw new TileConversionError(
|
|
952
|
+
"UNSUPPORTED_BROWSER_TILESET_CONTENT",
|
|
953
|
+
"Nested tilesets and JSON tile content require an explicit document budget"
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
if (documents >= maxTilesetDocuments) {
|
|
957
|
+
throw new TileConversionError(
|
|
958
|
+
"INPUT_TILESET_COUNT_EXCEEDED",
|
|
959
|
+
"Browser tileset exceeds its document limit"
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
let tile = frame.tileset.root;
|
|
963
|
+
for (const childIndex of descriptor.tilePath) tile = getTileChild(tile, childIndex);
|
|
964
|
+
if (tile.children?.length) {
|
|
965
|
+
throw new TileConversionError(
|
|
966
|
+
"INVALID_BROWSER_TILESET",
|
|
967
|
+
"External tileset references cannot also have child tiles"
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
const tileset = parseTilesetDocument(data);
|
|
971
|
+
const resources = collectResources(
|
|
972
|
+
tileset.root,
|
|
973
|
+
options.maxInputResources - declarations,
|
|
974
|
+
descriptor.transform
|
|
975
|
+
).map((resource) => ({
|
|
976
|
+
...resource,
|
|
977
|
+
resourceId: descriptor.resourceId + "/" + resource.resourceId,
|
|
978
|
+
tilesetUrl: documentUrl
|
|
979
|
+
}));
|
|
980
|
+
declarations += resources.length;
|
|
981
|
+
documents++;
|
|
982
|
+
stack.push({
|
|
983
|
+
resources,
|
|
984
|
+
index: 0,
|
|
985
|
+
tileset,
|
|
986
|
+
url: documentUrl,
|
|
987
|
+
ancestors: /* @__PURE__ */ new Set([
|
|
988
|
+
...frame.ancestors,
|
|
989
|
+
getDocumentKey(resourceUrl),
|
|
990
|
+
getDocumentKey(documentUrl)
|
|
991
|
+
])
|
|
992
|
+
});
|
|
993
|
+
continue;
|
|
741
994
|
}
|
|
742
|
-
yield { ...descriptor, data, ...contentType ? { contentType } : {} };
|
|
995
|
+
yield { ...descriptor, data, contentUrl: documentUrl, ...contentType ? { contentType } : {} };
|
|
743
996
|
}
|
|
744
997
|
}
|
|
745
998
|
};
|
|
746
999
|
}
|
|
747
|
-
function
|
|
1000
|
+
function selectBrowserTilesetContent(inspection, descriptor) {
|
|
1001
|
+
const placement = inspection.documents?.[descriptor.documentIndex ?? 0];
|
|
1002
|
+
if (inspection.documents && (!Number.isSafeInteger(descriptor.documentIndex) || !placement))
|
|
1003
|
+
throw new TileConversionError(
|
|
1004
|
+
"INVALID_BROWSER_SELECTION",
|
|
1005
|
+
"Content descriptor has no inspected document placement"
|
|
1006
|
+
);
|
|
1007
|
+
const document = structuredClone(placement?.tileset ?? inspection.tileset);
|
|
1008
|
+
const originalRoot = document.root;
|
|
1009
|
+
let transform = [...placement?.parentTransform ?? identityMatrix()];
|
|
1010
|
+
let maximum = placement?.parentGeometricError ?? 0;
|
|
1011
|
+
let tile = originalRoot;
|
|
1012
|
+
for (let depth = 0; ; depth++) {
|
|
1013
|
+
transform = multiplyMatrices(transform, normalizeMatrix(tile.transform));
|
|
1014
|
+
if (typeof tile.geometricError !== "number" || !Number.isFinite(tile.geometricError) || tile.geometricError < 0)
|
|
1015
|
+
throw new TileConversionError(
|
|
1016
|
+
"MESH_GEOMETRIC_ERROR_INVALID",
|
|
1017
|
+
"Every selected ancestor must declare a finite nonnegative geometricError"
|
|
1018
|
+
);
|
|
1019
|
+
maximum = Math.max(maximum, tile.geometricError * getTransformScale(transform));
|
|
1020
|
+
if (depth === descriptor.tilePath.length) break;
|
|
1021
|
+
const child = getTileChild(tile, descriptor.tilePath[depth]);
|
|
1022
|
+
delete tile.content;
|
|
1023
|
+
delete tile.contents;
|
|
1024
|
+
tile.children = [child];
|
|
1025
|
+
tile = child;
|
|
1026
|
+
}
|
|
1027
|
+
if (placement && placement.parentGeometricError === void 0 && descriptor.documentIndex !== 0)
|
|
1028
|
+
throw new TileConversionError(
|
|
1029
|
+
"MESH_GEOMETRIC_ERROR_INVALID",
|
|
1030
|
+
"External ancestors must declare geometricError"
|
|
1031
|
+
);
|
|
1032
|
+
const isLeaf = !tile.children?.length;
|
|
1033
|
+
const contentIndex = Number(descriptor.resourceId.match(/content-(\d+)$/)?.[1]);
|
|
1034
|
+
const content = tile.contents?.[contentIndex] ?? tile.content;
|
|
1035
|
+
if (!content || (content.uri ?? content.url) !== descriptor.uri)
|
|
1036
|
+
throw new TileConversionError(
|
|
1037
|
+
"INVALID_BROWSER_SELECTION",
|
|
1038
|
+
"Content descriptor does not match its inspected document"
|
|
1039
|
+
);
|
|
1040
|
+
tile.content = content;
|
|
1041
|
+
delete tile.contents;
|
|
1042
|
+
delete tile.children;
|
|
1043
|
+
if (placement)
|
|
1044
|
+
originalRoot.transform = multiplyMatrices(
|
|
1045
|
+
[...placement.parentTransform],
|
|
1046
|
+
normalizeMatrix(originalRoot.transform)
|
|
1047
|
+
);
|
|
1048
|
+
if (!Number.isFinite(maximum))
|
|
1049
|
+
throw new TileConversionError(
|
|
1050
|
+
"MESH_GEOMETRIC_ERROR_INVALID",
|
|
1051
|
+
"Scaled geometricError must remain finite"
|
|
1052
|
+
);
|
|
1053
|
+
return {
|
|
1054
|
+
tileset: document,
|
|
1055
|
+
contentUrl: resolveResourceUrl(descriptor.uri, placement?.url ?? inspection.rootUrl),
|
|
1056
|
+
geometricError: maximum,
|
|
1057
|
+
isLeaf
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
function getTileChild(tile, childIndex) {
|
|
1061
|
+
if (typeof childIndex !== "number" || !Number.isSafeInteger(childIndex) || childIndex < 0 || !Array.isArray(tile.children) || childIndex >= tile.children.length)
|
|
1062
|
+
throw new TileConversionError(
|
|
1063
|
+
"INVALID_BROWSER_SELECTION",
|
|
1064
|
+
"Tile paths require in-bounds nonnegative integer child indexes"
|
|
1065
|
+
);
|
|
1066
|
+
const child = tile.children.at(childIndex);
|
|
1067
|
+
if (!isRecord(child))
|
|
1068
|
+
throw new TileConversionError(
|
|
1069
|
+
"INVALID_BROWSER_SELECTION",
|
|
1070
|
+
"Selected child tile must be an object"
|
|
1071
|
+
);
|
|
1072
|
+
return child;
|
|
1073
|
+
}
|
|
1074
|
+
async function expandInspection(inspection, options, maxTilesetDocuments, fetcher, signal) {
|
|
1075
|
+
const documents = [
|
|
1076
|
+
{
|
|
1077
|
+
url: inspection.rootUrl,
|
|
1078
|
+
tileset: inspection.tileset,
|
|
1079
|
+
parentTransform: identityMatrix(),
|
|
1080
|
+
parentGeometricError: 0
|
|
1081
|
+
}
|
|
1082
|
+
];
|
|
748
1083
|
const resources = [];
|
|
1084
|
+
let bytes = inspection.tilesetBytes;
|
|
1085
|
+
let declarations = inspection.resources.length;
|
|
749
1086
|
const stack = [
|
|
750
|
-
{
|
|
1087
|
+
{
|
|
1088
|
+
resources: inspection.resources,
|
|
1089
|
+
index: 0,
|
|
1090
|
+
documentIndex: 0,
|
|
1091
|
+
ancestors: /* @__PURE__ */ new Set([getDocumentKey(inspection.rootUrl), getDocumentKey(options.input)])
|
|
1092
|
+
}
|
|
1093
|
+
];
|
|
1094
|
+
while (stack.length) {
|
|
1095
|
+
throwIfAborted4(signal);
|
|
1096
|
+
const frame = stack[stack.length - 1];
|
|
1097
|
+
if (frame.index === frame.resources.length) {
|
|
1098
|
+
stack.pop();
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
const descriptor = frame.resources[frame.index++];
|
|
1102
|
+
const placement = documents[frame.documentIndex];
|
|
1103
|
+
const resourceUrl = resolveResourceUrl(descriptor.uri, placement.url);
|
|
1104
|
+
if (!new URL(resourceUrl).pathname.toLowerCase().endsWith(".json")) {
|
|
1105
|
+
resources.push({ ...descriptor, documentIndex: frame.documentIndex });
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
if (frame.ancestors.has(getDocumentKey(resourceUrl)))
|
|
1109
|
+
throw new TileConversionError(
|
|
1110
|
+
"BROWSER_TILESET_CYCLE",
|
|
1111
|
+
"External tileset references an active ancestor document"
|
|
1112
|
+
);
|
|
1113
|
+
if (documents.length >= maxTilesetDocuments)
|
|
1114
|
+
throw new TileConversionError(
|
|
1115
|
+
"INPUT_TILESET_COUNT_EXCEEDED",
|
|
1116
|
+
"Browser tileset exceeds its document limit"
|
|
1117
|
+
);
|
|
1118
|
+
let tile = placement.tileset.root;
|
|
1119
|
+
for (const childIndex of descriptor.tilePath) tile = tile.children[childIndex];
|
|
1120
|
+
if (tile.children?.length || tile.contents?.length)
|
|
1121
|
+
throw new TileConversionError(
|
|
1122
|
+
"INVALID_BROWSER_TILESET",
|
|
1123
|
+
"Inspected external tilesets require one content and no child tiles"
|
|
1124
|
+
);
|
|
1125
|
+
const response = await fetcher(resourceUrl, { signal });
|
|
1126
|
+
const url = response.url || resourceUrl;
|
|
1127
|
+
if (!response.ok || frame.ancestors.has(getDocumentKey(url))) {
|
|
1128
|
+
await response.body?.cancel();
|
|
1129
|
+
throw new TileConversionError(
|
|
1130
|
+
response.ok ? "BROWSER_TILESET_CYCLE" : "BROWSER_SOURCE_FETCH_FAILED",
|
|
1131
|
+
"Could not inspect external tileset"
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
const data = await readResponseBytes2(response, options.maxInputBytes - bytes, signal);
|
|
1135
|
+
bytes += data.byteLength;
|
|
1136
|
+
const tileset = parseTilesetDocument(data);
|
|
1137
|
+
const prefix = descriptor.resourceId + "/";
|
|
1138
|
+
const nestedResources = collectResources(
|
|
1139
|
+
tileset.root,
|
|
1140
|
+
options.maxInputResources - declarations,
|
|
1141
|
+
descriptor.transform
|
|
1142
|
+
).map((resource) => ({ ...resource, resourceId: prefix + resource.resourceId, tilesetUrl: url }));
|
|
1143
|
+
declarations += nestedResources.length;
|
|
1144
|
+
let parentGeometricError;
|
|
1145
|
+
try {
|
|
1146
|
+
parentGeometricError = selectBrowserTilesetContent(
|
|
1147
|
+
{ ...inspection, documents },
|
|
1148
|
+
{ ...descriptor, documentIndex: frame.documentIndex }
|
|
1149
|
+
).geometricError;
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
if (!(error instanceof TileConversionError) || error.code !== "MESH_GEOMETRIC_ERROR_INVALID")
|
|
1152
|
+
throw error;
|
|
1153
|
+
}
|
|
1154
|
+
const documentIndex = documents.length;
|
|
1155
|
+
documents.push({
|
|
1156
|
+
url,
|
|
1157
|
+
tileset,
|
|
1158
|
+
parentResourceId: descriptor.resourceId,
|
|
1159
|
+
parentTransform: descriptor.transform,
|
|
1160
|
+
parentGeometricError
|
|
1161
|
+
});
|
|
1162
|
+
stack.push({
|
|
1163
|
+
resources: nestedResources,
|
|
1164
|
+
index: 0,
|
|
1165
|
+
documentIndex,
|
|
1166
|
+
ancestors: /* @__PURE__ */ new Set([...frame.ancestors, getDocumentKey(resourceUrl), getDocumentKey(url)])
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
return { ...inspection, resources, documents, tilesetBytes: bytes };
|
|
1170
|
+
}
|
|
1171
|
+
function getTransformScale(transform) {
|
|
1172
|
+
const columns = [0, 1, 2].map(
|
|
1173
|
+
(column) => [0, 1, 2].reduce((total, row) => total + Math.abs(transform[column * 4 + row]), 0)
|
|
1174
|
+
);
|
|
1175
|
+
const rows = [0, 1, 2].map(
|
|
1176
|
+
(row) => [0, 1, 2].reduce((total, column) => total + Math.abs(transform[column * 4 + row]), 0)
|
|
1177
|
+
);
|
|
1178
|
+
return Math.sqrt(Math.max(...columns)) * Math.sqrt(Math.max(...rows));
|
|
1179
|
+
}
|
|
1180
|
+
function parseTilesetDocument(data) {
|
|
1181
|
+
let tileset;
|
|
1182
|
+
try {
|
|
1183
|
+
tileset = JSON.parse(new TextDecoder().decode(data));
|
|
1184
|
+
} catch {
|
|
1185
|
+
throw new TileConversionError("INVALID_BROWSER_TILESET", "Tileset input is not valid JSON");
|
|
1186
|
+
}
|
|
1187
|
+
if (!isRecord(tileset) || !["1.0", "1.1"].includes(tileset.asset?.version || "") || !isRecord(tileset.root)) {
|
|
1188
|
+
throw new TileConversionError(
|
|
1189
|
+
"UNSUPPORTED_BROWSER_TILESET",
|
|
1190
|
+
"Browser tileset traversal supports explicit 3D Tiles 1.0 and 1.1 documents"
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
return tileset;
|
|
1194
|
+
}
|
|
1195
|
+
function getDocumentKey(url) {
|
|
1196
|
+
const documentUrl = new URL(url);
|
|
1197
|
+
documentUrl.hash = "";
|
|
1198
|
+
return documentUrl.href;
|
|
1199
|
+
}
|
|
1200
|
+
function collectResources(root, maxInputResources, parentTransform = identityMatrix()) {
|
|
1201
|
+
const resources = [];
|
|
1202
|
+
const stack = [
|
|
1203
|
+
{ tile: root, path: [], transform: [...parentTransform] }
|
|
751
1204
|
];
|
|
752
1205
|
while (stack.length > 0) {
|
|
753
|
-
const { tile, path, transform:
|
|
1206
|
+
const { tile, path, transform: parentTransform2 } = stack.pop();
|
|
754
1207
|
if (!isRecord(tile)) {
|
|
755
1208
|
throw new TileConversionError("INVALID_BROWSER_TILESET", "Tile entries must be objects");
|
|
756
1209
|
}
|
|
@@ -766,7 +1219,19 @@ function collectResources(root, maxInputResources) {
|
|
|
766
1219
|
if (tile.children !== void 0 && !Array.isArray(tile.children)) {
|
|
767
1220
|
throw new TileConversionError("INVALID_BROWSER_TILESET", "Tile children must be an array");
|
|
768
1221
|
}
|
|
769
|
-
|
|
1222
|
+
if (tile.content !== void 0 && tile.contents !== void 0) {
|
|
1223
|
+
throw new TileConversionError(
|
|
1224
|
+
"INVALID_BROWSER_TILESET",
|
|
1225
|
+
"Tile content and contents are mutually exclusive"
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
const transform = multiplyMatrices(parentTransform2, normalizeMatrix(tile.transform));
|
|
1229
|
+
if (transform.some((value) => !Number.isFinite(value))) {
|
|
1230
|
+
throw new TileConversionError(
|
|
1231
|
+
"INVALID_BROWSER_TILESET",
|
|
1232
|
+
"Composed tile transforms must remain finite"
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
770
1235
|
const contents = tile.contents ?? (tile.content ? [tile.content] : []);
|
|
771
1236
|
for (let contentIndex = 0; contentIndex < contents.length; contentIndex++) {
|
|
772
1237
|
const content = contents[contentIndex];
|
|
@@ -815,15 +1280,15 @@ async function readResponseBytes2(response, maximumBytes, signal) {
|
|
|
815
1280
|
"Browser inputs must provide a readable response stream"
|
|
816
1281
|
);
|
|
817
1282
|
}
|
|
818
|
-
const data = new Uint8Array(maximumBytes);
|
|
819
1283
|
let byteLength = 0;
|
|
820
1284
|
try {
|
|
1285
|
+
const data = new Uint8Array(maximumBytes);
|
|
821
1286
|
while (true) {
|
|
822
1287
|
throwIfAborted4(signal);
|
|
823
1288
|
const { done, value } = await reader.read();
|
|
1289
|
+
throwIfAborted4(signal);
|
|
824
1290
|
if (done) break;
|
|
825
1291
|
if (value.byteLength > maximumBytes - byteLength) {
|
|
826
|
-
await reader.cancel();
|
|
827
1292
|
throw new TileConversionError(
|
|
828
1293
|
"INPUT_RESOURCE_TOO_LARGE",
|
|
829
1294
|
`Browser tileset input exceeds the configured ${maximumBytes} byte remaining limit`
|
|
@@ -832,10 +1297,14 @@ async function readResponseBytes2(response, maximumBytes, signal) {
|
|
|
832
1297
|
data.set(value, byteLength);
|
|
833
1298
|
byteLength += value.byteLength;
|
|
834
1299
|
}
|
|
1300
|
+
return data.slice(0, byteLength);
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
await reader.cancel().catch(() => {
|
|
1303
|
+
});
|
|
1304
|
+
throw error;
|
|
835
1305
|
} finally {
|
|
836
1306
|
reader.releaseLock();
|
|
837
1307
|
}
|
|
838
|
-
return data.subarray(0, byteLength);
|
|
839
1308
|
}
|
|
840
1309
|
function isJsonContent(data, contentType) {
|
|
841
1310
|
if (contentType?.includes("json")) return true;
|
|
@@ -894,6 +1363,7 @@ export {
|
|
|
894
1363
|
createTiles3DConversionSpatialContext,
|
|
895
1364
|
createTilesetConversionSource,
|
|
896
1365
|
inspectTileset,
|
|
1366
|
+
selectBrowserTilesetContent,
|
|
897
1367
|
traversePointCloudSource,
|
|
898
1368
|
validateTileset
|
|
899
1369
|
};
|