@loaders.gl/tile-converter 5.0.0-alpha.1 → 5.0.0-alpha.11

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 (141) hide show
  1. package/LICENSE +14 -3
  2. package/README.md +497 -0
  3. package/dist/converter.min.cjs +210 -282
  4. package/dist/i3s-server/bin/i3s-server.min.cjs +106 -341
  5. package/dist/index.cjs +38586 -0
  6. package/dist/index.js +38557 -0
  7. package/dist/v5/adapters.cjs +3548 -0
  8. package/dist/v5/adapters.d.ts +33 -0
  9. package/dist/v5/adapters.js +3537 -0
  10. package/dist/v5/browser-sink.d.ts +37 -0
  11. package/dist/v5/browser-source.d.ts +38 -0
  12. package/dist/v5/browser-tileset-source.d.ts +70 -0
  13. package/dist/v5/browser.cjs +76 -0
  14. package/dist/v5/browser.d.ts +44 -0
  15. package/dist/v5/browser.js +75 -0
  16. package/dist/v5/conversion-api.d.ts +122 -0
  17. package/dist/v5/conversion-archive.d.ts +32 -0
  18. package/dist/v5/core.cjs +917 -0
  19. package/dist/v5/core.d.ts +16 -0
  20. package/dist/v5/core.js +899 -0
  21. package/dist/v5/feature-arrow.d.ts +36 -0
  22. package/dist/v5/i3s-mesh-conversion.d.ts +32 -0
  23. package/dist/v5/i3s-mesh-sink.d.ts +25 -0
  24. package/dist/v5/i3s-mesh-source-features.d.ts +13 -0
  25. package/dist/v5/i3s-mesh-source-material.d.ts +5 -0
  26. package/dist/v5/i3s-mesh-source.d.ts +30 -0
  27. package/dist/v5/index.cjs +73 -0
  28. package/dist/v5/index.d.ts +40 -0
  29. package/dist/v5/index.js +75 -0
  30. package/dist/v5/mesh-conversion.d.ts +71 -0
  31. package/dist/v5/mesh-draco.d.ts +8 -0
  32. package/dist/v5/mesh-features.d.ts +9 -0
  33. package/dist/v5/mesh-source-features.d.ts +21 -0
  34. package/dist/v5/mesh-source-texture.d.ts +6 -0
  35. package/dist/v5/mesh-source.d.ts +41 -0
  36. package/dist/v5/mesh-tileset-archive.d.ts +18 -0
  37. package/dist/v5/mesh-tileset-sink.d.ts +51 -0
  38. package/dist/v5/mesh.d.ts +86 -0
  39. package/dist/v5/point-cloud-conversion.d.ts +31 -0
  40. package/dist/v5/point-cloud-source-encoder.d.ts +46 -0
  41. package/dist/v5/point-cloud-source.d.ts +25 -0
  42. package/dist/v5/point-cloud-spatial.d.ts +22 -0
  43. package/dist/v5/point-cloud-tileset-sink.d.ts +28 -0
  44. package/dist/v5/point-cloud.d.ts +49 -0
  45. package/dist/v5/resource-fetcher.d.ts +18 -0
  46. package/dist/v5/resource-manifest.d.ts +58 -0
  47. package/dist/v5/spatial-conversion.d.ts +61 -0
  48. package/dist/v5/tileset-source.d.ts +21 -0
  49. package/package.json +52 -26
  50. package/src/common/README.md +5 -0
  51. package/src/index.ts +1 -2
  52. package/src/{3d-tiles-converter → v4/3d-tiles-converter}/3d-tiles-converter.ts +13 -2
  53. package/src/v4/3d-tiles-converter/helpers/validate-i3s-spatial-reference.ts +124 -0
  54. package/src/{deps-installer → v4/deps-installer}/deps-installer.ts +10 -7
  55. package/src/{i3s-converter → v4/i3s-converter}/helpers/batch-ids-extensions.ts +1 -1
  56. package/src/v4/i3s-converter/helpers/encode-string-attribute.ts +23 -0
  57. package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-converter.ts +30 -55
  58. package/src/{i3s-converter → v4/i3s-converter}/helpers/gltf-attributes.ts +5 -4
  59. package/src/{i3s-converter → v4/i3s-converter}/helpers/load-3d-tiles.ts +5 -9
  60. package/src/{i3s-converter → v4/i3s-converter}/i3s-converter.ts +6 -7
  61. package/src/{i3s-server → v4/i3s-server}/README.md +2 -2
  62. package/src/{i3s-server → v4/i3s-server}/routes/slpk-router.ts +1 -0
  63. package/src/v4/index.ts +2 -0
  64. package/src/{lib → v4/lib}/utils/compress-util.ts +8 -7
  65. package/src/v5/adapters.ts +75 -0
  66. package/src/v5/browser-sink.ts +130 -0
  67. package/src/v5/browser-source.ts +155 -0
  68. package/src/v5/browser-tileset-source.ts +328 -0
  69. package/src/v5/browser.ts +139 -0
  70. package/src/v5/conversion-api.ts +287 -0
  71. package/src/v5/conversion-archive.ts +85 -0
  72. package/src/v5/core.ts +61 -0
  73. package/src/v5/feature-arrow.ts +429 -0
  74. package/src/v5/i3s-mesh-conversion.ts +192 -0
  75. package/src/v5/i3s-mesh-sink.ts +351 -0
  76. package/src/v5/i3s-mesh-source-features.ts +279 -0
  77. package/src/v5/i3s-mesh-source-material.ts +157 -0
  78. package/src/v5/i3s-mesh-source.ts +227 -0
  79. package/src/v5/index.ts +131 -0
  80. package/src/v5/mesh-conversion.ts +230 -0
  81. package/src/v5/mesh-draco.ts +182 -0
  82. package/src/v5/mesh-features.ts +319 -0
  83. package/src/v5/mesh-source-features.ts +263 -0
  84. package/src/v5/mesh-source-texture.ts +169 -0
  85. package/src/v5/mesh-source.ts +422 -0
  86. package/src/v5/mesh-tileset-archive.ts +50 -0
  87. package/src/v5/mesh-tileset-sink.ts +324 -0
  88. package/src/v5/mesh.ts +526 -0
  89. package/src/v5/point-cloud-conversion.ts +88 -0
  90. package/src/v5/point-cloud-source-encoder.ts +116 -0
  91. package/src/v5/point-cloud-source.ts +84 -0
  92. package/src/v5/point-cloud-spatial.ts +280 -0
  93. package/src/v5/point-cloud-tileset-sink.ts +234 -0
  94. package/src/v5/point-cloud.ts +398 -0
  95. package/src/v5/resource-fetcher.ts +110 -0
  96. package/src/v5/resource-manifest.ts +213 -0
  97. package/src/v5/spatial-conversion.ts +389 -0
  98. package/src/v5/tileset-source.ts +54 -0
  99. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/3d-tiles-content-converter.ts +0 -0
  100. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/i3s-obb-to-3d-tiles-obb.ts +0 -0
  101. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/load-i3s.ts +0 -0
  102. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/texture-atlas.ts +0 -0
  103. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/json-templates/tileset.ts +0 -0
  104. /package/src/{constants.ts → v4/constants.ts} +0 -0
  105. /package/src/{converter-cli.ts → v4/converter-cli.ts} +0 -0
  106. /package/src/{i3s-converter → v4/i3s-converter}/helpers/attribute-metadata-info.ts +0 -0
  107. /package/src/{i3s-converter → v4/i3s-converter}/helpers/coordinate-converter.ts +0 -0
  108. /package/src/{i3s-converter → v4/i3s-converter}/helpers/create-scene-server-path.ts +0 -0
  109. /package/src/{i3s-converter → v4/i3s-converter}/helpers/feature-attributes.ts +0 -0
  110. /package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-attributes.ts +0 -0
  111. /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-debug.ts +0 -0
  112. /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-index-document.ts +0 -0
  113. /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-pages.ts +0 -0
  114. /package/src/{i3s-converter → v4/i3s-converter}/helpers/preprocess-3d-tiles.ts +0 -0
  115. /package/src/{i3s-converter → v4/i3s-converter}/helpers/progress.ts +0 -0
  116. /package/src/{i3s-converter → v4/i3s-converter}/helpers/tileset-traversal.ts +0 -0
  117. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/geometry-definitions.ts +0 -0
  118. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/layers.ts +0 -0
  119. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/metadata.ts +0 -0
  120. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/node.ts +0 -0
  121. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/scene-server.ts +0 -0
  122. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/shared-resources.ts +0 -0
  123. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/store.ts +0 -0
  124. /package/src/{i3s-converter → v4/i3s-converter}/types.ts +0 -0
  125. /package/src/{i3s-server → v4/i3s-server}/app.ts +0 -0
  126. /package/src/{i3s-server → v4/i3s-server}/bin/www.ts +0 -0
  127. /package/src/{i3s-server → v4/i3s-server}/certs/cert.pem +0 -0
  128. /package/src/{i3s-server → v4/i3s-server}/certs/key.pem +0 -0
  129. /package/src/{i3s-server → v4/i3s-server}/controllers/slpk-controller.ts +0 -0
  130. /package/src/{i3s-server → v4/i3s-server}/utils/create-scene-server.ts +0 -0
  131. /package/src/{i3s-server → v4/i3s-server}/utils/server-utils.ts +0 -0
  132. /package/src/{lib → v4/lib}/json-schemas/conversion-dump-json-schema.ts +0 -0
  133. /package/src/{lib → v4/lib}/utils/cli-utils.ts +0 -0
  134. /package/src/{lib → v4/lib}/utils/conversion-dump.ts +0 -0
  135. /package/src/{lib → v4/lib}/utils/file-utils.ts +0 -0
  136. /package/src/{lib → v4/lib}/utils/geometry-utils.ts +0 -0
  137. /package/src/{lib → v4/lib}/utils/lod-conversion-utils.ts +0 -0
  138. /package/src/{lib → v4/lib}/utils/queue.ts +0 -0
  139. /package/src/{lib → v4/lib}/utils/statistic-utils.ts +0 -0
  140. /package/src/{lib → v4/lib}/utils/write-queue.ts +0 -0
  141. /package/src/{pgm-loader.ts → v4/pgm-loader.ts} +0 -0
