@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,319 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {convertArrowToSchema} from '@loaders.gl/schema-utils';
6
+ import type {MeshGeometry, Field} from '@loaders.gl/schema';
7
+ import type {GLTFScenegraph} from '@loaders.gl/gltf';
8
+ import type {I3SMeshFeatures} from '@loaders.gl/i3s';
9
+ import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
10
+
11
+ /** Explicit-schema Arrow features; stable identifiers are distinct from geometry row indices. */
12
+ export type MeshTileFeatures = Pick<
13
+ I3SMeshFeatures,
14
+ 'batches' | 'triangleFeatureIndices' | 'featureIdField'
15
+ >;
16
+
17
+ /** Expands shared vertices so each complete triangle retains its own feature row through Draco. */
18
+ export function prepareMeshFeatureGeometry(
19
+ mesh: MeshGeometry,
20
+ features?: MeshTileFeatures
21
+ ): MeshGeometry {
22
+ if (!features) return mesh;
23
+ const count = features.batches.reduce((total, batch) => total + batch.data.numRows, 0);
24
+ const indices = mesh.indices?.value;
25
+ const vertexCount = indices?.length ?? mesh.attributes.POSITION.value.length / 3;
26
+ const associations = features.triangleFeatureIndices;
27
+ if (
28
+ !(associations instanceof Uint32Array) ||
29
+ associations.length !== vertexCount / 3 ||
30
+ associations.some(row => row >= count) ||
31
+ new Set(associations).size !== count
32
+ ) {
33
+ throw new TileConversionError(
34
+ 'MESH_FEATURE_ASSOCIATION_INVALID',
35
+ 'Every feature row must own a triangle and every triangle must reference a valid row'
36
+ );
37
+ }
38
+ if (count > 2 ** 24)
39
+ throw new TileConversionError(
40
+ 'MESH_FEATURE_COUNT_UNSUPPORTED',
41
+ 'Feature row indices must be exactly representable in a glTF vertex attribute'
42
+ );
43
+ const attributes: MeshGeometry['attributes'] = Object.fromEntries(
44
+ Object.entries(mesh.attributes).map(([name, attribute]) => {
45
+ const values = attribute.value;
46
+ const Constructor = values.constructor as {new (length: number): typeof values};
47
+ const output = new Constructor(vertexCount * attribute.size);
48
+ for (let vertex = 0; vertex < vertexCount; vertex++) {
49
+ const sourceVertex = indices?.[vertex] ?? vertex;
50
+ for (let component = 0; component < attribute.size; component++)
51
+ output[vertex * attribute.size + component] = Number(
52
+ values[sourceVertex * attribute.size + component]
53
+ );
54
+ }
55
+ return [name, {...attribute, value: output}];
56
+ })
57
+ );
58
+ const featureIndices =
59
+ count <= 65536 ? new Uint16Array(vertexCount) : new Float32Array(vertexCount);
60
+ for (let vertex = 0; vertex < vertexCount; vertex++)
61
+ featureIndices[vertex] = associations[Math.floor(vertex / 3)];
62
+ attributes._FEATURE_ID_0 = {value: featureIndices, size: 1};
63
+ return {...mesh, indices: undefined, attributes};
64
+ }
65
+
66
+ /** Writes one inline structural metadata table without number coercion or nullable sentinel collisions. */
67
+ export function encodeMeshFeatures(
68
+ scenegraph: GLTFScenegraph,
69
+ meshIndex: number,
70
+ features?: MeshTileFeatures
71
+ ): void {
72
+ if (!features) return;
73
+ const {batches, featureIdField} = features;
74
+ const schema = batches[0]?.schema;
75
+ const count = batches.reduce((total, batch) => total + batch.data.numRows, 0);
76
+ const metadataClass = schema?.metadata?.['loaders.gl:feature-class'] || 'features';
77
+ if (
78
+ !schema ||
79
+ !count ||
80
+ !/^[A-Za-z_][A-Za-z0-9_]*$/.test(metadataClass) ||
81
+ !schema.fields.some(field => field.name === featureIdField && field.nullable !== true) ||
82
+ new Set(schema.fields.map(field => field.name)).size !== schema.fields.length
83
+ ) {
84
+ throw new TileConversionError(
85
+ 'MESH_FEATURE_SCHEMA_INVALID',
86
+ 'Features require a single named class, unique fields, and a non-null stable identifier'
87
+ );
88
+ }
89
+ for (const batch of batches) {
90
+ const actual = convertArrowToSchema(batch.data.schema).fields;
91
+ if (
92
+ JSON.stringify(batch.schema) !== JSON.stringify(schema) ||
93
+ batch.length !== batch.data.numRows ||
94
+ JSON.stringify(actual.map(field => [field.name, field.type])) !==
95
+ JSON.stringify(schema.fields.map(field => [field.name, field.type]))
96
+ ) {
97
+ throw new TileConversionError(
98
+ 'MESH_FEATURE_SCHEMA_INVALID',
99
+ 'Declared feature schemas and Arrow column types must agree across batches'
100
+ );
101
+ }
102
+ }
103
+ const properties: Record<string, object> = Object.create(null);
104
+ const columns: Record<string, object> = Object.create(null);
105
+ for (const field of schema.fields) {
106
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(field.name))
107
+ throw new TileConversionError(
108
+ 'MESH_FEATURE_SCHEMA_INVALID',
109
+ 'Property names must be metadata identifiers'
110
+ );
111
+ const values = batches.flatMap(batch => {
112
+ const column = batch.data.getChild(field.name);
113
+ if (!column)
114
+ throw new TileConversionError(
115
+ 'MESH_FEATURE_SCHEMA_INVALID',
116
+ `Missing Arrow column ${field.name}`
117
+ );
118
+ return Array.from({length: column.length}, (_, index) => column.get(index));
119
+ });
120
+ if (
121
+ field.name === featureIdField &&
122
+ (values.some(
123
+ value =>
124
+ !(typeof value === 'string' || typeof value === 'bigint' || Number.isSafeInteger(value))
125
+ ) ||
126
+ new Set(values).size !== count)
127
+ )
128
+ throw new TileConversionError(
129
+ 'MESH_FEATURE_IDS_INVALID',
130
+ 'Stable identifiers must be unique strings or exact integers'
131
+ );
132
+ const encoded = encodeFeatureColumn(field, values);
133
+ properties[field.name] = encoded.property;
134
+ columns[field.name] = {
135
+ values: addMetadataBuffer(scenegraph, encoded.data),
136
+ ...(encoded.offsets
137
+ ? {
138
+ stringOffsets: addMetadataBuffer(scenegraph, encoded.offsets),
139
+ stringOffsetType: 'UINT32'
140
+ }
141
+ : {})
142
+ };
143
+ }
144
+ scenegraph.json.extensions ||= {};
145
+ scenegraph.json.extensions.EXT_structural_metadata = {
146
+ schema: {id: 'loaders_gl_features', classes: {[metadataClass]: {properties}}},
147
+ propertyTables: [{class: metadataClass, count, properties: columns}]
148
+ };
149
+ const primitive = scenegraph.json.meshes![meshIndex].primitives[0];
150
+ primitive.extensions ||= {};
151
+ primitive.extensions.EXT_mesh_features = {
152
+ featureIds: [{featureCount: count, attribute: 0, propertyTable: 0}]
153
+ };
154
+ scenegraph.registerUsedExtension('EXT_structural_metadata');
155
+ scenegraph.registerUsedExtension('EXT_mesh_features');
156
+ }
157
+
158
+ /** Aligns every metadata buffer to eight bytes, including INT64, UINT64 and FLOAT64 columns. */
159
+ function addMetadataBuffer(scenegraph: GLTFScenegraph, data: ArrayBufferView): number {
160
+ if (scenegraph.byteLength % 8) scenegraph.addBufferView(new Uint8Array(4));
161
+ return scenegraph.addBufferView(data);
162
+ }
163
+
164
+ /** Encodes supported scalar values and chooses an unused noData value for nullable columns. */
165
+ function encodeFeatureColumn(
166
+ field: Field,
167
+ values: unknown[]
168
+ ): {property: object; data: Uint8Array; offsets?: Uint32Array} {
169
+ const type = field.type;
170
+ const hasNull = values.includes(null);
171
+ if (hasNull && field.nullable !== true)
172
+ throw new TileConversionError(
173
+ 'MESH_FEATURE_VALUE_INVALID',
174
+ `Required field ${field.name} contains null`
175
+ );
176
+ const present = values.filter(value => value !== null);
177
+ let noData: string | number | undefined;
178
+ if (type === 'utf8') {
179
+ if (
180
+ present.some(
181
+ value =>
182
+ typeof value !== 'string' ||
183
+ new TextDecoder().decode(new TextEncoder().encode(value)) !== value
184
+ )
185
+ )
186
+ throw new TileConversionError('MESH_FEATURE_VALUE_INVALID', 'Strings require valid Unicode');
187
+ if (hasNull) {
188
+ noData = '';
189
+ const strings = new Set(present);
190
+ while (strings.has(noData)) noData += '_';
191
+ }
192
+ const encoded = values.map(value =>
193
+ new TextEncoder().encode(value === null ? (noData as string) : (value as string))
194
+ );
195
+ const offsets = new Uint32Array(values.length + 1);
196
+ for (let index = 0; index < values.length; index++)
197
+ offsets[index + 1] = offsets[index] + encoded[index].length;
198
+ const data = new Uint8Array(offsets[values.length]);
199
+ encoded.forEach((bytes, index) => data.set(bytes, offsets[index]));
200
+ return {
201
+ property: {type: 'STRING', required: field.nullable !== true, ...(hasNull ? {noData} : {})},
202
+ data,
203
+ offsets
204
+ };
205
+ }
206
+ const widths: Record<string, number> = {
207
+ int8: 1,
208
+ uint8: 1,
209
+ int16: 2,
210
+ uint16: 2,
211
+ int32: 4,
212
+ uint32: 4,
213
+ int64: 8,
214
+ uint64: 8,
215
+ float32: 4,
216
+ float64: 8
217
+ };
218
+ const width = widths[String(type)];
219
+ if (!width)
220
+ throw new TileConversionError(
221
+ 'MESH_FEATURE_TYPE_UNSUPPORTED',
222
+ `No qualified GLB property mapping for ${String(type)}`
223
+ );
224
+ const isInteger64 = type === 'int64' || type === 'uint64';
225
+ const isFloat = type === 'float32' || type === 'float64';
226
+ const minimum = isFloat
227
+ ? -Infinity
228
+ : String(type).startsWith('uint')
229
+ ? 0n
230
+ : -(1n << BigInt(width * 8 - 1));
231
+ const maximum = isFloat
232
+ ? Infinity
233
+ : (1n << BigInt(width * 8 - (String(type).startsWith('uint') ? 0 : 1))) - 1n;
234
+ if (
235
+ present.some(value =>
236
+ isInteger64
237
+ ? typeof value !== 'bigint' || value < minimum || value > maximum
238
+ : typeof value !== 'number' ||
239
+ !Number.isFinite(value) ||
240
+ (!isFloat &&
241
+ (!Number.isSafeInteger(value) || BigInt(value) < minimum || BigInt(value) > maximum))
242
+ )
243
+ )
244
+ throw new TileConversionError(
245
+ 'MESH_FEATURE_VALUE_INVALID',
246
+ `Values do not fit ${String(type)}`
247
+ );
248
+ if (hasNull) {
249
+ const used = new Set(present.map(String));
250
+ noData = 0;
251
+ while (used.has(String(noData))) noData++;
252
+ if (noData > maximum && minimum < 0) {
253
+ noData = -1;
254
+ while (used.has(String(noData))) noData--;
255
+ }
256
+ if (noData > maximum || noData < minimum)
257
+ throw new TileConversionError(
258
+ 'MESH_FEATURE_NULL_UNREPRESENTABLE',
259
+ 'No unused value remains for null without losing a real value'
260
+ );
261
+ }
262
+ const data = new Uint8Array(values.length * width);
263
+ const view = new DataView(data.buffer);
264
+ values.forEach((value, index) => {
265
+ const selected = value === null ? (isInteger64 ? BigInt(noData!) : noData) : value;
266
+ const offset = index * width;
267
+ switch (type) {
268
+ case 'int8':
269
+ view.setInt8(offset, selected as number);
270
+ break;
271
+ case 'uint8':
272
+ view.setUint8(offset, selected as number);
273
+ break;
274
+ case 'int16':
275
+ view.setInt16(offset, selected as number, true);
276
+ break;
277
+ case 'uint16':
278
+ view.setUint16(offset, selected as number, true);
279
+ break;
280
+ case 'int32':
281
+ view.setInt32(offset, selected as number, true);
282
+ break;
283
+ case 'uint32':
284
+ view.setUint32(offset, selected as number, true);
285
+ break;
286
+ case 'int64':
287
+ view.setBigInt64(offset, selected as bigint, true);
288
+ break;
289
+ case 'uint64':
290
+ view.setBigUint64(offset, selected as bigint, true);
291
+ break;
292
+ case 'float32':
293
+ view.setFloat32(offset, selected as number, true);
294
+ break;
295
+ case 'float64':
296
+ view.setFloat64(offset, selected as number, true);
297
+ break;
298
+ }
299
+ if (
300
+ isFloat &&
301
+ !Number.isFinite(
302
+ type === 'float32' ? view.getFloat32(offset, true) : view.getFloat64(offset, true)
303
+ )
304
+ )
305
+ throw new TileConversionError(
306
+ 'MESH_FEATURE_VALUE_INVALID',
307
+ 'Floating point property overflow'
308
+ );
309
+ });
310
+ return {
311
+ property: {
312
+ type: 'SCALAR',
313
+ componentType: String(type).toUpperCase(),
314
+ required: field.nullable !== true,
315
+ ...(hasNull ? {noData} : {})
316
+ },
317
+ data
318
+ };
319
+ }
@@ -0,0 +1,263 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {MeshGeometry} from '@loaders.gl/schema';
6
+ import type {GLTFPostprocessed, GLTFMeshPrimitivePostprocessed} from '@loaders.gl/gltf';
7
+ import type {I3SMeshFeatures} from '@loaders.gl/i3s';
8
+ import {convertFeatureAttributesToArrowBatches} from './feature-arrow.js';
9
+ import type {FeatureArrowBatchOptions} from './feature-arrow.js';
10
+ import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
11
+
12
+ /** Explicit source feature mapping; target type changes must be authorized separately. */
13
+ export interface MeshSourceFeatureOptions extends FeatureArrowBatchOptions {
14
+ /** Batch-table class label, or the exact structural-metadata class to select. */
15
+ readonly metadataClass: string;
16
+ /** Optional source property providing stable identifiers; otherwise row indices are stable within the content. */
17
+ readonly sourceFeatureIdProperty?: string;
18
+ /** Explicit I3S representation for 64-bit integers; omission rejects those target fields. */
19
+ readonly integer64Encoding?: 'decimal-string';
20
+ }
21
+
22
+ /** Extracts legacy batch tables or one decoded modern property table and validates geometry ownership. */
23
+ export function extractMeshFeatures(
24
+ gltf: GLTFPostprocessed,
25
+ primitive: GLTFMeshPrimitivePostprocessed,
26
+ payload: {
27
+ batchTableJson?: Record<string, unknown>;
28
+ header?: {batchLength?: number};
29
+ featureTableJson?: Record<string, unknown>;
30
+ },
31
+ mesh: MeshGeometry,
32
+ options?: MeshSourceFeatureOptions
33
+ ): I3SMeshFeatures | undefined {
34
+ const declarations = primitive.extensions?.EXT_mesh_features?.featureIds;
35
+ const metadata = gltf.extensions?.EXT_structural_metadata;
36
+ const legacy = primitive.attributes._BATCHID;
37
+ if (
38
+ !declarations &&
39
+ !legacy &&
40
+ !metadata &&
41
+ !Object.keys(payload.batchTableJson || {}).length &&
42
+ !payload.header?.batchLength
43
+ ) {
44
+ if (Object.keys(primitive.attributes).some(name => /^_FEATURE_ID_\d+$/.test(name)))
45
+ throw new TileConversionError(
46
+ 'MESH_FEATURE_MAPPING_REQUIRED',
47
+ 'Feature attributes require an EXT_mesh_features declaration'
48
+ );
49
+ return undefined;
50
+ }
51
+ if (!options)
52
+ throw new TileConversionError(
53
+ 'MESH_FEATURE_SCHEMA_REQUIRED',
54
+ 'Feature extraction requires an explicit Arrow schema and class mapping'
55
+ );
56
+ if (!options.metadataClass)
57
+ throw new TileConversionError(
58
+ 'MESH_FEATURE_CLASS_REQUIRED',
59
+ 'Feature metadata class must be explicit'
60
+ );
61
+ let count: number;
62
+ let attribute = legacy;
63
+ let properties: Record<string, ArrayLike<unknown>>;
64
+ if (declarations) {
65
+ if (
66
+ legacy ||
67
+ declarations.length !== 1 ||
68
+ metadata?.propertyTables?.length !== 1 ||
69
+ metadata.propertyTextures?.length ||
70
+ metadata.propertyAttributes?.length
71
+ )
72
+ throw new TileConversionError(
73
+ 'MESH_FEATURE_MAPPING_UNSUPPORTED',
74
+ 'Only one attribute-backed feature set and one property table are supported'
75
+ );
76
+ const declaration = declarations[0];
77
+ if (
78
+ !Number.isInteger(declaration.attribute) ||
79
+ declaration.attribute < 0 ||
80
+ declaration.propertyTable !== 0 ||
81
+ declaration.texture ||
82
+ declaration.implicit ||
83
+ declaration.nullFeatureId !== undefined
84
+ )
85
+ throw new TileConversionError(
86
+ 'MESH_FEATURE_MAPPING_UNSUPPORTED',
87
+ 'Features require an attribute and property table 0 without null/texture/implicit IDs'
88
+ );
89
+ attribute = primitive.attributes[`_FEATURE_ID_${declaration.attribute}`];
90
+ const table = metadata.propertyTables[0];
91
+ if (table.class !== options.metadataClass || table.count !== declaration.featureCount)
92
+ throw new TileConversionError(
93
+ 'MESH_FEATURE_CLASS_MISMATCH',
94
+ 'Feature class and declared table counts must agree'
95
+ );
96
+ count = table.count;
97
+ properties = {};
98
+ const classProperties = metadata.schema?.classes?.[table.class]?.properties;
99
+ if (!classProperties)
100
+ throw new TileConversionError(
101
+ 'MESH_FEATURE_SCHEMA_REQUIRED',
102
+ 'A decoded inline structural metadata class is required'
103
+ );
104
+ if (Object.keys(table.properties || {}).some(name => !Object.hasOwn(classProperties, name)))
105
+ throw new TileConversionError(
106
+ 'MESH_FEATURE_PROPERTY_UNMAPPED',
107
+ 'Property table columns must be declared in the metadata class'
108
+ );
109
+ for (const [name, property] of Object.entries(classProperties) as [
110
+ string,
111
+ {
112
+ type: string;
113
+ componentType?: string;
114
+ array?: boolean;
115
+ noData?: unknown;
116
+ default?: unknown;
117
+ normalized?: boolean;
118
+ offset?: unknown;
119
+ scale?: unknown;
120
+ }
121
+ ][]) {
122
+ const column = table.properties?.[name];
123
+ if (
124
+ !['SCALAR', 'STRING'].includes(property.type) ||
125
+ property.array ||
126
+ (property.noData !== undefined &&
127
+ (property.normalized ||
128
+ property.offset !== undefined ||
129
+ property.scale !== undefined ||
130
+ column?.offset !== undefined ||
131
+ column?.scale !== undefined)) ||
132
+ (['INT64', 'UINT64'].includes(property.componentType || '') &&
133
+ (property.normalized ||
134
+ property.offset !== undefined ||
135
+ property.scale !== undefined ||
136
+ column?.offset !== undefined ||
137
+ column?.scale !== undefined))
138
+ )
139
+ throw new TileConversionError(
140
+ 'MESH_FEATURE_PROPERTY_UNSUPPORTED',
141
+ `Property ${name} requires a separately qualified semantic mapping`
142
+ );
143
+ const data = column?.data;
144
+ if (data === undefined && property.default === undefined)
145
+ throw new TileConversionError(
146
+ 'MESH_FEATURE_PROPERTY_UNAVAILABLE',
147
+ `Property ${name} has not been decoded`
148
+ );
149
+ const decoded = data ?? Array.from({length: count}, () => property.default);
150
+ if (property.noData === undefined || data === undefined) {
151
+ properties[name] = decoded;
152
+ continue;
153
+ }
154
+ const sentinel = property.noData;
155
+ const integer64 = ['INT64', 'UINT64'].includes(property.componentType || '');
156
+ if (
157
+ (property.type === 'STRING'
158
+ ? typeof sentinel !== 'string'
159
+ : typeof sentinel !== 'number' ||
160
+ !Number.isFinite(sentinel) ||
161
+ (integer64 && !Number.isSafeInteger(sentinel))) ||
162
+ !decoded ||
163
+ (!Array.isArray(decoded) && !ArrayBuffer.isView(decoded)) ||
164
+ (decoded as ArrayLike<unknown>).length !== count
165
+ )
166
+ throw new TileConversionError(
167
+ 'MESH_FEATURE_PROPERTY_UNSUPPORTED',
168
+ `Property ${name} requires a valid decoded noData mapping`
169
+ );
170
+ const comparable = integer64 ? BigInt(sentinel as number) : sentinel;
171
+ properties[name] = Array.from(decoded as ArrayLike<unknown>, value =>
172
+ value === comparable ? (property.default ?? null) : value
173
+ );
174
+ }
175
+ } else {
176
+ if (metadata)
177
+ throw new TileConversionError(
178
+ 'MESH_FEATURE_MAPPING_UNSUPPORTED',
179
+ 'Structural metadata requires an attribute-backed feature declaration'
180
+ );
181
+ count = payload.header?.batchLength ?? Number(payload.featureTableJson?.BATCH_LENGTH);
182
+ properties = (payload.batchTableJson as Record<string, ArrayLike<unknown>>) || {};
183
+ }
184
+ const vertexCount = mesh.attributes.POSITION.value.length / 3;
185
+ if (
186
+ !Number.isSafeInteger(count) ||
187
+ count <= 0 ||
188
+ !attribute ||
189
+ attribute.components !== 1 ||
190
+ attribute.normalized ||
191
+ attribute.value.length !== vertexCount ||
192
+ Array.from(attribute.value as ArrayLike<number>).some(
193
+ value => !Number.isSafeInteger(value) || value < 0 || value >= count
194
+ )
195
+ )
196
+ throw new TileConversionError(
197
+ 'MESH_FEATURE_IDS_INVALID',
198
+ 'Feature IDs must be one valid property-table row index per vertex'
199
+ );
200
+ const selectedAttribute = declarations ? `_FEATURE_ID_${declarations[0].attribute}` : '_BATCHID';
201
+ if (
202
+ Object.keys(primitive.attributes).some(
203
+ name => (name === '_BATCHID' || /^_FEATURE_ID_\d+$/.test(name)) && name !== selectedAttribute
204
+ )
205
+ )
206
+ throw new TileConversionError(
207
+ 'MESH_FEATURE_MAPPING_UNSUPPORTED',
208
+ 'Additional feature ID sets require separate mappings'
209
+ );
210
+ for (const [name, values] of Object.entries(properties)) {
211
+ if (
212
+ !values ||
213
+ (!Array.isArray(values) && !ArrayBuffer.isView(values)) ||
214
+ values.length !== count
215
+ )
216
+ throw new TileConversionError(
217
+ 'MESH_FEATURE_PROPERTY_UNAVAILABLE',
218
+ `Property ${name} must be a decoded column matching the table count`
219
+ );
220
+ }
221
+ const sourceIdentifier = options.sourceFeatureIdProperty;
222
+ if (sourceIdentifier && !properties[sourceIdentifier])
223
+ throw new TileConversionError(
224
+ 'MESH_FEATURE_ID_PROPERTY_REQUIRED',
225
+ 'Stable source identifier property is missing'
226
+ );
227
+ const batches = convertFeatureAttributesToArrowBatches(
228
+ Array.from({length: count}, (_, index) => ({
229
+ featureId: (sourceIdentifier ? properties[sourceIdentifier][index] : index) as
230
+ | string
231
+ | number
232
+ | bigint,
233
+ metadataClass: options.metadataClass,
234
+ properties: Object.fromEntries(
235
+ Object.entries(properties)
236
+ .filter(([name]) => name !== sourceIdentifier)
237
+ .map(([name, values]) => [name, values[index]])
238
+ )
239
+ })),
240
+ options
241
+ );
242
+ const indices = mesh.indices?.value;
243
+ const triangleFeatureIndices = new Uint32Array((indices?.length ?? vertexCount) / 3);
244
+ for (let triangle = 0; triangle < triangleFeatureIndices.length; triangle++) {
245
+ const corners = [0, 1, 2].map(
246
+ corner => indices?.[triangle * 3 + corner] ?? triangle * 3 + corner
247
+ );
248
+ const rows = corners.map(corner => attribute.value[corner]);
249
+ // Repeated-index strip connectors have no surface; retain the first corner's feature row.
250
+ if (rows.some(row => row !== rows[0]) && new Set(corners).size === 3)
251
+ throw new TileConversionError(
252
+ 'MESH_FEATURE_TRIANGLE_MIXED',
253
+ 'All vertices of a nondegenerate triangle must reference the same feature row'
254
+ );
255
+ triangleFeatureIndices[triangle] = Number(rows[0]);
256
+ }
257
+ return {
258
+ batches,
259
+ triangleFeatureIndices,
260
+ featureIdField: options.featureIdField || 'feature_id',
261
+ integer64Encoding: options.integer64Encoding
262
+ };
263
+ }