@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.
Files changed (152) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +180 -4
  3. package/bin/converter-v5.js +2 -0
  4. package/dist/converter.min.cjs +172 -182
  5. package/dist/i3s-server/bin/i3s-server.min.cjs +113 -158
  6. package/dist/index.cjs +18832 -19228
  7. package/dist/index.js +19566 -19968
  8. package/dist/v5/adapters.cjs +2002 -230
  9. package/dist/v5/adapters.d.ts +20 -0
  10. package/dist/v5/adapters.js +2005 -214
  11. package/dist/v5/archive-source.d.ts +36 -0
  12. package/dist/v5/browser-tileset-source.d.ts +55 -11
  13. package/dist/v5/browser.cjs +18 -0
  14. package/dist/v5/browser.d.ts +15 -1
  15. package/dist/v5/browser.js +26 -0
  16. package/dist/v5/conversion-api.d.ts +34 -0
  17. package/dist/v5/converter-cli.d.ts +1 -0
  18. package/dist/v5/converter-cli.js +1230 -0
  19. package/dist/v5/core.cjs +547 -77
  20. package/dist/v5/core.d.ts +3 -1
  21. package/dist/v5/core.js +540 -70
  22. package/dist/v5/feature-arrow.d.ts +1 -1
  23. package/dist/v5/i3s-mesh-conversion.d.ts +5 -1
  24. package/dist/v5/i3s-mesh-source-features.d.ts +2 -2
  25. package/dist/v5/i3s-mesh-source-material.d.ts +1 -1
  26. package/dist/v5/i3s-mesh-source.d.ts +2 -2
  27. package/dist/v5/i3s-selection-source.d.ts +70 -0
  28. package/dist/v5/index.cjs +15 -0
  29. package/dist/v5/index.d.ts +13 -1
  30. package/dist/v5/index.js +22 -0
  31. package/dist/v5/mesh-conversion.d.ts +21 -3
  32. package/dist/v5/mesh-draco.d.ts +2 -2
  33. package/dist/v5/mesh-features.d.ts +2 -2
  34. package/dist/v5/mesh-hierarchy.d.ts +47 -0
  35. package/dist/v5/mesh-source-features.d.ts +6 -5
  36. package/dist/v5/mesh-source-schema.d.ts +12 -0
  37. package/dist/v5/mesh-source-texture.d.ts +3 -3
  38. package/dist/v5/mesh-source.d.ts +10 -7
  39. package/dist/v5/mesh-tileset-sink.d.ts +5 -0
  40. package/dist/v5/mesh.d.ts +1 -1
  41. package/dist/v5/node-cli.d.ts +9 -0
  42. package/dist/v5/node-conversion.d.ts +34 -0
  43. package/dist/v5/node-feature-schema.d.ts +3 -0
  44. package/dist/v5/node-i3s-source.d.ts +41 -0
  45. package/dist/v5/node-source.d.ts +47 -0
  46. package/dist/v5/node.cjs +913 -0
  47. package/dist/v5/node.d.ts +6 -0
  48. package/dist/v5/node.js +890 -0
  49. package/dist/v5/point-cloud-attributes.d.ts +19 -0
  50. package/dist/v5/point-cloud-conversion.d.ts +1 -1
  51. package/dist/v5/point-cloud-copc.d.ts +44 -0
  52. package/dist/v5/point-cloud-file.d.ts +83 -0
  53. package/dist/v5/point-cloud-potree.d.ts +47 -0
  54. package/dist/v5/point-cloud-source-encoder.d.ts +1 -1
  55. package/dist/v5/point-cloud-source.d.ts +1 -1
  56. package/dist/v5/point-cloud-tileset.d.ts +44 -0
  57. package/dist/v5/point-cloud.d.ts +1 -1
  58. package/dist/v5/resource-manifest.d.ts +10 -1
  59. package/dist/v5/selected-mesh-source.d.ts +53 -0
  60. package/dist/v5/spatial-conversion.d.ts +1 -1
  61. package/dist/v5/tileset-source.d.ts +1 -1
  62. package/package.json +25 -32
  63. package/src/index.ts +4 -1
  64. package/src/v4/3d-tiles-converter/3d-tiles-converter.ts +11 -12
  65. package/src/v4/3d-tiles-converter/helpers/3d-tiles-content-converter.ts +7 -3
  66. package/src/v4/3d-tiles-converter/helpers/i3s-obb-to-3d-tiles-obb.ts +2 -2
  67. package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +4 -6
  68. package/src/v4/3d-tiles-converter/helpers/texture-atlas.ts +1 -1
  69. package/src/v4/3d-tiles-converter/helpers/validate-i3s-spatial-reference.ts +2 -2
  70. package/src/v4/3d-tiles-converter/json-templates/tileset.ts +58 -39
  71. package/src/v4/converter-cli.ts +10 -4
  72. package/src/v4/deps-installer/deps-installer.ts +3 -3
  73. package/src/v4/i3s-converter/helpers/attribute-metadata-info.ts +1 -1
  74. package/src/v4/i3s-converter/helpers/batch-ids-extensions.ts +9 -10
  75. package/src/v4/i3s-converter/helpers/conversion-gltf.ts +108 -0
  76. package/src/v4/i3s-converter/helpers/coordinate-converter.ts +6 -5
  77. package/src/v4/i3s-converter/helpers/create-scene-server-path.ts +5 -6
  78. package/src/v4/i3s-converter/helpers/feature-attributes.ts +4 -4
  79. package/src/v4/i3s-converter/helpers/geometry-attributes.ts +1 -1
  80. package/src/v4/i3s-converter/helpers/geometry-converter.ts +69 -77
  81. package/src/v4/i3s-converter/helpers/gltf-attributes.ts +58 -74
  82. package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +6 -54
  83. package/src/v4/i3s-converter/helpers/node-debug.ts +7 -3
  84. package/src/v4/i3s-converter/helpers/node-index-document.ts +11 -9
  85. package/src/v4/i3s-converter/helpers/node-pages.ts +3 -4
  86. package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +2 -2
  87. package/src/v4/i3s-converter/helpers/tileset-traversal.ts +1 -1
  88. package/src/v4/i3s-converter/i3s-converter.ts +29 -31
  89. package/src/v4/i3s-converter/json-templates/geometry-definitions.ts +71 -66
  90. package/src/v4/i3s-converter/json-templates/layers.ts +97 -132
  91. package/src/v4/i3s-converter/json-templates/metadata.ts +31 -21
  92. package/src/v4/i3s-converter/json-templates/node.ts +60 -74
  93. package/src/v4/i3s-converter/json-templates/scene-server.ts +36 -27
  94. package/src/v4/i3s-converter/json-templates/shared-resources.ts +62 -123
  95. package/src/v4/i3s-converter/json-templates/store.ts +39 -57
  96. package/src/v4/i3s-converter/types.ts +4 -4
  97. package/src/v4/i3s-server/controllers/slpk-controller.ts +3 -3
  98. package/src/v4/i3s-server/utils/create-scene-server.ts +3 -3
  99. package/src/v4/lib/utils/builder-input.ts +8 -0
  100. package/src/v4/lib/utils/conversion-dump.ts +5 -1
  101. package/src/v4/lib/utils/file-utils.ts +1 -1
  102. package/src/v4/lib/utils/hash-string.ts +21 -0
  103. package/src/v4/lib/utils/lod-conversion-utils.ts +3 -3
  104. package/src/v4/pgm-loader.ts +2 -2
  105. package/src/v5/adapters.ts +53 -0
  106. package/src/v5/archive-source.ts +235 -0
  107. package/src/v5/browser-tileset-source.ts +418 -48
  108. package/src/v5/browser.ts +46 -0
  109. package/src/v5/conversion-api.ts +184 -17
  110. package/src/v5/conversion-archive.ts +2 -2
  111. package/src/v5/converter-cli.ts +28 -0
  112. package/src/v5/core.ts +7 -0
  113. package/src/v5/feature-arrow.ts +2 -2
  114. package/src/v5/i3s-mesh-conversion.ts +41 -3
  115. package/src/v5/i3s-mesh-sink.ts +1 -1
  116. package/src/v5/i3s-mesh-source-features.ts +3 -3
  117. package/src/v5/i3s-mesh-source-material.ts +18 -11
  118. package/src/v5/i3s-mesh-source.ts +3 -2
  119. package/src/v5/i3s-selection-source.ts +298 -0
  120. package/src/v5/index.ts +41 -0
  121. package/src/v5/mesh-conversion.ts +24 -4
  122. package/src/v5/mesh-draco.ts +2 -2
  123. package/src/v5/mesh-features.ts +3 -3
  124. package/src/v5/mesh-hierarchy.ts +648 -0
  125. package/src/v5/mesh-source-features.ts +165 -51
  126. package/src/v5/mesh-source-schema.ts +140 -0
  127. package/src/v5/mesh-source-texture.ts +49 -19
  128. package/src/v5/mesh-source.ts +80 -49
  129. package/src/v5/mesh-tileset-archive.ts +1 -1
  130. package/src/v5/mesh-tileset-sink.ts +2 -2
  131. package/src/v5/mesh.ts +1 -1
  132. package/src/v5/node-cli.ts +201 -0
  133. package/src/v5/node-conversion.ts +362 -0
  134. package/src/v5/node-feature-schema.ts +155 -0
  135. package/src/v5/node-i3s-source.ts +181 -0
  136. package/src/v5/node-source.ts +328 -0
  137. package/src/v5/node.ts +16 -0
  138. package/src/v5/point-cloud-attributes.ts +113 -0
  139. package/src/v5/point-cloud-conversion.ts +1 -1
  140. package/src/v5/point-cloud-copc.ts +289 -0
  141. package/src/v5/point-cloud-file.ts +306 -0
  142. package/src/v5/point-cloud-potree.ts +134 -0
  143. package/src/v5/point-cloud-source-encoder.ts +1 -1
  144. package/src/v5/point-cloud-source.ts +1 -1
  145. package/src/v5/point-cloud-spatial.ts +2 -2
  146. package/src/v5/point-cloud-tileset.ts +137 -0
  147. package/src/v5/point-cloud.ts +3 -3
  148. package/src/v5/resource-fetcher.ts +4 -1
  149. package/src/v5/resource-manifest.ts +40 -5
  150. package/src/v5/selected-mesh-source.ts +384 -0
  151. package/src/v5/spatial-conversion.ts +90 -19
  152. package/src/v5/tileset-source.ts +2 -2
