@loaders.gl/tile-converter 5.0.0-alpha.10 → 5.0.0-alpha.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -2
- package/bin/converter-v5.js +2 -0
- package/dist/converter.min.cjs +162 -163
- package/dist/i3s-server/bin/i3s-server.min.cjs +73 -73
- package/dist/index.cjs +1933 -2232
- package/dist/index.js +1925 -2235
- package/dist/v5/adapters.cjs +3186 -208
- package/dist/v5/adapters.d.ts +29 -2
- package/dist/v5/adapters.js +3202 -196
- package/dist/v5/archive-source.d.ts +36 -0
- package/dist/v5/browser-tileset-source.d.ts +55 -11
- package/dist/v5/browser.cjs +30 -0
- package/dist/v5/browser.d.ts +26 -2
- package/dist/v5/browser.js +45 -1
- package/dist/v5/conversion-api.d.ts +9 -0
- package/dist/v5/converter-cli.d.ts +1 -0
- package/dist/v5/converter-cli.js +1214 -0
- package/dist/v5/core.cjs +515 -50
- package/dist/v5/core.d.ts +2 -0
- package/dist/v5/core.js +515 -50
- package/dist/v5/i3s-mesh-conversion.d.ts +14 -4
- package/dist/v5/i3s-mesh-sink.d.ts +25 -0
- package/dist/v5/i3s-mesh-source-features.d.ts +13 -0
- package/dist/v5/i3s-mesh-source-material.d.ts +5 -0
- package/dist/v5/i3s-mesh-source.d.ts +30 -0
- package/dist/v5/i3s-selection-source.d.ts +70 -0
- package/dist/v5/index.cjs +20 -0
- package/dist/v5/index.d.ts +18 -2
- package/dist/v5/index.js +31 -1
- package/dist/v5/mesh-conversion.d.ts +29 -2
- package/dist/v5/mesh-features.d.ts +9 -0
- package/dist/v5/mesh-hierarchy.d.ts +47 -0
- package/dist/v5/mesh-source-features.d.ts +2 -1
- package/dist/v5/mesh-source-schema.d.ts +12 -0
- package/dist/v5/mesh-source-texture.d.ts +2 -0
- package/dist/v5/mesh-source.d.ts +13 -3
- package/dist/v5/mesh-tileset-sink.d.ts +5 -0
- package/dist/v5/mesh.d.ts +6 -1
- package/dist/v5/node-cli.d.ts +9 -0
- package/dist/v5/node-conversion.d.ts +32 -0
- package/dist/v5/node-feature-schema.d.ts +3 -0
- package/dist/v5/node-i3s-source.d.ts +41 -0
- package/dist/v5/node-source.d.ts +47 -0
- package/dist/v5/node.cjs +900 -0
- package/dist/v5/node.d.ts +6 -0
- package/dist/v5/node.js +877 -0
- package/dist/v5/point-cloud-file.d.ts +83 -0
- package/dist/v5/point-cloud-potree.d.ts +47 -0
- package/dist/v5/point-cloud-source-encoder.d.ts +4 -7
- package/dist/v5/point-cloud-spatial.d.ts +22 -0
- package/dist/v5/point-cloud-tileset-sink.d.ts +28 -0
- package/dist/v5/point-cloud-tileset.d.ts +44 -0
- package/dist/v5/point-cloud.d.ts +26 -1
- package/dist/v5/resource-fetcher.d.ts +18 -0
- package/dist/v5/resource-manifest.d.ts +10 -1
- package/dist/v5/selected-mesh-source.d.ts +53 -0
- package/dist/v5/spatial-conversion.d.ts +3 -1
- package/package.json +33 -25
- package/src/index.ts +4 -1
- package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +2 -4
- package/src/v4/converter-cli.ts +8 -2
- package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +3 -51
- package/src/v4/i3s-converter/helpers/node-index-document.ts +5 -2
- package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +1 -1
- package/src/v4/i3s-converter/json-templates/store.ts +2 -1
- package/src/v5/adapters.ts +69 -2
- package/src/v5/archive-source.ts +235 -0
- package/src/v5/browser-tileset-source.ts +418 -48
- package/src/v5/browser.ts +81 -2
- package/src/v5/conversion-api.ts +69 -9
- package/src/v5/converter-cli.ts +28 -0
- package/src/v5/core.ts +6 -0
- package/src/v5/i3s-mesh-conversion.ts +97 -19
- package/src/v5/i3s-mesh-sink.ts +351 -0
- package/src/v5/i3s-mesh-source-features.ts +279 -0
- package/src/v5/i3s-mesh-source-material.ts +157 -0
- package/src/v5/i3s-mesh-source.ts +228 -0
- package/src/v5/i3s-selection-source.ts +298 -0
- package/src/v5/index.ts +60 -2
- package/src/v5/mesh-conversion.ts +61 -11
- package/src/v5/mesh-draco.ts +47 -3
- package/src/v5/mesh-features.ts +319 -0
- package/src/v5/mesh-hierarchy.ts +648 -0
- package/src/v5/mesh-source-features.ts +73 -10
- package/src/v5/mesh-source-schema.ts +134 -0
- package/src/v5/mesh-source-texture.ts +47 -0
- package/src/v5/mesh-source.ts +99 -25
- package/src/v5/mesh-tileset-sink.ts +1 -1
- package/src/v5/mesh.ts +16 -7
- package/src/v5/node-cli.ts +197 -0
- package/src/v5/node-conversion.ts +345 -0
- package/src/v5/node-feature-schema.ts +155 -0
- package/src/v5/node-i3s-source.ts +181 -0
- package/src/v5/node-source.ts +328 -0
- package/src/v5/node.ts +16 -0
- package/src/v5/point-cloud-conversion.ts +9 -1
- package/src/v5/point-cloud-file.ts +306 -0
- package/src/v5/point-cloud-potree.ts +130 -0
- package/src/v5/point-cloud-source-encoder.ts +31 -20
- package/src/v5/point-cloud-spatial.ts +280 -0
- package/src/v5/point-cloud-tileset-sink.ts +234 -0
- package/src/v5/point-cloud-tileset.ts +137 -0
- package/src/v5/point-cloud.ts +159 -32
- package/src/v5/resource-fetcher.ts +113 -0
- package/src/v5/resource-manifest.ts +40 -5
- package/src/v5/selected-mesh-source.ts +384 -0
- package/src/v5/spatial-conversion.ts +161 -6
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {prepareMeshHierarchy} from './mesh-hierarchy.js';
|
|
6
|
+
import {coreApi, parse} from '@loaders.gl/core';
|
|
7
|
+
import type {Loader, LoaderOptions} from '@loaders.gl/loader-utils';
|
|
8
|
+
import {Tiles3DLoader} from '@loaders.gl/3d-tiles';
|
|
9
|
+
import {Tiles3DSource, Tileset3D} from '@loaders.gl/tiles';
|
|
10
|
+
import type {
|
|
11
|
+
TilesetSourceInput,
|
|
12
|
+
TilesetSourceMetadata,
|
|
13
|
+
TilesetSpatialReference
|
|
14
|
+
} from '@loaders.gl/tiles';
|
|
15
|
+
import type {DracoLibraryOptions} from '@loaders.gl/draco/bundled';
|
|
16
|
+
import {
|
|
17
|
+
createBrowserTilesetConversionSource,
|
|
18
|
+
selectBrowserTilesetContent,
|
|
19
|
+
createTiles3DConversionSpatialContext,
|
|
20
|
+
TileConversionError
|
|
21
|
+
} from '@loaders.gl/tile-converter/v5/core';
|
|
22
|
+
import type {
|
|
23
|
+
BrowserTilesetConversionInspection,
|
|
24
|
+
BrowserTilesetConversionInputResource,
|
|
25
|
+
TileConversionSource
|
|
26
|
+
} from '@loaders.gl/tile-converter/v5/core';
|
|
27
|
+
import {
|
|
28
|
+
createMeshTilesetConversionSource,
|
|
29
|
+
createTileConversionResourceFetcher
|
|
30
|
+
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
31
|
+
import type {
|
|
32
|
+
MeshSourceResource,
|
|
33
|
+
MeshSourceFeatureOptions
|
|
34
|
+
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
35
|
+
|
|
36
|
+
/** Required transport, decoded geometry, retained output, and archive budgets for this demo. */
|
|
37
|
+
export const SELECTED_MESH_LIMITS = {
|
|
38
|
+
/** Root JSON plus selected content transport bytes; also the decoded geometry/image/feature byte gate. */
|
|
39
|
+
maxInputBytes: 16 * 1024 * 1024,
|
|
40
|
+
/** Maximum declared content placements inspected. */
|
|
41
|
+
maxInputResources: 1000,
|
|
42
|
+
/** Maximum inspected root/external document placements. */
|
|
43
|
+
maxTilesetDocuments: 32,
|
|
44
|
+
/** Maximum explicitly selected content placements in one partial archive. */
|
|
45
|
+
maxSelectedResources: 64,
|
|
46
|
+
/** Maximum primitive placements authored across all selected contents. */
|
|
47
|
+
maxMeshResources: 64,
|
|
48
|
+
/** Maximum retained file bytes and final archive bytes. */
|
|
49
|
+
maxOutputBytes: 32 * 1024 * 1024,
|
|
50
|
+
/** Maximum measured reconstruction error in meters. */
|
|
51
|
+
maxPositionError: 0.01
|
|
52
|
+
} as const;
|
|
53
|
+
|
|
54
|
+
/** Injected transport and decoder assets for the bounded selected mesh profile. */
|
|
55
|
+
export interface SelectedMeshSourceOptions {
|
|
56
|
+
/** Require a complete qualified explicit tree; pair with createMeshHierarchyTilesetSink. */
|
|
57
|
+
readonly preserveHierarchy?: boolean;
|
|
58
|
+
/** Inspected content placement identifiers; one to 64 distinct selections. */
|
|
59
|
+
readonly resourceIds: readonly string[];
|
|
60
|
+
/** Cancellation across qualification, raw reads and decoding. */
|
|
61
|
+
readonly signal: AbortSignal;
|
|
62
|
+
/** Application-owned decoder asset locations or modules. */
|
|
63
|
+
readonly dracoLibraryOptions: DracoLibraryOptions;
|
|
64
|
+
/** Transport for decoded buffers/images, confined by a platform adapter when local. */
|
|
65
|
+
readonly fetcher?: typeof fetch;
|
|
66
|
+
/** Optional source owning the raw input lifetime, such as a Node file/archive adapter. */
|
|
67
|
+
readonly source?: TileConversionSource<
|
|
68
|
+
BrowserTilesetConversionInspection,
|
|
69
|
+
BrowserTilesetConversionInputResource
|
|
70
|
+
>;
|
|
71
|
+
/** Explicit feature mapping, or the qualified inline structural metadata profile. */
|
|
72
|
+
readonly features?: MeshSourceFeatureOptions;
|
|
73
|
+
/** Lower the shared 16 MiB transport and decoded mesh limit. */
|
|
74
|
+
readonly maxInputBytes?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Qualified selected-mesh decoding and source error policy. */
|
|
78
|
+
export interface SelectedMeshConversionSource {
|
|
79
|
+
/** Lazy source that releases each dedicated mesh runtime after use. */
|
|
80
|
+
readonly source: TileConversionSource<BrowserTilesetConversionInspection, MeshSourceResource>;
|
|
81
|
+
/** Native ECEF coordinate context, qualified before reading mesh content. */
|
|
82
|
+
readonly spatialContext: ReturnType<typeof createTiles3DConversionSpatialContext>;
|
|
83
|
+
/** Conservative metric source error plus the position reconstruction allowance. */
|
|
84
|
+
readonly geometricError: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Qualifies and constructs a lazy selected mesh source without platform I/O or output ownership. */
|
|
88
|
+
export async function createSelectedMeshConversionSource(
|
|
89
|
+
inspection: BrowserTilesetConversionInspection,
|
|
90
|
+
options: SelectedMeshSourceOptions
|
|
91
|
+
): Promise<SelectedMeshConversionSource> {
|
|
92
|
+
const {
|
|
93
|
+
resourceIds,
|
|
94
|
+
signal,
|
|
95
|
+
dracoLibraryOptions,
|
|
96
|
+
features,
|
|
97
|
+
preserveHierarchy,
|
|
98
|
+
fetcher = fetch
|
|
99
|
+
} = options;
|
|
100
|
+
const maxInputBytes = options.maxInputBytes ?? SELECTED_MESH_LIMITS.maxInputBytes;
|
|
101
|
+
if (
|
|
102
|
+
!Number.isSafeInteger(maxInputBytes) ||
|
|
103
|
+
maxInputBytes < 1 ||
|
|
104
|
+
maxInputBytes > SELECTED_MESH_LIMITS.maxInputBytes
|
|
105
|
+
)
|
|
106
|
+
throw new TileConversionError(
|
|
107
|
+
'INVALID_SELECTED_MESH_OPTIONS',
|
|
108
|
+
'Selected mesh input budget must be a positive safe integer at most 16 MiB'
|
|
109
|
+
);
|
|
110
|
+
if (
|
|
111
|
+
!resourceIds.length ||
|
|
112
|
+
resourceIds.length > SELECTED_MESH_LIMITS.maxSelectedResources ||
|
|
113
|
+
new Set(resourceIds).size !== resourceIds.length ||
|
|
114
|
+
resourceIds.some(
|
|
115
|
+
identifier => !inspection.resources.some(resource => resource.resourceId === identifier)
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
throw new Error('Select between 1 and 64 distinct content placements.');
|
|
119
|
+
if (preserveHierarchy !== undefined && typeof preserveHierarchy !== 'boolean')
|
|
120
|
+
throw new TileConversionError(
|
|
121
|
+
'MESH_HIERARCHY_UNSUPPORTED',
|
|
122
|
+
'preserveHierarchy must be a boolean'
|
|
123
|
+
);
|
|
124
|
+
if (preserveHierarchy) prepareMeshHierarchy(inspection, resourceIds);
|
|
125
|
+
const descriptors = inspection.resources.filter(resource =>
|
|
126
|
+
resourceIds.includes(resource.resourceId)
|
|
127
|
+
);
|
|
128
|
+
const selections = descriptors.map(descriptor =>
|
|
129
|
+
selectBrowserTilesetContent(inspection, descriptor)
|
|
130
|
+
);
|
|
131
|
+
const selectedContents = new Map(
|
|
132
|
+
descriptors.map((descriptor, index) => [descriptor.resourceId, selections[index]])
|
|
133
|
+
);
|
|
134
|
+
if (
|
|
135
|
+
!preserveHierarchy &&
|
|
136
|
+
resourceIds.length > 1 &&
|
|
137
|
+
selections.some(selection => !selection.isLeaf)
|
|
138
|
+
)
|
|
139
|
+
throw new Error(
|
|
140
|
+
'Multi-selection requires leaf contents; source LOD hierarchy is not exported.'
|
|
141
|
+
);
|
|
142
|
+
signal.throwIfAborted();
|
|
143
|
+
const boundedFetcher = createTileConversionResourceFetcher({
|
|
144
|
+
maxInputBytes,
|
|
145
|
+
initialInputBytes: inspection.tilesetBytes,
|
|
146
|
+
fetcher,
|
|
147
|
+
signal
|
|
148
|
+
});
|
|
149
|
+
const qualification = createSelectedRuntime(
|
|
150
|
+
inspection.tileset,
|
|
151
|
+
new Uint8Array(),
|
|
152
|
+
signal,
|
|
153
|
+
boundedFetcher,
|
|
154
|
+
inspection.rootUrl,
|
|
155
|
+
dracoLibraryOptions
|
|
156
|
+
);
|
|
157
|
+
let spatialContext: ReturnType<typeof createTiles3DConversionSpatialContext>;
|
|
158
|
+
try {
|
|
159
|
+
await qualification.tilesetInitializationPromise;
|
|
160
|
+
const metadata = await createMeshTilesetConversionSource(qualification).inspect(signal);
|
|
161
|
+
spatialContext = createTiles3DConversionSpatialContext(metadata.spatialReference!);
|
|
162
|
+
} finally {
|
|
163
|
+
qualification.destroy();
|
|
164
|
+
}
|
|
165
|
+
const rawSource =
|
|
166
|
+
options.source ??
|
|
167
|
+
createBrowserTilesetConversionSource({
|
|
168
|
+
input: inspection.rootUrl,
|
|
169
|
+
...SELECTED_MESH_LIMITS,
|
|
170
|
+
maxInputBytes,
|
|
171
|
+
fetcher: boundedFetcher
|
|
172
|
+
});
|
|
173
|
+
const selectedInspection = {...inspection, resources: descriptors};
|
|
174
|
+
const source = {
|
|
175
|
+
/** Reuses inspected declarations without fetching the root again. */
|
|
176
|
+
inspect: async (inspectionSignal?: AbortSignal) => {
|
|
177
|
+
signal.throwIfAborted();
|
|
178
|
+
inspectionSignal?.throwIfAborted();
|
|
179
|
+
return selectedInspection;
|
|
180
|
+
},
|
|
181
|
+
/** Decodes and releases each selected placement in declaration order. */
|
|
182
|
+
async *read(_inspection: BrowserTilesetConversionInspection, readSignal?: AbortSignal) {
|
|
183
|
+
const signal = readSignal ? AbortSignal.any([options.signal, readSignal]) : options.signal;
|
|
184
|
+
signal.throwIfAborted();
|
|
185
|
+
let decodedBytes = 0;
|
|
186
|
+
let primitiveCount = 0;
|
|
187
|
+
// A 3D Tiles contents array has one tile transform, so its GLBs need a shared origin.
|
|
188
|
+
const tileOrigins = new Map<string, readonly [number, number, number]>();
|
|
189
|
+
for await (const raw of rawSource.read(selectedInspection, signal)) {
|
|
190
|
+
const selection = selectedContents.get(raw.resourceId);
|
|
191
|
+
if (!selection)
|
|
192
|
+
throw new TileConversionError(
|
|
193
|
+
'INVALID_BROWSER_SELECTION',
|
|
194
|
+
'Source yielded an uninspected content placement'
|
|
195
|
+
);
|
|
196
|
+
const contentUrl = raw.contentUrl ?? selection.contentUrl;
|
|
197
|
+
const runtime = createSelectedRuntime(
|
|
198
|
+
selection.tileset,
|
|
199
|
+
raw.data,
|
|
200
|
+
signal,
|
|
201
|
+
boundedFetcher,
|
|
202
|
+
contentUrl,
|
|
203
|
+
dracoLibraryOptions,
|
|
204
|
+
spatialContext.spatialReference
|
|
205
|
+
);
|
|
206
|
+
try {
|
|
207
|
+
await runtime.tilesetInitializationPromise;
|
|
208
|
+
const meshSource = createMeshTilesetConversionSource(runtime, {
|
|
209
|
+
unloadContent: true,
|
|
210
|
+
features,
|
|
211
|
+
readExternalResource: async (uri, _contentUri, resourceSignal) => {
|
|
212
|
+
const response = await boundedFetcher(new URL(uri, contentUrl), {
|
|
213
|
+
signal: resourceSignal
|
|
214
|
+
});
|
|
215
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
const metadata = await meshSource.inspect(signal);
|
|
219
|
+
const selectedContext = createTiles3DConversionSpatialContext(metadata.spatialReference!);
|
|
220
|
+
if (
|
|
221
|
+
selectedContext.spatialReference.coordinateEpoch !==
|
|
222
|
+
spatialContext.spatialReference.coordinateEpoch
|
|
223
|
+
)
|
|
224
|
+
throw new TileConversionError(
|
|
225
|
+
'MESH_SOURCE_EPOCH_MISMATCH',
|
|
226
|
+
'Selected content coordinate epoch must match the root; epoch transformations are unsupported'
|
|
227
|
+
);
|
|
228
|
+
let meshCount = 0;
|
|
229
|
+
for await (const resource of meshSource.read(metadata, signal)) {
|
|
230
|
+
meshCount++;
|
|
231
|
+
if (++primitiveCount > SELECTED_MESH_LIMITS.maxMeshResources)
|
|
232
|
+
throw new Error('Selected contents exceed the mesh primitive placement limit.');
|
|
233
|
+
decodedBytes += measureMeshBytes(resource);
|
|
234
|
+
if (decodedBytes > maxInputBytes)
|
|
235
|
+
throw new TileConversionError(
|
|
236
|
+
'INPUT_RESOURCE_TOO_LARGE',
|
|
237
|
+
'Selected decoded geometry and encoded images exceed the aggregate input byte limit.'
|
|
238
|
+
);
|
|
239
|
+
const tileIdentifier = raw.resourceId.replace(/content-\d+$/, '');
|
|
240
|
+
if (preserveHierarchy && !tileOrigins.has(tileIdentifier))
|
|
241
|
+
tileOrigins.set(tileIdentifier, [...resource.origin]);
|
|
242
|
+
yield {
|
|
243
|
+
...resource,
|
|
244
|
+
origin: preserveHierarchy ? tileOrigins.get(tileIdentifier)! : resource.origin,
|
|
245
|
+
id: `${raw.resourceId}/${resource.id}`,
|
|
246
|
+
contentResourceId: raw.resourceId
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (!meshCount) throw new Error('Selected content has no mesh primitive.');
|
|
250
|
+
} finally {
|
|
251
|
+
runtime.destroy();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
return {
|
|
257
|
+
source,
|
|
258
|
+
spatialContext,
|
|
259
|
+
geometricError:
|
|
260
|
+
Math.max(...selections.map(selection => selection.geometricError)) +
|
|
261
|
+
SELECTED_MESH_LIMITS.maxPositionError
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Dedicated runtime source that inherits an absent external-document CRS from the qualified root. */
|
|
266
|
+
class SelectedTiles3DSource extends Tiles3DSource {
|
|
267
|
+
/** Parent frame; explicit, unknown or unresolved child declarations are never replaced. */
|
|
268
|
+
private readonly inheritedSpatialReference?: TilesetSpatialReference;
|
|
269
|
+
|
|
270
|
+
/** Creates a selected source with optional external-document frame inheritance. */
|
|
271
|
+
constructor(
|
|
272
|
+
input: TilesetSourceInput,
|
|
273
|
+
options: LoaderOptions,
|
|
274
|
+
inheritedSpatialReference?: TilesetSpatialReference
|
|
275
|
+
) {
|
|
276
|
+
super(input, options);
|
|
277
|
+
this.inheritedSpatialReference = inheritedSpatialReference;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Returns source metadata with the same absent-CRS inheritance used by external tilesets. */
|
|
281
|
+
override getMetadata(): TilesetSourceMetadata {
|
|
282
|
+
const metadata = super.getMetadata();
|
|
283
|
+
const reference = metadata.spatialReference;
|
|
284
|
+
if (
|
|
285
|
+
this.inheritedSpatialReference &&
|
|
286
|
+
reference?.crs.state === 'absent' &&
|
|
287
|
+
reference.coordinateEpoch === undefined &&
|
|
288
|
+
!reference.verticalCrs &&
|
|
289
|
+
!reference.warnings.length
|
|
290
|
+
)
|
|
291
|
+
return {...metadata, spatialReference: this.inheritedSpatialReference};
|
|
292
|
+
return metadata;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Creates a dedicated runtime whose only content response is the bounded selected payload. */
|
|
297
|
+
function createSelectedRuntime(
|
|
298
|
+
document: BrowserTilesetConversionInspection['tileset'],
|
|
299
|
+
data: Uint8Array,
|
|
300
|
+
signal: AbortSignal,
|
|
301
|
+
boundedFetcher: typeof fetch,
|
|
302
|
+
contentUrl: string,
|
|
303
|
+
dracoLibraryOptions: DracoLibraryOptions,
|
|
304
|
+
inheritedSpatialReference?: TilesetSpatialReference
|
|
305
|
+
): Tileset3D {
|
|
306
|
+
const {
|
|
307
|
+
modules,
|
|
308
|
+
decoderProfile,
|
|
309
|
+
CDN: contentDeliveryNetwork,
|
|
310
|
+
useLocalLibraries
|
|
311
|
+
} = dracoLibraryOptions;
|
|
312
|
+
const loadOptions = {
|
|
313
|
+
modules,
|
|
314
|
+
core: {
|
|
315
|
+
worker: false,
|
|
316
|
+
fetch: boundedFetcher,
|
|
317
|
+
CDN: contentDeliveryNetwork,
|
|
318
|
+
useLocalLibraries
|
|
319
|
+
},
|
|
320
|
+
// Reuse the full decoder already bundled for output verification.
|
|
321
|
+
draco: {decoderProfile},
|
|
322
|
+
// Preserve authored UVs and the transform for the converter's qualified GLB writer.
|
|
323
|
+
gltf: {
|
|
324
|
+
loadImages: false,
|
|
325
|
+
decompressMeshes: true,
|
|
326
|
+
excludeExtensions: {
|
|
327
|
+
KHR_texture_transform: false,
|
|
328
|
+
// Only Draco input compression is qualified by this conversion profile.
|
|
329
|
+
EXT_meshopt_compression: false,
|
|
330
|
+
KHR_meshopt_compression: false
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
'3d-tiles': {loadGLTF: true}
|
|
334
|
+
};
|
|
335
|
+
return new Tileset3D(
|
|
336
|
+
new SelectedTiles3DSource(
|
|
337
|
+
{
|
|
338
|
+
url: contentUrl,
|
|
339
|
+
loader: Tiles3DLoader,
|
|
340
|
+
resolver: {
|
|
341
|
+
/** Normalizes the selected hierarchy through the format loader, retaining ancestor transforms. */
|
|
342
|
+
loadRoot: async (_url: string, loader: Loader, options: LoaderOptions) =>
|
|
343
|
+
parse(new TextEncoder().encode(JSON.stringify(document)), loader, options),
|
|
344
|
+
/** Decodes only the selected bounded payload; no sibling content can be requested. */
|
|
345
|
+
loadResource: async (_url: string, loader: Loader, options: LoaderOptions) => {
|
|
346
|
+
signal.throwIfAborted();
|
|
347
|
+
return parse(data, loader, options, {
|
|
348
|
+
url: contentUrl,
|
|
349
|
+
baseUrl: contentUrl.startsWith('blob:') ? contentUrl : new URL('.', contentUrl).href,
|
|
350
|
+
fetch: boundedFetcher,
|
|
351
|
+
coreApi,
|
|
352
|
+
_parse: parse
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
loadOptions,
|
|
358
|
+
inheritedSpatialReference
|
|
359
|
+
)
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Charges geometry, encoded image bytes, triangle associations and Arrow columns after extraction. */
|
|
364
|
+
export function measureMeshBytes(resource: MeshSourceResource): number {
|
|
365
|
+
const featureBytes = resource.features
|
|
366
|
+
? resource.features.triangleFeatureIndices.byteLength +
|
|
367
|
+
resource.features.batches.reduce(
|
|
368
|
+
(bytes, batch) =>
|
|
369
|
+
bytes +
|
|
370
|
+
batch.data.schema.fields.reduce(
|
|
371
|
+
(columnBytes, field) =>
|
|
372
|
+
columnBytes + (batch.data.getChild(field.name)?.byteLength ?? 0),
|
|
373
|
+
0
|
|
374
|
+
),
|
|
375
|
+
0
|
|
376
|
+
)
|
|
377
|
+
: 0;
|
|
378
|
+
return Object.values(resource.mesh.attributes).reduce(
|
|
379
|
+
(bytes, attribute) => bytes + attribute.value.byteLength,
|
|
380
|
+
(resource.mesh.indices?.value.byteLength ?? 0) +
|
|
381
|
+
(resource.material?.baseColorTexture?.data.byteLength ?? 0) +
|
|
382
|
+
featureBytes
|
|
383
|
+
);
|
|
384
|
+
}
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
CreateTilesetSpatialReferenceOptions,
|
|
14
14
|
I3SSpatialBounds,
|
|
15
15
|
I3STransformedPositions,
|
|
16
|
+
I3STransformedGeometry,
|
|
16
17
|
Tiles3DSpatialBoundingVolume,
|
|
17
18
|
TilesetSpatialOptions,
|
|
18
19
|
TilesetSpatialReference
|
|
@@ -44,6 +45,12 @@ export interface I3SConversionSpatialContext {
|
|
|
44
45
|
positions: ArrayLike<number>,
|
|
45
46
|
sourceOrigin: ArrayLike<number>
|
|
46
47
|
): I3STransformedPositions;
|
|
48
|
+
/** Transform absolute conversion geometry without intermediate float32 rounding. */
|
|
49
|
+
transformGeometryAsync(
|
|
50
|
+
positions: ArrayLike<number>,
|
|
51
|
+
normals?: ArrayLike<number>,
|
|
52
|
+
normalReferenceFrame?: string
|
|
53
|
+
): Promise<I3STransformedGeometry>;
|
|
47
54
|
/** Transform absolute I3S positions when terrain or scene sampling is asynchronous. */
|
|
48
55
|
transformPositionsAsync(
|
|
49
56
|
positions: ArrayLike<number>,
|
|
@@ -87,6 +94,13 @@ export function createTiles3DConversionSpatialContext(
|
|
|
87
94
|
throw createSpatialConversionError(spatialReference);
|
|
88
95
|
}
|
|
89
96
|
|
|
97
|
+
if (spatialReference.status !== 'native' && spatialReference.coordinateEpoch !== undefined) {
|
|
98
|
+
throw new TileConversionError(
|
|
99
|
+
'SPATIAL_EPOCH_UNSUPPORTED',
|
|
100
|
+
'Epoch-tagged coordinates require an epoch-aware datum operation before conversion'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
90
104
|
const transformer =
|
|
91
105
|
spatialReference.status === 'native'
|
|
92
106
|
? undefined
|
|
@@ -101,9 +115,11 @@ export function createTiles3DConversionSpatialContext(
|
|
|
101
115
|
return {
|
|
102
116
|
spatialReference: outputSpatialReference,
|
|
103
117
|
transformPositions: positions =>
|
|
104
|
-
transformer ? transformer.transformPositions(positions) :
|
|
118
|
+
transformer ? transformer.transformPositions(positions) : copyNativePositions(positions),
|
|
105
119
|
transformNormals: (normals, positions) =>
|
|
106
|
-
transformer
|
|
120
|
+
transformer
|
|
121
|
+
? transformer.transformNormals(normals, positions)
|
|
122
|
+
: copyNativeNormals(normals, positions),
|
|
107
123
|
transformBoundingVolume: (volume, sourceTransform) =>
|
|
108
124
|
transformer
|
|
109
125
|
? transformer.transformBoundingVolume(volume, sourceTransform)
|
|
@@ -131,6 +147,13 @@ export function createI3SConversionSpatialContext(
|
|
|
131
147
|
throw createSpatialConversionError(spatialReference);
|
|
132
148
|
}
|
|
133
149
|
|
|
150
|
+
if (spatialReference.status !== 'native' && spatialReference.coordinateEpoch !== undefined) {
|
|
151
|
+
throw new TileConversionError(
|
|
152
|
+
'SPATIAL_EPOCH_UNSUPPORTED',
|
|
153
|
+
'Epoch-tagged coordinates require an epoch-aware datum operation before conversion'
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
134
157
|
const transformer =
|
|
135
158
|
spatialReference.status === 'native'
|
|
136
159
|
? undefined
|
|
@@ -139,6 +162,36 @@ export function createI3SConversionSpatialContext(
|
|
|
139
162
|
() => new I3SSpatialTransformer(spatialReference, options)
|
|
140
163
|
);
|
|
141
164
|
const outputSpatialReference = transformer ? transformer.spatialReference : spatialReference;
|
|
165
|
+
// I3S vector bases can differ from the coordinate basis even when positions retain their CRS.
|
|
166
|
+
let nativeNormalTransformer: I3SSpatialTransformer | undefined;
|
|
167
|
+
/** Maps native I3S vectors into the writer basis without reprojecting its positions. */
|
|
168
|
+
const transformNativeNormals = (
|
|
169
|
+
normals: ArrayLike<number>,
|
|
170
|
+
positions: ArrayLike<number>,
|
|
171
|
+
normalReferenceFrame?: string
|
|
172
|
+
): Float32Array => {
|
|
173
|
+
validateNormalReferenceFrame(normalReferenceFrame);
|
|
174
|
+
const targetNormals = copyNativeNormals(normals, positions);
|
|
175
|
+
if (
|
|
176
|
+
!spatialReference.sourceCrs ||
|
|
177
|
+
['local', 'cartesian'].includes(spatialReference.coordinateFrame)
|
|
178
|
+
) {
|
|
179
|
+
return targetNormals;
|
|
180
|
+
}
|
|
181
|
+
nativeNormalTransformer ||= createSpatialTransformer(
|
|
182
|
+
spatialReference,
|
|
183
|
+
() =>
|
|
184
|
+
new I3SSpatialTransformer(
|
|
185
|
+
{
|
|
186
|
+
...spatialReference,
|
|
187
|
+
status: 'transformable',
|
|
188
|
+
targetCrs: spatialReference.sourceCrs
|
|
189
|
+
},
|
|
190
|
+
options
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
return nativeNormalTransformer.transformNormals(targetNormals, positions, normalReferenceFrame);
|
|
194
|
+
};
|
|
142
195
|
|
|
143
196
|
return {
|
|
144
197
|
spatialReference: outputSpatialReference,
|
|
@@ -146,6 +199,31 @@ export function createI3SConversionSpatialContext(
|
|
|
146
199
|
transformer
|
|
147
200
|
? transformer.transformPositions(positions, sourceOrigin)
|
|
148
201
|
: createNativeI3SPositions(positions, sourceOrigin, spatialReference),
|
|
202
|
+
transformGeometryAsync: async (positions, normals, normalReferenceFrame) => {
|
|
203
|
+
if (transformer)
|
|
204
|
+
return transformer.transformGeometryAsync(positions, normals, normalReferenceFrame);
|
|
205
|
+
const targetPositions = Float64Array.from(positions);
|
|
206
|
+
if (
|
|
207
|
+
targetPositions.length % 3 ||
|
|
208
|
+
targetPositions.some(value => !Number.isFinite(value)) ||
|
|
209
|
+
(normals &&
|
|
210
|
+
(normals.length !== positions.length ||
|
|
211
|
+
Array.from(normals).some(value => !Number.isFinite(value)))) ||
|
|
212
|
+
(normalReferenceFrame !== undefined &&
|
|
213
|
+
!['earth-centered', 'vertex-reference-frame'].includes(normalReferenceFrame))
|
|
214
|
+
) {
|
|
215
|
+
throw new TileConversionError(
|
|
216
|
+
'SPATIAL_INPUT_INVALID',
|
|
217
|
+
'Conversion geometry requires finite matching xyz arrays and a supported I3S vector frame'
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
positions: targetPositions,
|
|
222
|
+
normals: normals
|
|
223
|
+
? transformNativeNormals(normals, positions, normalReferenceFrame)
|
|
224
|
+
: undefined
|
|
225
|
+
};
|
|
226
|
+
},
|
|
149
227
|
transformPositionsAsync: (positions, sourceOrigin) =>
|
|
150
228
|
transformer
|
|
151
229
|
? transformer.transformPositionsAsync(positions, sourceOrigin)
|
|
@@ -153,7 +231,7 @@ export function createI3SConversionSpatialContext(
|
|
|
153
231
|
transformNormals: (normals, positions, normalReferenceFrame) =>
|
|
154
232
|
transformer
|
|
155
233
|
? transformer.transformNormals(normals, positions, normalReferenceFrame)
|
|
156
|
-
:
|
|
234
|
+
: transformNativeNormals(normals, positions, normalReferenceFrame),
|
|
157
235
|
transformBoundingVolume: bounds =>
|
|
158
236
|
transformer ? transformer.transformBoundingVolume(bounds) : cloneI3SBounds(bounds),
|
|
159
237
|
transformBoundingVolumeAsync: bounds =>
|
|
@@ -277,21 +355,33 @@ function createNativeI3SPositions(
|
|
|
277
355
|
sourceOrigin: ArrayLike<number>,
|
|
278
356
|
spatialReference: TilesetSpatialReference
|
|
279
357
|
): I3STransformedPositions {
|
|
280
|
-
|
|
281
|
-
|
|
358
|
+
const sourcePositions64 = copyNativePositions(sourcePositions);
|
|
359
|
+
if (
|
|
360
|
+
sourceOrigin.length !== 3 ||
|
|
361
|
+
Array.from(sourceOrigin).some(value => !Number.isFinite(value))
|
|
362
|
+
) {
|
|
363
|
+
throw new TileConversionError(
|
|
364
|
+
'SPATIAL_INPUT_INVALID',
|
|
365
|
+
'I3S origins require three finite coordinates'
|
|
366
|
+
);
|
|
282
367
|
}
|
|
283
368
|
const origin: [number, number, number] = [
|
|
284
369
|
Number(sourceOrigin[0]),
|
|
285
370
|
Number(sourceOrigin[1]),
|
|
286
371
|
Number(sourceOrigin[2])
|
|
287
372
|
];
|
|
288
|
-
const sourcePositions64 = Float64Array.from(sourcePositions);
|
|
289
373
|
const positions = new Float32Array(sourcePositions.length);
|
|
290
374
|
for (let index = 0; index < sourcePositions.length; index += 3) {
|
|
291
375
|
positions[index] = Number(sourcePositions[index]) - origin[0];
|
|
292
376
|
positions[index + 1] = Number(sourcePositions[index + 1]) - origin[1];
|
|
293
377
|
positions[index + 2] = Number(sourcePositions[index + 2]) - origin[2];
|
|
294
378
|
}
|
|
379
|
+
if (positions.some(value => !Number.isFinite(value))) {
|
|
380
|
+
throw new TileConversionError(
|
|
381
|
+
'SPATIAL_INPUT_INVALID',
|
|
382
|
+
'Native I3S offsets exceed float32 range'
|
|
383
|
+
);
|
|
384
|
+
}
|
|
295
385
|
const isGeographic = spatialReference.coordinateFrame === 'geographic';
|
|
296
386
|
const modelMatrix = isGeographic ? new Matrix4() : new Matrix4().translate(origin);
|
|
297
387
|
return {
|
|
@@ -303,3 +393,68 @@ function createNativeI3SPositions(
|
|
|
303
393
|
modelMatrix
|
|
304
394
|
};
|
|
305
395
|
}
|
|
396
|
+
|
|
397
|
+
/** Validates and copies native xyz coordinates without invoking a CRS transformer. */
|
|
398
|
+
function copyNativePositions(positions: ArrayLike<number>): Float64Array {
|
|
399
|
+
validateNativePositions(positions);
|
|
400
|
+
return Float64Array.from(positions);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Checks native coordinates without allocating an additional position buffer. */
|
|
404
|
+
function validateNativePositions(positions: ArrayLike<number>): void {
|
|
405
|
+
if (positions.length % 3) {
|
|
406
|
+
throw new TileConversionError(
|
|
407
|
+
'SPATIAL_INPUT_INVALID',
|
|
408
|
+
'Positions require complete finite xyz coordinates'
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
for (let index = 0; index < positions.length; index++) {
|
|
412
|
+
if (!Number.isFinite(positions[index])) {
|
|
413
|
+
throw new TileConversionError(
|
|
414
|
+
'SPATIAL_INPUT_INVALID',
|
|
415
|
+
'Positions require complete finite xyz coordinates'
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Validates matching native normals and normalizes them without changing their basis. */
|
|
422
|
+
function copyNativeNormals(normals: ArrayLike<number>, positions: ArrayLike<number>): Float32Array {
|
|
423
|
+
validateNativePositions(positions);
|
|
424
|
+
if (normals.length !== positions.length) {
|
|
425
|
+
throw new TileConversionError(
|
|
426
|
+
'SPATIAL_INPUT_INVALID',
|
|
427
|
+
'Normals require finite xyz values matching positions'
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
const output = new Float32Array(normals.length);
|
|
431
|
+
for (let index = 0; index < normals.length; index += 3) {
|
|
432
|
+
const scale = Math.max(
|
|
433
|
+
Math.abs(normals[index]),
|
|
434
|
+
Math.abs(normals[index + 1]),
|
|
435
|
+
Math.abs(normals[index + 2])
|
|
436
|
+
);
|
|
437
|
+
if (!Number.isFinite(scale)) {
|
|
438
|
+
throw new TileConversionError(
|
|
439
|
+
'SPATIAL_INPUT_INVALID',
|
|
440
|
+
'Normals require finite xyz values matching positions'
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
const length = scale
|
|
444
|
+
? Math.hypot(normals[index] / scale, normals[index + 1] / scale, normals[index + 2] / scale)
|
|
445
|
+
: 0;
|
|
446
|
+
for (let axis = 0; axis < 3; axis++)
|
|
447
|
+
output[index + axis] = length ? normals[index + axis] / scale / length : 0;
|
|
448
|
+
}
|
|
449
|
+
return output;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Rejects unknown I3S vector frames even when no position reprojection is requested. */
|
|
453
|
+
function validateNormalReferenceFrame(frame?: string): void {
|
|
454
|
+
if (frame !== undefined && !['earth-centered', 'vertex-reference-frame'].includes(frame)) {
|
|
455
|
+
throw new TileConversionError(
|
|
456
|
+
'SPATIAL_INPUT_INVALID',
|
|
457
|
+
'Unsupported I3S normal reference frame'
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
}
|