@@ -0,0 +1,351 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {Ellipsoid} from '@math.gl/geospatial';
6
+ import {GZipDecompressor, GZipCompressor} from '@loaders.gl/compression';
7
+ import {
8
+ TileConversionError,
9
+ createBoundedMemoryTileConversionSink
10
+ } from '@loaders.gl/tile-converter/v5/core';
11
+ import type {
12
+ BrowserTileConversionFile,
13
+ TileConversionSink
14
+ } from '@loaders.gl/tile-converter/v5/core';
15
+ import type {I3SMeshConversionResource} from './i3s-mesh-conversion.js';
16
+
17
+ /** Bounded flat collection of mesh nodes sharing one I3S feature/geometry schema. */
18
+ export interface I3SMeshSinkOptions {
19
+ /** Maximum compressed bytes retained, including final layer/index metadata. */
20
+ readonly maxTotalBytes: number;
21
+ /** Maximum primitive placements, from one through 64. */
22
+ readonly maxMeshes: number;
23
+ /** Maximum decompressed or generated metadata/attribute resource bytes. */
24
+ readonly maxResourceBytes: number;
25
+ }
26
+
27
+ /** Atomic multi-mesh I3S output, ready for portable SLPK packaging. */
28
+ export interface I3SMeshSink extends TileConversionSink<I3SMeshConversionResource> {
29
+ /** Finalized files only; failed or unfinished conversion exposes no output. */
30
+ getFiles(): readonly BrowserTileConversionFile[];
31
+ }
32
+
33
+ /** Geographic center and conservative axis-aligned ECEF half extents. */
34
+ interface MeshBounds {
35
+ /** Longitude, latitude and ellipsoidal height. */ readonly center: number[];
36
+ /** Half extents in meters along ECEF axes. */ readonly halfSize: number[];
37
+ /** Identity orientation from the qualified mesh writer. */ readonly quaternion: number[];
38
+ }
39
+ /** Generated paged mesh node, retaining geometry/material/attribute references. */
40
+ interface MeshNode {
41
+ /** Index in the flat collection. */ index: number;
42
+ /** Contentless root index. */ parentIndex: number;
43
+ /** Writer bounds. */ obb: MeshBounds;
44
+ /** Leaf resources and their shared definitions. */ mesh: {
45
+ /** Material and optional texture resource. */ material: {
46
+ /** Index into the layer's material definitions. */
47
+ definition: number;
48
+ /** Node holding the base-color texture, when present. */
49
+ resource?: number;
50
+ };
51
+ /** Geometry definition/resource and counts. */ geometry: {
52
+ /** Index into the layer's geometry definitions. */
53
+ definition: number;
54
+ /** Node holding the encoded geometry. */
55
+ resource: number;
56
+ /** Number of encoded vertices. */
57
+ vertexCount: number;
58
+ /** Number of feature rows. */
59
+ featureCount: number;
60
+ };
61
+ /** Feature attribute resource. */ attribute: {
62
+ /** Node holding feature attribute buffers. */
63
+ resource: number;
64
+ };
65
+ };
66
+ }
67
+ /** Layer descriptors shared by all leaves, produced by the qualified single-mesh writer. */
68
+ interface MeshLayer {
69
+ /** Source class description, retained consistently across nodes. */ readonly description?: string;
70
+ /** Storage profile and geographic extent. */ store: {
71
+ /** Geographic rectangle enclosing all nodes. */
72
+ extent: number[];
73
+ /** Legacy binary geometry layout shared by all nodes. */
74
+ defaultGeometrySchema: object;
75
+ /** Resource kinds needed by the layer. */
76
+ resourcePattern: string[];
77
+ };
78
+ /** Public feature schema. */ fields: object[];
79
+ /** Binary feature storage schema. */ attributeStorageInfo: object[];
80
+ /** Per-node materials. */ materialDefinitions: {
81
+ /** PBR material controls copied from the mesh writer. */
82
+ pbrMetallicRoughness?: {
83
+ /** Optional base-color texture profile. */
84
+ baseColorTexture?: {
85
+ /** Index into the layer's texture definitions. */
86
+ textureSetDefinitionId: number;
87
+ };
88
+ };
89
+ }[];
90
+ /** Encoded geometry schemas. */ geometryDefinitions: object[];
91
+ /** Optional texture profiles. */ textureSetDefinitions?: object[];
92
+ }
93
+
94
+ /**
95
+ * Combines independently authored mesh resources into one flat I3S layer.
96
+ * Leaves retain geographic placements, compressed geometry and attributes. A shared schema is
97
+ * required; generated object IDs must be disjoint (the conversion codec allocates them).
98
+ * Finalization rebuilds legacy indices, paged indices and enclosing ECEF bounds, including a
99
+ * second page for the 64th leaf. No source LOD hierarchy is implied. Byte gates bound retained
100
+ * resources and metadata, not peak decoder/compressor memory.
101
+ */
102
+ export function createI3SMeshSink(options: I3SMeshSinkOptions): I3SMeshSink {
103
+ const {maxMeshes, maxResourceBytes} = options;
104
+ if (
105
+ !Number.isInteger(maxMeshes) ||
106
+ maxMeshes < 1 ||
107
+ maxMeshes > 64 ||
108
+ !Number.isSafeInteger(maxResourceBytes) ||
109
+ maxResourceBytes < 1
110
+ )
111
+ throw new TileConversionError(
112
+ 'I3S_COLLECTION_OPTIONS_INVALID',
113
+ 'I3S collections require 1–64 meshes and a positive resource byte limit'
114
+ );
115
+ const memory = createBoundedMemoryTileConversionSink(options);
116
+ const compressor = new GZipCompressor({useNative: false, gzip: {mtime: 0}});
117
+ const decompressor = new GZipDecompressor({useNative: false});
118
+ const leaves: {node: MeshNode; legacy: Record<string, unknown>}[] = [];
119
+ const identifiers = new Set<string>();
120
+ const objectIdRanges: [number, number][] = [];
121
+ let layer: MeshLayer | undefined;
122
+ let schemaKey: string | undefined;
123
+ let state: 'open' | 'writing' | 'closed' = 'open';
124
+ let completed = false;
125
+ /** Decodes writer metadata and rejects documents exceeding the decompressed resource limit. */
126
+ async function readBytes(bytes: ArrayBuffer): Promise<ArrayBuffer> {
127
+ const output = await decompressor.decompress(bytes);
128
+ if (output.byteLength > maxResourceBytes)
129
+ throw new TileConversionError(
130
+ 'I3S_COLLECTION_RESOURCE_TOO_LARGE',
131
+ 'I3S metadata exceeds maxResourceBytes'
132
+ );
133
+ return output;
134
+ }
135
+ /** Reads one generated compressed JSON document. */
136
+ async function readJson<Value>(
137
+ resource: I3SMeshConversionResource,
138
+ name: string
139
+ ): Promise<Value> {
140
+ return JSON.parse(new TextDecoder().decode(await readBytes(resource.files[name]))) as Value;
141
+ }
142
+ /** Encodes final indices under the same resource and retained-output budgets. */
143
+ async function writeJson(resourceId: string, value: unknown): Promise<void> {
144
+ const bytes = new TextEncoder().encode(JSON.stringify(value));
145
+ if (bytes.byteLength > maxResourceBytes)
146
+ throw new TileConversionError(
147
+ 'I3S_COLLECTION_RESOURCE_TOO_LARGE',
148
+ 'I3S index exceeds maxResourceBytes'
149
+ );
150
+ await memory.write({
151
+ resourceId,
152
+ parts: [await compressor.compress(bytes.buffer)],
153
+ contentType: 'application/gzip'
154
+ });
155
+ }
156
+ return {
157
+ /** Exposes the whole collection atomically. */
158
+ getFiles: () => (completed ? memory.getFiles() : []),
159
+ /** Retains one leaf's binary resources and validates its shared schema and IDs. */
160
+ async write(resource, signal) {
161
+ signal?.throwIfAborted();
162
+ if (state !== 'open' || identifiers.has(resource.id) || leaves.length >= maxMeshes)
163
+ throw new TileConversionError(
164
+ 'I3S_COLLECTION_UNAVAILABLE',
165
+ 'I3S collection is busy, closed, full, or repeats a placement'
166
+ );
167
+ state = 'writing';
168
+ const authored = await readJson<MeshLayer>(resource, '3dSceneLayer.json.gz');
169
+ const key = JSON.stringify([
170
+ authored.fields,
171
+ authored.attributeStorageInfo,
172
+ authored.description,
173
+ authored.store.defaultGeometrySchema
174
+ ]);
175
+ if (schemaKey !== undefined && key !== schemaKey)
176
+ throw new TileConversionError(
177
+ 'I3S_COLLECTION_SCHEMA_MISMATCH',
178
+ 'Mesh nodes must share feature fields and the legacy geometry layout'
179
+ );
180
+ const objectIds = new DataView(
181
+ await readBytes(resource.files['nodes/1/attributes/f_0/0.bin.gz'])
182
+ );
183
+ const count = objectIds.getUint32(0, true);
184
+ const first = objectIds.getUint32(4, true);
185
+ if (
186
+ objectIds.byteLength !== 4 + count * 4 ||
187
+ !count ||
188
+ objectIdRanges.some(([start, end]) => first < end && first + count > start) ||
189
+ Array.from({length: count}, (_, index) => objectIds.getUint32(4 + index * 4, true)).some(
190
+ (value, index) => value !== first + index
191
+ )
192
+ )
193
+ throw new TileConversionError(
194
+ 'I3S_COLLECTION_IDS_INVALID',
195
+ 'Generated object IDs must be contiguous and disjoint across nodes'
196
+ );
197
+ const index = leaves.length + 1;
198
+ const node = (await readJson<{nodes: MeshNode[]}>(resource, 'nodepages/0.json.gz')).nodes[1];
199
+ const legacy = await readJson<Record<string, unknown>>(
200
+ resource,
201
+ 'nodes/1/3dNodeIndexDocument.json.gz'
202
+ );
203
+ if (
204
+ node.obb.quaternion.join(',') !== '0,0,0,1' ||
205
+ [...node.obb.center, ...node.obb.halfSize].some(value => !Number.isFinite(value)) ||
206
+ node.obb.halfSize.some(value => value < 0)
207
+ )
208
+ throw new TileConversionError(
209
+ 'I3S_COLLECTION_BOUNDS_INVALID',
210
+ 'Expected finite axis-aligned bounds from the I3S mesh writer'
211
+ );
212
+ if (!layer) {
213
+ layer = structuredClone(authored);
214
+ layer.materialDefinitions = [];
215
+ layer.geometryDefinitions = [];
216
+ layer.textureSetDefinitions = [];
217
+ schemaKey = key;
218
+ }
219
+ const material = structuredClone(authored.materialDefinitions[0]);
220
+ const texture = material.pbrMetallicRoughness?.baseColorTexture;
221
+ if (texture) {
222
+ texture.textureSetDefinitionId = layer.textureSetDefinitions!.length;
223
+ layer.textureSetDefinitions!.push(authored.textureSetDefinitions![0]);
224
+ node.mesh.material.resource = index;
225
+ }
226
+ node.index = index;
227
+ node.parentIndex = 0;
228
+ node.mesh.material.definition = layer.materialDefinitions.length;
229
+ node.mesh.geometry.definition = layer.geometryDefinitions.length;
230
+ node.mesh.geometry.resource = index;
231
+ node.mesh.attribute.resource = index;
232
+ layer.materialDefinitions.push(material);
233
+ layer.geometryDefinitions.push(authored.geometryDefinitions[0]);
234
+ layer.store.resourcePattern = [
235
+ ...new Set([...layer.store.resourcePattern, ...authored.store.resourcePattern])
236
+ ];
237
+ layer.store.extent = layer.store.extent.map((value, axis) =>
238
+ axis < 2
239
+ ? Math.min(value, authored.store.extent[axis])
240
+ : Math.max(value, authored.store.extent[axis])
241
+ );
242
+ for (const [name, bytes] of Object.entries(resource.files)) {
243
+ if (name.startsWith('nodes/1/') && !name.endsWith('3dNodeIndexDocument.json.gz'))
244
+ await memory.write(
245
+ {
246
+ resourceId: name.replace('nodes/1/', `nodes/${index}/`),
247
+ parts: [bytes],
248
+ contentType: name.endsWith('.gz')
249
+ ? 'application/gzip'
250
+ : name.endsWith('.png')
251
+ ? 'image/png'
252
+ : 'image/jpeg'
253
+ },
254
+ signal
255
+ );
256
+ }
257
+ leaves.push({node, legacy: {...legacy, id: String(index)}});
258
+ identifiers.add(resource.id);
259
+ objectIdRanges.push([first, first + count]);
260
+ state = 'open';
261
+ },
262
+ /** Rebuilds both indices and layer bounds only after every leaf has completed. */
263
+ async finalize(report) {
264
+ if (state !== 'open' || !layer || !leaves.length)
265
+ throw new TileConversionError(
266
+ 'I3S_COLLECTION_INCOMPLETE',
267
+ 'Finalization requires successfully written mesh nodes'
268
+ );
269
+ state = 'writing';
270
+ const minimum = [Infinity, Infinity, Infinity];
271
+ const maximum = [-Infinity, -Infinity, -Infinity];
272
+ for (const {node} of leaves) {
273
+ const center = Ellipsoid.WGS84.cartographicToCartesian(node.obb.center);
274
+ for (let axis = 0; axis < 3; axis++) {
275
+ minimum[axis] = Math.min(minimum[axis], center[axis] - node.obb.halfSize[axis]);
276
+ maximum[axis] = Math.max(maximum[axis], center[axis] + node.obb.halfSize[axis]);
277
+ }
278
+ }
279
+ const center = minimum.map((value, axis) => value / 2 + maximum[axis] / 2);
280
+ const geographicCenter = Array.from(Ellipsoid.WGS84.cartesianToCartographic(center));
281
+ const reconstructedCenter = Ellipsoid.WGS84.cartographicToCartesian(geographicCenter);
282
+ const obb = {
283
+ center: geographicCenter,
284
+ halfSize: minimum.map(
285
+ (value, axis) =>
286
+ maximum[axis] / 2 -
287
+ value / 2 +
288
+ Math.abs(center[axis] - reconstructedCenter[axis]) +
289
+ 1e-7
290
+ ),
291
+ quaternion: [0, 0, 0, 1]
292
+ };
293
+ if ([...obb.center, ...obb.halfSize].some(value => !Number.isFinite(value)))
294
+ throw new TileConversionError(
295
+ 'I3S_COLLECTION_BOUNDS_INVALID',
296
+ 'Collection bounds cannot be represented'
297
+ );
298
+ const mbs = [...obb.center, Math.hypot(...obb.halfSize)];
299
+ const children = leaves.map(({node}) => ({
300
+ id: String(node.index),
301
+ href: `../${node.index}`,
302
+ obb: node.obb,
303
+ mbs: [...node.obb.center, Math.hypot(...node.obb.halfSize)]
304
+ }));
305
+ await writeJson('nodes/root/3dNodeIndexDocument.json.gz', {
306
+ id: 'root',
307
+ level: 0,
308
+ version: '1',
309
+ obb,
310
+ mbs,
311
+ lodSelection: [{metricType: 'maxScreenThresholdSQ', maxError: 0}],
312
+ children
313
+ });
314
+ for (const {node, legacy} of leaves)
315
+ await writeJson(`nodes/${node.index}/3dNodeIndexDocument.json.gz`, {
316
+ ...legacy,
317
+ parentNode: {id: 'root', href: '../root', obb, mbs}
318
+ });
319
+ const nodes = [
320
+ {index: 0, obb, lodThreshold: 0, children: leaves.map(({node}) => node.index)},
321
+ ...leaves.map(({node}) => node)
322
+ ];
323
+ for (let page = 0; page * 64 < nodes.length; page++)
324
+ await writeJson(`nodepages/${page}.json.gz`, {
325
+ nodes: nodes.slice(page * 64, (page + 1) * 64)
326
+ });
327
+ if (!layer.textureSetDefinitions?.length) delete layer.textureSetDefinitions;
328
+ await writeJson('3dSceneLayer.json.gz', layer);
329
+ await writeJson('metadata.json.gz', {
330
+ folderPattern: 'BASIC',
331
+ archiveCompressionType: 'STORE',
332
+ resourceCompressionType: 'GZIP',
333
+ I3SVersion: '1.7',
334
+ nodeCount: nodes.length
335
+ });
336
+ await memory.finalize(report);
337
+ state = 'closed';
338
+ completed = true;
339
+ },
340
+ /** Discards binary resources and index metadata after any failure. */
341
+ async abort(error) {
342
+ completed = false;
343
+ state = 'closed';
344
+ leaves.length = 0;
345
+ identifiers.clear();
346
+ objectIdRanges.length = 0;
347
+ layer = undefined;
348
+ await memory.abort(error);
349
+ }
350
+ };
351
+ }
@@ -0,0 +1,279 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {I3SAttributeLoader} from '@loaders.gl/i3s';
6
+ import type {AttributeStorageInfo, I3STileContent, I3SMeshFeatures} from '@loaders.gl/i3s';
7
+ import type {TilesetContentTraversalItem, TilesetSourceMetadata} from '@loaders.gl/tiles';
8
+ import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
9
+ import {convertFeatureAttributesToArrowBatches} from './feature-arrow.js';
10
+ import type {MeshSourceFeatureOptions} from './mesh-source-features.js';
11
+ import type {MeshTilesetSourceOptions} from './mesh-source.js';
12
+
13
+ /** Explicit mapping of scalar I3S attribute resources and their geometry identifiers. */
14
+ export interface I3SMeshSourceFeatureOptions extends MeshSourceFeatureOptions {
15
+ /** Attribute column whose exact integer values identify the decoded geometry. */
16
+ readonly objectIdProperty: string;
17
+ /** Aggregate encoded attribute byte limit for each node, checked before parsing. */
18
+ readonly maxAttributeBytes: number;
19
+ }
20
+
21
+ /** Reads exact scalar/string columns and constructs complete triangle-to-Arrow-row ownership. */
22
+ export async function extractI3SMeshFeatures(
23
+ content: I3STileContent,
24
+ item: TilesetContentTraversalItem,
25
+ metadata: TilesetSourceMetadata,
26
+ options: I3SMeshSourceFeatureOptions,
27
+ readExternalResource: MeshTilesetSourceOptions['readExternalResource'],
28
+ contentUri: string | undefined,
29
+ signal?: AbortSignal
30
+ ): Promise<I3SMeshFeatures> {
31
+ const maximumAttributeBytes = options.maxAttributeBytes;
32
+ const storage = metadata.tileset.attributeStorageInfo as AttributeStorageInfo[] | undefined;
33
+ const attributeUrls = item.tile.header.attributeUrls as string[] | undefined;
34
+ if (
35
+ !options.metadataClass ||
36
+ !options.objectIdProperty ||
37
+ options.rawMetadataField ||
38
+ !Number.isSafeInteger(maximumAttributeBytes) ||
39
+ maximumAttributeBytes <= 0 ||
40
+ !storage?.length ||
41
+ attributeUrls?.length !== storage.length ||
42
+ !readExternalResource ||
43
+ new Set(storage.map(attribute => attribute.name)).size !== storage.length ||
44
+ new Set(storage.map(attribute => attribute.key)).size !== storage.length ||
45
+ !storage.some(attribute => attribute.name === options.objectIdProperty) ||
46
+ (metadata.tileset.fields || []).some(
47
+ (field: {name: string; domain?: unknown; type?: string}) =>
48
+ field.domain ||
49
+ field.type === 'esriFieldTypeDate' ||
50
+ !storage.some(attribute => attribute.name === field.name)
51
+ )
52
+ ) {
53
+ throw new TileConversionError(
54
+ 'I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED',
55
+ 'I3S attributes require an explicit scalar schema, object ID column, byte budget and resource reader; domains/dates/raw metadata need a custom mapper'
56
+ );
57
+ }
58
+ const attributeLoader = await I3SAttributeLoader.preload();
59
+ const columns: Record<string, ArrayLike<unknown>> = Object.create(null);
60
+ let inputBytes = 0;
61
+ let count: number | undefined;
62
+ for (const [index, attribute] of storage.entries()) {
63
+ signal?.throwIfAborted();
64
+ const values = attribute.objectIds ?? attribute.attributeValues;
65
+ const type = values?.valueType;
66
+ const expectedOrdering = attribute.objectIds
67
+ ? ['objectIds']
68
+ : type === 'String'
69
+ ? ['attributeByteCounts', 'attributeValues']
70
+ : ['attributeValues'];
71
+ const expectedHeader =
72
+ type === 'String'
73
+ ? [
74
+ {property: 'count', valueType: 'UInt32'},
75
+ {property: 'attributeValuesByteCount', valueType: 'UInt32'}
76
+ ]
77
+ : [{property: 'count', valueType: 'UInt32'}];
78
+ if (
79
+ !type ||
80
+ values?.valuesPerElement !== 1 ||
81
+ (attribute.objectIds && attribute.attributeValues) ||
82
+ attribute.header?.length !== expectedHeader.length ||
83
+ expectedHeader.some(
84
+ (value, headerIndex) =>
85
+ attribute.header[headerIndex]?.property !== value.property ||
86
+ attribute.header[headerIndex]?.valueType !== value.valueType
87
+ ) ||
88
+ (attribute.ordering &&
89
+ JSON.stringify(
90
+ attribute.ordering.map(value => (value === 'ObjectIds' ? 'objectIds' : value))
91
+ ) !== JSON.stringify(expectedOrdering)) ||
92
+ (type === 'String' &&
93
+ (values.encoding !== 'UTF-8' ||
94
+ attribute.attributeByteCounts?.valueType !== 'UInt32' ||
95
+ attribute.attributeByteCounts.valuesPerElement !== 1)) ||
96
+ !attributeUrls[index]
97
+ ) {
98
+ throw new TileConversionError(
99
+ 'I3S_MESH_SOURCE_ATTRIBUTE_UNSUPPORTED',
100
+ `Attribute ${attribute.name} requires a standard scalar/string resource layout`
101
+ );
102
+ }
103
+ const bytes = await readExternalResource(attributeUrls[index], contentUri, signal);
104
+ signal?.throwIfAborted();
105
+ inputBytes += bytes instanceof Uint8Array ? bytes.byteLength : Infinity;
106
+ if (!(bytes instanceof Uint8Array) || inputBytes > maximumAttributeBytes) {
107
+ throw new TileConversionError(
108
+ 'INPUT_RESOURCE_TOO_LARGE',
109
+ 'I3S node attributes exceed maxAttributeBytes'
110
+ );
111
+ }
112
+ // Copy only this view, including Uint8Array subclasses whose slice returns a pooled view.
113
+ let parsed;
114
+ try {
115
+ parsed = await attributeLoader.parse(Uint8Array.from(bytes).buffer, {
116
+ attributeName: attribute.name,
117
+ attributeType: type,
118
+ i3s: {attributeValues: 'exact'}
119
+ });
120
+ } catch (error) {
121
+ throw new TileConversionError(
122
+ 'I3S_MESH_SOURCE_ATTRIBUTE_INVALID',
123
+ `Invalid attribute ${attribute.name}: ${error instanceof Error ? error.message : String(error)}`
124
+ );
125
+ }
126
+ const column = parsed[attribute.name];
127
+ if (!column || (count !== undefined && column.length !== count) || !column.length) {
128
+ throw new TileConversionError(
129
+ 'I3S_MESH_SOURCE_ATTRIBUTE_COUNT_INVALID',
130
+ 'Every attribute must have the same nonzero row count'
131
+ );
132
+ }
133
+ if (
134
+ Array.from(column as ArrayLike<unknown>).some(
135
+ value => typeof value === 'number' && !Number.isFinite(value)
136
+ )
137
+ ) {
138
+ throw new TileConversionError(
139
+ 'I3S_MESH_SOURCE_ATTRIBUTE_UNSUPPORTED',
140
+ 'Numeric noData values require an explicit custom mapper'
141
+ );
142
+ }
143
+ count = column.length;
144
+ columns[attribute.name] = column;
145
+ }
146
+ const identifiers = columns[options.objectIdProperty];
147
+ const attributeRows = new Map<bigint, number>();
148
+ for (let index = 0; index < count!; index++) {
149
+ const identifier = identifiers[index];
150
+ if (
151
+ !(
152
+ typeof identifier === 'bigint' ||
153
+ (typeof identifier === 'number' && Number.isSafeInteger(identifier))
154
+ ) ||
155
+ attributeRows.has(BigInt(identifier as number | bigint))
156
+ ) {
157
+ throw new TileConversionError(
158
+ 'I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED',
159
+ 'Object IDs must be unique exact integers'
160
+ );
161
+ }
162
+ attributeRows.set(BigInt(identifier), index);
163
+ }
164
+ const vertexCount = content.vertexCount;
165
+ const geometryIdentifiers = content.featureIds;
166
+ const attribute = content.sourceAttributes?.id;
167
+ if (
168
+ geometryIdentifiers.length !== vertexCount ||
169
+ attribute?.size !== 1 ||
170
+ attribute.normalized ||
171
+ attribute.value.length !== vertexCount ||
172
+ Array.from(geometryIdentifiers).some((value, index) => value !== attribute.value[index])
173
+ ) {
174
+ throw new TileConversionError(
175
+ 'I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED',
176
+ 'Geometry requires one exact unnormalized object ID per vertex'
177
+ );
178
+ }
179
+ const indices = content.indices;
180
+ const triangleFeatureIndices = new Uint32Array((indices?.length ?? vertexCount) / 3);
181
+ if (content.drawRanges?.length) {
182
+ const covered = new Uint8Array(triangleFeatureIndices.length);
183
+ for (const range of content.drawRanges) {
184
+ if (
185
+ !Number.isSafeInteger(range.firstPrimitive) ||
186
+ range.firstPrimitive < 0 ||
187
+ !Number.isSafeInteger(range.primitiveCount) ||
188
+ range.primitiveCount <= 0 ||
189
+ range.firstPrimitive + range.primitiveCount > covered.length
190
+ ) {
191
+ throw new TileConversionError(
192
+ 'I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID',
193
+ 'Feature draw ranges must cover valid triangles'
194
+ );
195
+ }
196
+ for (
197
+ let triangle = range.firstPrimitive;
198
+ triangle < range.firstPrimitive + range.primitiveCount;
199
+ triangle++
200
+ ) {
201
+ const vertex = indices?.[triangle * 3] ?? triangle * 3;
202
+ if (covered[triangle] || geometryIdentifiers[vertex] !== range.featureId) {
203
+ throw new TileConversionError(
204
+ 'I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID',
205
+ 'Feature draw ranges must agree with decoded ownership without overlapping'
206
+ );
207
+ }
208
+ covered[triangle] = 1;
209
+ }
210
+ }
211
+ if (covered.some(value => value === 0)) {
212
+ throw new TileConversionError(
213
+ 'I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID',
214
+ 'Feature draw ranges must cover every triangle'
215
+ );
216
+ }
217
+ }
218
+ const usedRows = new Set<number>();
219
+ for (let triangle = 0; triangle < triangleFeatureIndices.length; triangle++) {
220
+ const corners = [0, 1, 2].map(
221
+ corner => indices?.[triangle * 3 + corner] ?? triangle * 3 + corner
222
+ );
223
+ const featureIds = corners.map(corner => geometryIdentifiers[corner]);
224
+ const row = attributeRows.get(BigInt(featureIds[0]));
225
+ if (row === undefined || featureIds.some(identifier => identifier !== featureIds[0])) {
226
+ throw new TileConversionError(
227
+ 'I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID',
228
+ 'Each triangle must reference one declared attribute row'
229
+ );
230
+ }
231
+ triangleFeatureIndices[triangle] = row;
232
+ usedRows.add(row);
233
+ }
234
+ if (usedRows.size !== count) {
235
+ throw new TileConversionError(
236
+ 'I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID',
237
+ 'Attribute rows without geometry require a separately qualified mapping'
238
+ );
239
+ }
240
+ const stableProperty = options.sourceFeatureIdProperty ?? options.objectIdProperty;
241
+ if (!Object.hasOwn(columns, stableProperty)) {
242
+ throw new TileConversionError(
243
+ 'I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED',
244
+ 'Stable identifier column is missing'
245
+ );
246
+ }
247
+ const batches = convertFeatureAttributesToArrowBatches(
248
+ Array.from({length: count!}, (_, index) => ({
249
+ featureId: columns[stableProperty][index] as string | number | bigint,
250
+ metadataClass: options.metadataClass,
251
+ properties: Object.fromEntries(
252
+ Object.entries(columns)
253
+ .filter(([name]) => name !== options.objectIdProperty && name !== stableProperty)
254
+ .map(([name, values]) => [name, values[index]])
255
+ )
256
+ })),
257
+ options
258
+ );
259
+ const stableIds = new Set<string | number | bigint>();
260
+ for (const batch of batches) {
261
+ const column = batch.data.getChild(options.featureIdField || 'feature_id')!;
262
+ for (let index = 0; index < column.length; index++) {
263
+ const identifier = column.get(index);
264
+ if (stableIds.has(identifier)) {
265
+ throw new TileConversionError(
266
+ 'I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED',
267
+ 'Stable IDs must be unique'
268
+ );
269
+ }
270
+ stableIds.add(identifier);
271
+ }
272
+ }
273
+ return {
274
+ batches,
275
+ triangleFeatureIndices,
276
+ featureIdField: options.featureIdField || 'feature_id',
277
+ integer64Encoding: options.integer64Encoding
278
+ };
279
+ }