@@ -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/core/loader-utils';
8
+ import {Tiles3DLoader} from '@loaders.gl/tiles-3d/3d-tiles';
9
+ import {Tiles3DSource, Tileset3D} from '@loaders.gl/tiles-3d';
10
+ import type {
11
+ TilesetSourceInput,
12
+ TilesetSourceMetadata,
13
+ TilesetSpatialReference
14
+ } from '@loaders.gl/tiles-3d';
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
+ }
@@ -7,7 +7,7 @@ import {
7
7
  I3SSpatialTransformer,
8
8
  markTilesetSpatialReferenceTransformed,
9
9
  Tiles3DSpatialTransformer
10
- } from '@loaders.gl/tiles';
10
+ } from '@loaders.gl/tiles-3d';
11
11
  import {Matrix4} from '@math.gl/core';
12
12
  import type {
13
13
  CreateTilesetSpatialReferenceOptions,
@@ -17,7 +17,7 @@ import type {
17
17
  Tiles3DSpatialBoundingVolume,
18
18
  TilesetSpatialOptions,
19
19
  TilesetSpatialReference
20
- } from '@loaders.gl/tiles';
20
+ } from '@loaders.gl/tiles-3d';
21
21
  import {TileConversionError} from './conversion-api.js';
22
22
  import type {TileConversionDiagnostic} from './conversion-api.js';
23
23
 
@@ -115,9 +115,11 @@ export function createTiles3DConversionSpatialContext(
115
115
  return {
116
116
  spatialReference: outputSpatialReference,
117
117
  transformPositions: positions =>
118
- transformer ? transformer.transformPositions(positions) : Float64Array.from(positions),
118
+ transformer ? transformer.transformPositions(positions) : copyNativePositions(positions),
119
119
  transformNormals: (normals, positions) =>
120
- transformer ? transformer.transformNormals(normals, positions) : Float32Array.from(normals),
120
+ transformer
121
+ ? transformer.transformNormals(normals, positions)
122
+ : copyNativeNormals(normals, positions),
121
123
  transformBoundingVolume: (volume, sourceTransform) =>
122
124
  transformer
123
125
  ? transformer.transformBoundingVolume(volume, sourceTransform)
@@ -168,20 +170,12 @@ export function createI3SConversionSpatialContext(
168
170
  positions: ArrayLike<number>,
169
171
  normalReferenceFrame?: string
170
172
  ): Float32Array => {
173
+ validateNormalReferenceFrame(normalReferenceFrame);
174
+ const targetNormals = copyNativeNormals(normals, positions);
171
175
  if (
172
176
  !spatialReference.sourceCrs ||
173
177
  ['local', 'cartesian'].includes(spatialReference.coordinateFrame)
174
178
  ) {
175
- // Unreferenced/native local vectors already use the retained position basis.
176
- const targetNormals = Float32Array.from(normals);
177
- for (let index = 0; index < normals.length; index += 3) {
178
- const length = Math.hypot(normals[index], normals[index + 1], normals[index + 2]);
179
- if (length) {
180
- for (let axis = 0; axis < 3; axis++) {
181
- targetNormals[index + axis] = normals[index + axis] / length;
182
- }
183
- }
184
- }
185
179
  return targetNormals;
186
180
  }
187
181
  nativeNormalTransformer ||= createSpatialTransformer(
@@ -196,7 +190,7 @@ export function createI3SConversionSpatialContext(
196
190
  options
197
191
  )
198
192
  );
199
- return nativeNormalTransformer.transformNormals(normals, positions, normalReferenceFrame);
193
+ return nativeNormalTransformer.transformNormals(targetNormals, positions, normalReferenceFrame);
200
194
  };
201
195
 
202
196
  return {
@@ -237,7 +231,7 @@ export function createI3SConversionSpatialContext(
237
231
  transformNormals: (normals, positions, normalReferenceFrame) =>
238
232
  transformer
239
233
  ? transformer.transformNormals(normals, positions, normalReferenceFrame)
240
- : Float32Array.from(normals),
234
+ : transformNativeNormals(normals, positions, normalReferenceFrame),
241
235
  transformBoundingVolume: bounds =>
242
236
  transformer ? transformer.transformBoundingVolume(bounds) : cloneI3SBounds(bounds),
243
237
  transformBoundingVolumeAsync: bounds =>
@@ -361,21 +355,33 @@ function createNativeI3SPositions(
361
355
  sourceOrigin: ArrayLike<number>,
362
356
  spatialReference: TilesetSpatialReference
363
357
  ): I3STransformedPositions {
364
- if (sourcePositions.length % 3 !== 0 || sourceOrigin.length < 3) {
365
- throw new Error('I3S positions and origin must contain complete xyz coordinates');
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
+ );
366
367
  }
367
368
  const origin: [number, number, number] = [
368
369
  Number(sourceOrigin[0]),
369
370
  Number(sourceOrigin[1]),
370
371
  Number(sourceOrigin[2])
371
372
  ];
372
- const sourcePositions64 = Float64Array.from(sourcePositions);
373
373
  const positions = new Float32Array(sourcePositions.length);
374
374
  for (let index = 0; index < sourcePositions.length; index += 3) {
375
375
  positions[index] = Number(sourcePositions[index]) - origin[0];
376
376
  positions[index + 1] = Number(sourcePositions[index + 1]) - origin[1];
377
377
  positions[index + 2] = Number(sourcePositions[index + 2]) - origin[2];
378
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
+ }
379
385
  const isGeographic = spatialReference.coordinateFrame === 'geographic';
380
386
  const modelMatrix = isGeographic ? new Matrix4() : new Matrix4().translate(origin);
381
387
  return {
@@ -387,3 +393,68 @@ function createNativeI3SPositions(
387
393
  modelMatrix
388
394
  };
389
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
+ }
@@ -2,13 +2,13 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import {traverseTilesetContents} from '@loaders.gl/tiles';
5
+ import {traverseTilesetContents} from '@loaders.gl/tiles-3d';
6
6
  import type {
7
7
  Tileset3D,
8
8
  TilesetContentTraversalItem,
9
9
  TilesetSourceMetadata,
10
10
  TraverseTilesetContentsOptions
11
- } from '@loaders.gl/tiles';
11
+ } from '@loaders.gl/tiles-3d';
12
12
  import {TileConversionError} from './conversion-api.js';
13
13
  import type {TileConversionSource} from './conversion-api.js';
14
14