@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
@@ -2,13 +2,29 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
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';
5
+ import type {MeshGeometry} from '@loaders.gl/core/schema';
6
+ import type {
7
+ GLTFWithBuffers,
8
+ GLTFMeshPrimitive,
9
+ GLTF_EXT_mesh_features,
10
+ GLTF_EXT_structural_metadata_GLTF
11
+ } from '@loaders.gl/gltf';
12
+ import {getGLTFAccessor, getGLTFAccessorValue} from '@loaders.gl/gltf';
13
+ import type {I3SMeshFeatures} from '@loaders.gl/tiles-3d/i3s';
8
14
  import {convertFeatureAttributesToArrowBatches} from './feature-arrow.js';
9
15
  import type {FeatureArrowBatchOptions} from './feature-arrow.js';
16
+ import {createMeshSourceFeatureOptions} from './mesh-source-schema.js';
17
+ import type {MeshSourceSchemaOptions} from './mesh-source-schema.js';
10
18
  import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
11
19
 
20
+ /** Data-only rule for absent columns and decoded noData values. */
21
+ interface MeshSourcePropertyMapping {
22
+ /** Value used for an absent column or a decoded noData cell. */
23
+ readonly defaultValue: unknown;
24
+ /** Decoded sentinel; omitted for absent-column defaults. */
25
+ readonly noData?: unknown;
26
+ }
27
+
12
28
  /** Explicit source feature mapping; target type changes must be authorized separately. */
13
29
  export interface MeshSourceFeatureOptions extends FeatureArrowBatchOptions {
14
30
  /** Batch-table class label, or the exact structural-metadata class to select. */
@@ -21,22 +37,26 @@ export interface MeshSourceFeatureOptions extends FeatureArrowBatchOptions {
21
37
 
22
38
  /** Extracts legacy batch tables or one decoded modern property table and validates geometry ownership. */
23
39
  export function extractMeshFeatures(
24
- gltf: GLTFPostprocessed,
25
- primitive: GLTFMeshPrimitivePostprocessed,
40
+ gltf: GLTFWithBuffers,
41
+ primitive: GLTFMeshPrimitive,
26
42
  payload: {
27
43
  batchTableJson?: Record<string, unknown>;
28
44
  header?: {batchLength?: number};
29
45
  featureTableJson?: Record<string, unknown>;
30
46
  },
31
47
  mesh: MeshGeometry,
32
- options?: MeshSourceFeatureOptions
48
+ options?: MeshSourceFeatureOptions | MeshSourceSchemaOptions
33
49
  ): I3SMeshFeatures | undefined {
34
- const declarations = primitive.extensions?.EXT_mesh_features?.featureIds;
35
- const metadata = gltf.extensions?.EXT_structural_metadata;
50
+ const declarations = (
51
+ primitive.extensions?.EXT_mesh_features as GLTF_EXT_mesh_features | undefined
52
+ )?.featureIds;
53
+ const metadata = gltf.json.extensions?.EXT_structural_metadata as
54
+ | GLTF_EXT_structural_metadata_GLTF
55
+ | undefined;
36
56
  const legacy = primitive.attributes._BATCHID;
37
57
  if (
38
58
  !declarations &&
39
- !legacy &&
59
+ legacy === undefined &&
40
60
  !metadata &&
41
61
  !Object.keys(payload.batchTableJson || {}).length &&
42
62
  !payload.header?.batchLength
@@ -51,19 +71,22 @@ export function extractMeshFeatures(
51
71
  if (!options)
52
72
  throw new TileConversionError(
53
73
  'MESH_FEATURE_SCHEMA_REQUIRED',
54
- 'Feature extraction requires an explicit Arrow schema and class mapping'
74
+ 'Feature extraction requires a class selection and a declared Arrow or inline source schema'
55
75
  );
56
76
  if (!options.metadataClass)
57
77
  throw new TileConversionError(
58
78
  'MESH_FEATURE_CLASS_REQUIRED',
59
79
  'Feature metadata class must be explicit'
60
80
  );
81
+ const mapping =
82
+ 'schema' in options ? options : createMeshSourceFeatureOptions(gltf.json, options);
83
+ const propertyMappings = new Map<string, MeshSourcePropertyMapping>();
61
84
  let count: number;
62
- let attribute = legacy;
85
+ let attributeReference = legacy;
63
86
  let properties: Record<string, ArrayLike<unknown>>;
64
87
  if (declarations) {
65
88
  if (
66
- legacy ||
89
+ legacy !== undefined ||
67
90
  declarations.length !== 1 ||
68
91
  metadata?.propertyTables?.length !== 1 ||
69
92
  metadata.propertyTextures?.length ||
@@ -75,6 +98,7 @@ export function extractMeshFeatures(
75
98
  );
76
99
  const declaration = declarations[0];
77
100
  if (
101
+ typeof declaration.attribute !== 'number' ||
78
102
  !Number.isInteger(declaration.attribute) ||
79
103
  declaration.attribute < 0 ||
80
104
  declaration.propertyTable !== 0 ||
@@ -86,15 +110,27 @@ export function extractMeshFeatures(
86
110
  'MESH_FEATURE_MAPPING_UNSUPPORTED',
87
111
  'Features require an attribute and property table 0 without null/texture/implicit IDs'
88
112
  );
89
- attribute = primitive.attributes[`_FEATURE_ID_${declaration.attribute}`];
113
+ attributeReference = primitive.attributes[`_FEATURE_ID_${declaration.attribute}`];
90
114
  const table = metadata.propertyTables[0];
91
- if (table.class !== options.metadataClass || table.count !== declaration.featureCount)
115
+ if (
116
+ table.class !== options.metadataClass ||
117
+ !Number.isSafeInteger(declaration.featureCount) ||
118
+ declaration.featureCount <= 0 ||
119
+ declaration.featureCount > table.count
120
+ )
92
121
  throw new TileConversionError(
93
122
  'MESH_FEATURE_CLASS_MISMATCH',
94
- 'Feature class and declared table counts must agree'
123
+ 'Feature class must agree and declared feature count must fit the property table'
95
124
  );
96
125
  count = table.count;
97
- properties = {};
126
+ // Source tables may be shared by primitives; dense output indices are uint32.
127
+ if (!Number.isSafeInteger(count) || count <= 0 || count > 0xffffffff) {
128
+ throw new TileConversionError(
129
+ 'MESH_FEATURE_IDS_INVALID',
130
+ 'Feature row count must fit uint32 indices'
131
+ );
132
+ }
133
+ properties = Object.create(null);
98
134
  const classProperties = metadata.schema?.classes?.[table.class]?.properties;
99
135
  if (!classProperties)
100
136
  throw new TileConversionError(
@@ -110,6 +146,7 @@ export function extractMeshFeatures(
110
146
  string,
111
147
  {
112
148
  type: string;
149
+ required?: boolean;
113
150
  componentType?: string;
114
151
  array?: boolean;
115
152
  noData?: unknown;
@@ -141,18 +178,36 @@ export function extractMeshFeatures(
141
178
  `Property ${name} requires a separately qualified semantic mapping`
142
179
  );
143
180
  const data = column?.data;
144
- if (data === undefined && property.default === undefined)
181
+ if (
182
+ (column !== undefined && (data === undefined || data === null)) ||
183
+ (data === undefined && property.required === true)
184
+ )
145
185
  throw new TileConversionError(
146
186
  'MESH_FEATURE_PROPERTY_UNAVAILABLE',
147
187
  `Property ${name} has not been decoded`
148
188
  );
149
- const decoded = data ?? Array.from({length: count}, () => property.default);
189
+ const integer64 = ['INT64', 'UINT64'].includes(property.componentType || '');
190
+ if (
191
+ integer64 &&
192
+ property.default !== undefined &&
193
+ (typeof property.default !== 'number' || !Number.isSafeInteger(property.default))
194
+ ) {
195
+ throw new TileConversionError(
196
+ 'MESH_FEATURE_PROPERTY_UNSUPPORTED',
197
+ `Property ${name} requires an exact 64-bit default`
198
+ );
199
+ }
200
+ const defaultValue =
201
+ integer64 && property.default !== undefined
202
+ ? BigInt(property.default as number)
203
+ : (property.default ?? null);
204
+ const decoded = data;
150
205
  if (property.noData === undefined || data === undefined) {
151
- properties[name] = decoded;
206
+ if (decoded !== undefined) properties[name] = decoded as ArrayLike<unknown>;
207
+ else propertyMappings.set(name, {defaultValue});
152
208
  continue;
153
209
  }
154
210
  const sentinel = property.noData;
155
- const integer64 = ['INT64', 'UINT64'].includes(property.componentType || '');
156
211
  if (
157
212
  (property.type === 'STRING'
158
213
  ? typeof sentinel !== 'string'
@@ -168,9 +223,9 @@ export function extractMeshFeatures(
168
223
  `Property ${name} requires a valid decoded noData mapping`
169
224
  );
170
225
  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
- );
226
+ const values = decoded as ArrayLike<unknown>;
227
+ properties[name] = values;
228
+ propertyMappings.set(name, {noData: comparable, defaultValue});
174
229
  }
175
230
  } else {
176
231
  if (metadata)
@@ -181,6 +236,15 @@ export function extractMeshFeatures(
181
236
  count = payload.header?.batchLength ?? Number(payload.featureTableJson?.BATCH_LENGTH);
182
237
  properties = (payload.batchTableJson as Record<string, ArrayLike<unknown>>) || {};
183
238
  }
239
+ const accessor =
240
+ attributeReference === undefined ? undefined : getGLTFAccessor(gltf, attributeReference);
241
+ const attribute = accessor
242
+ ? {
243
+ ...accessor,
244
+ components: accessor.type === 'SCALAR' ? 1 : Number(accessor.type.slice(3)),
245
+ value: getGLTFAccessorValue(gltf, accessor)
246
+ }
247
+ : undefined;
184
248
  const vertexCount = mesh.attributes.POSITION.value.length / 3;
185
249
  if (
186
250
  !Number.isSafeInteger(count) ||
@@ -188,15 +252,26 @@ export function extractMeshFeatures(
188
252
  !attribute ||
189
253
  attribute.components !== 1 ||
190
254
  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
- )
255
+ attribute.value.length !== vertexCount
195
256
  )
196
257
  throw new TileConversionError(
197
258
  'MESH_FEATURE_IDS_INVALID',
198
259
  'Feature IDs must be one valid property-table row index per vertex'
199
260
  );
261
+ const vertexRows = declarations ? new Set<number>() : undefined;
262
+ for (const value of attribute.value) {
263
+ if (!Number.isSafeInteger(value) || value < 0 || value >= count)
264
+ throw new TileConversionError(
265
+ 'MESH_FEATURE_IDS_INVALID',
266
+ 'Feature IDs must reference valid property-table rows'
267
+ );
268
+ vertexRows?.add(Number(value));
269
+ }
270
+ if (declarations && vertexRows!.size !== declarations[0].featureCount)
271
+ throw new TileConversionError(
272
+ 'MESH_FEATURE_IDS_INVALID',
273
+ 'Declared feature count must match distinct vertex feature IDs'
274
+ );
200
275
  const selectedAttribute = declarations ? `_FEATURE_ID_${declarations[0].attribute}` : '_BATCHID';
201
276
  if (
202
277
  Object.keys(primitive.attributes).some(
@@ -215,45 +290,84 @@ export function extractMeshFeatures(
215
290
  )
216
291
  throw new TileConversionError(
217
292
  'MESH_FEATURE_PROPERTY_UNAVAILABLE',
218
- `Property ${name} must be a decoded column matching the table count`
293
+ `Property ${name} must be a decoded column matching the feature row count`
219
294
  );
220
295
  }
221
296
  const sourceIdentifier = options.sourceFeatureIdProperty;
222
- if (sourceIdentifier && !properties[sourceIdentifier])
297
+ if (sourceIdentifier && !properties[sourceIdentifier] && !propertyMappings.has(sourceIdentifier))
223
298
  throw new TileConversionError(
224
299
  'MESH_FEATURE_ID_PROPERTY_REQUIRED',
225
300
  'Stable source identifier property is missing'
226
301
  );
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
302
  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
303
+ const cornerCount = indices?.length ?? vertexCount;
304
+ if (cornerCount % 3)
305
+ throw new TileConversionError(
306
+ 'MESH_FEATURE_IDS_INVALID',
307
+ 'Triangle geometry requires complete triples'
247
308
  );
248
- const rows = corners.map(corner => attribute.value[corner]);
309
+ const triangleFeatureIndices = new Uint32Array(cornerCount / 3);
310
+ const referencedRows = new Set<number>();
311
+ /** Reads a validated triangle corner without allocating per-triangle arrays. */
312
+ const readFeatureRow = (corner: number): number => {
313
+ if (!Number.isSafeInteger(corner) || corner < 0 || corner >= vertexCount)
314
+ throw new TileConversionError(
315
+ 'MESH_FEATURE_IDS_INVALID',
316
+ 'Triangle indices must reference valid mesh vertices'
317
+ );
318
+ return Number(attribute.value[corner]);
319
+ };
320
+ for (let triangle = 0; triangle < triangleFeatureIndices.length; triangle++) {
321
+ const firstCorner = indices?.[triangle * 3] ?? triangle * 3;
322
+ const secondCorner = indices?.[triangle * 3 + 1] ?? triangle * 3 + 1;
323
+ const thirdCorner = indices?.[triangle * 3 + 2] ?? triangle * 3 + 2;
324
+ const firstRow = readFeatureRow(firstCorner);
325
+ const secondRow = readFeatureRow(secondCorner);
326
+ const thirdRow = readFeatureRow(thirdCorner);
249
327
  // 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)
328
+ if (
329
+ (firstRow !== secondRow || firstRow !== thirdRow) &&
330
+ firstCorner !== secondCorner &&
331
+ firstCorner !== thirdCorner &&
332
+ secondCorner !== thirdCorner
333
+ )
251
334
  throw new TileConversionError(
252
335
  'MESH_FEATURE_TRIANGLE_MIXED',
253
336
  'All vertices of a nondegenerate triangle must reference the same feature row'
254
337
  );
255
- triangleFeatureIndices[triangle] = Number(rows[0]);
338
+ triangleFeatureIndices[triangle] = firstRow;
339
+ referencedRows.add(firstRow);
256
340
  }
341
+ // Sort once to retain source table order, then materialize only geometry-owned rows.
342
+ const selectedRows = [...referencedRows].sort((left, right) => left - right);
343
+ const denseRows = new Map(selectedRows.map((row, index) => [row, index]));
344
+ for (let triangle = 0; triangle < triangleFeatureIndices.length; triangle++) {
345
+ triangleFeatureIndices[triangle] = denseRows.get(triangleFeatureIndices[triangle])!;
346
+ }
347
+ /** Reads one referenced source value without expanding defaults or noData columns. */
348
+ const readProperty = (name: string, row: number): unknown => {
349
+ const values = properties[name];
350
+ const propertyMapping = propertyMappings.get(name);
351
+ if (!values) return propertyMapping?.defaultValue;
352
+ const value = values[row];
353
+ return propertyMapping && value === propertyMapping.noData
354
+ ? propertyMapping.defaultValue
355
+ : value;
356
+ };
357
+ const propertyNames = [
358
+ ...new Set([...Object.keys(properties), ...propertyMappings.keys()])
359
+ ].filter(name => name !== sourceIdentifier);
360
+ const batches = convertFeatureAttributesToArrowBatches(
361
+ selectedRows.map(row => ({
362
+ featureId: (sourceIdentifier ? readProperty(sourceIdentifier, row) : row) as
363
+ | string
364
+ | number
365
+ | bigint,
366
+ metadataClass: options.metadataClass,
367
+ properties: Object.fromEntries(propertyNames.map(name => [name, readProperty(name, row)]))
368
+ })),
369
+ mapping
370
+ );
257
371
  return {
258
372
  batches,
259
373
  triangleFeatureIndices,
@@ -0,0 +1,140 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {
6
+ GLTF,
7
+ GLTF_EXT_structural_metadata_ClassProperty,
8
+ GLTF_EXT_structural_metadata_GLTF
9
+ } from '@loaders.gl/gltf';
10
+ import type {DataType, Field} from '@loaders.gl/core/schema';
11
+ import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
12
+ import type {MeshSourceFeatureOptions} from './mesh-source-features.js';
13
+
14
+ /** Class and identifier selection for a mapping derived from an inline glTF schema. */
15
+ export type MeshSourceSchemaOptions = Omit<MeshSourceFeatureOptions, 'schema' | 'rawMetadataField'>;
16
+
17
+ /** Declared scalar storage widths; no property values participate in type selection. */
18
+ const SCALAR_TYPES: Readonly<Record<string, DataType>> = {
19
+ INT8: 'int8',
20
+ UINT8: 'uint8',
21
+ INT16: 'int16',
22
+ UINT16: 'uint16',
23
+ INT32: 'int32',
24
+ UINT32: 'uint32',
25
+ INT64: 'int64',
26
+ UINT64: 'uint64',
27
+ FLOAT32: 'float32',
28
+ FLOAT64: 'float64'
29
+ };
30
+
31
+ /**
32
+ * Builds an explicit Arrow mapping from one inline structural-metadata class and property table.
33
+ * Scalar widths, strings and optional/default/noData declarations are retained. The caller selects
34
+ * the class and stable identifier property; otherwise identifiers are uint32 table row indices.
35
+ * Arrays, enums, vectors, transformed numeric properties and external schemas fail explicitly.
36
+ * This does not authorize destination type changes or loss of source class semantics.
37
+ */
38
+ export function createMeshSourceFeatureOptions(
39
+ gltf: GLTF,
40
+ options: MeshSourceSchemaOptions
41
+ ): MeshSourceFeatureOptions {
42
+ const metadata = gltf.extensions?.EXT_structural_metadata as
43
+ | GLTF_EXT_structural_metadata_GLTF
44
+ | undefined;
45
+ const table = metadata?.propertyTables?.[0];
46
+ const properties = metadata?.schema?.classes?.[options.metadataClass]?.properties;
47
+ if (
48
+ !options.metadataClass ||
49
+ metadata?.propertyTables?.length !== 1 ||
50
+ table?.class !== options.metadataClass ||
51
+ !properties ||
52
+ !Number.isSafeInteger(table.count) ||
53
+ table.count < 1 ||
54
+ table.count > 0xffffffff ||
55
+ metadata.propertyTextures?.length ||
56
+ metadata.propertyAttributes?.length
57
+ ) {
58
+ throw new TileConversionError(
59
+ 'MESH_FEATURE_SCHEMA_REQUIRED',
60
+ 'Schema mapping requires one inline class, one matching property table and a positive uint32 row count'
61
+ );
62
+ }
63
+ if (Object.keys(table.properties || {}).some(name => !Object.hasOwn(properties, name))) {
64
+ throw new TileConversionError(
65
+ 'MESH_FEATURE_PROPERTY_UNMAPPED',
66
+ 'Property table columns must be declared in the selected class'
67
+ );
68
+ }
69
+ const fields: Field[] = Object.entries(properties).map(([name, property]) => {
70
+ const declaration = property as GLTF_EXT_structural_metadata_ClassProperty;
71
+ const column = table.properties?.[name];
72
+ if (
73
+ declaration.array ||
74
+ !['SCALAR', 'STRING'].includes(declaration.type) ||
75
+ declaration.normalized ||
76
+ declaration.offset !== undefined ||
77
+ declaration.scale !== undefined ||
78
+ column?.offset !== undefined ||
79
+ column?.scale !== undefined ||
80
+ (declaration.required &&
81
+ (declaration.noData !== undefined || declaration.default !== undefined))
82
+ ) {
83
+ throw new TileConversionError(
84
+ 'MESH_FEATURE_PROPERTY_UNSUPPORTED',
85
+ `Property ${name} requires an explicit separately qualified mapping`
86
+ );
87
+ }
88
+ const type =
89
+ declaration.type === 'STRING'
90
+ ? 'utf8'
91
+ : Object.hasOwn(SCALAR_TYPES, declaration.componentType || '')
92
+ ? SCALAR_TYPES[declaration.componentType!]
93
+ : undefined;
94
+ if (!type) {
95
+ throw new TileConversionError(
96
+ 'MESH_FEATURE_PROPERTY_UNSUPPORTED',
97
+ `Property ${name} has no supported declared scalar width`
98
+ );
99
+ }
100
+ return {
101
+ name,
102
+ type,
103
+ nullable: declaration.required !== true,
104
+ metadata: {'loaders.gl:structural-property': JSON.stringify(declaration)}
105
+ };
106
+ });
107
+ const identifierName = options.featureIdField || 'feature_id';
108
+ const sourceIdentifier = options.sourceFeatureIdProperty;
109
+ const sourceField = sourceIdentifier
110
+ ? fields.find(field => field.name === sourceIdentifier)
111
+ : undefined;
112
+ if (
113
+ (sourceIdentifier && !sourceField) ||
114
+ fields.some(field => field.name === identifierName && field !== sourceField) ||
115
+ (sourceField &&
116
+ !['utf8', 'int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'int64', 'uint64'].includes(
117
+ String(sourceField.type)
118
+ ))
119
+ ) {
120
+ throw new TileConversionError(
121
+ 'MESH_FEATURE_ID_PROPERTY_REQUIRED',
122
+ 'Select a declared string/integer identifier and a distinct output identifier name'
123
+ );
124
+ }
125
+ return {
126
+ ...options,
127
+ schema: {
128
+ fields: [
129
+ {
130
+ ...sourceField,
131
+ name: identifierName,
132
+ type: sourceField?.type || 'uint32',
133
+ nullable: false
134
+ },
135
+ ...fields.filter(field => field !== sourceField)
136
+ ],
137
+ metadata: {'loaders.gl:source-schema': JSON.stringify(metadata.schema)}
138
+ }
139
+ };
140
+ }
@@ -3,17 +3,20 @@
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
5
  import {getBinaryImageMetadata} from '@loaders.gl/images';
6
- import type {GLTFMaterialPostprocessed, GLTFImagePostprocessed} from '@loaders.gl/gltf';
6
+ import type {GLTFMaterial, GLTFImage, GLTFWithBuffers} from '@loaders.gl/gltf';
7
+ import {GLTFScenegraph} from '@loaders.gl/gltf';
7
8
  import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
8
9
  import type {MeshTileSampler, MeshTileTexture, MeshTileTextureTransform} from './mesh.js';
9
10
 
10
11
  /** Maps a buffer-view or base64 PNG/JPEG image, sampling and UV transform without decoding pixels. */
11
12
  export function mapMeshSourceTexture(
12
- textureInfo: NonNullable<GLTFMaterialPostprocessed['pbrMetallicRoughness']>['baseColorTexture']
13
+ gltf: GLTFWithBuffers,
14
+ textureInfo: NonNullable<GLTFMaterial['pbrMetallicRoughness']>['baseColorTexture']
13
15
  ): MeshTileTexture {
14
- const texture = textureInfo?.texture;
15
- const image = texture?.source;
16
- const sampler = texture?.sampler;
16
+ const texture = textureInfo ? gltf.json.textures?.[textureInfo.index] : undefined;
17
+ const image = texture?.source === undefined ? undefined : gltf.json.images?.[texture.source];
18
+ const sampler =
19
+ texture?.sampler === undefined ? undefined : gltf.json.samplers?.[texture.sampler];
17
20
  const extensions = textureInfo?.extensions || {};
18
21
  const hasTransform = Object.hasOwn(extensions, 'KHR_texture_transform');
19
22
  const transform = extensions.KHR_texture_transform;
@@ -43,8 +46,9 @@ export function mapMeshSourceTexture(
43
46
  (image.uri !== undefined && image.bufferView !== undefined) ||
44
47
  Object.keys(image.extensions || {}).length ||
45
48
  (image.uri === undefined &&
46
- (!(image.bufferView?.data instanceof Uint8Array) ||
47
- Object.keys(image.bufferView.extensions || {}).length ||
49
+ (image.bufferView === undefined ||
50
+ !gltf.json.bufferViews?.[image.bufferView] ||
51
+ Object.keys(gltf.json.bufferViews?.[image.bufferView!]?.extensions || {}).length ||
48
52
  !['image/png', 'image/jpeg'].includes(image.mimeType || ''))) ||
49
53
  (sampler &&
50
54
  (Object.keys(sampler.extensions || {}).length ||
@@ -77,7 +81,7 @@ export function mapMeshSourceTexture(
77
81
  } as MeshTileSampler)
78
82
  : undefined;
79
83
  return {
80
- ...mapMeshSourceImage(image),
84
+ ...mapMeshSourceImage(gltf, image),
81
85
  ...(hasTransform
82
86
  ? {
83
87
  transform: {
@@ -95,10 +99,14 @@ export function mapMeshSourceTexture(
95
99
 
96
100
  /** Reads self-contained encoded bytes without fetch, pixel decoding, or Node APIs. */
97
101
  function mapMeshSourceImage(
98
- image: GLTFImagePostprocessed
102
+ gltf: GLTFWithBuffers,
103
+ image: GLTFImage
99
104
  ): Pick<MeshTileTexture, 'data' | 'mimeType'> {
100
105
  if (image.uri === undefined) {
101
- return {data: image.bufferView!.data, mimeType: image.mimeType as MeshTileTexture['mimeType']};
106
+ return {
107
+ data: new GLTFScenegraph(gltf).getTypedArrayForBufferView(image.bufferView!),
108
+ mimeType: image.mimeType as MeshTileTexture['mimeType']
109
+ };
102
110
  }
103
111
  const match =
104
112
  typeof image.uri === 'string'
@@ -124,16 +132,18 @@ function mapMeshSourceImage(
124
132
 
125
133
  /** Resolves one external encoded image through an explicitly supplied application reader. */
126
134
  export async function resolveMeshSourceTexture(
127
- textureInfo: NonNullable<GLTFMaterialPostprocessed['pbrMetallicRoughness']>['baseColorTexture'],
135
+ gltf: GLTFWithBuffers,
136
+ textureInfo: NonNullable<GLTFMaterial['pbrMetallicRoughness']>['baseColorTexture'],
128
137
  readExternalResource:
129
138
  | ((uri: string, contentUri: string | undefined, signal?: AbortSignal) => Promise<Uint8Array>)
130
139
  | undefined,
131
140
  contentUri: string | undefined,
132
141
  signal?: AbortSignal
133
142
  ): Promise<MeshTileTexture> {
134
- const image = textureInfo?.texture?.source;
143
+ const texture = textureInfo ? gltf.json.textures?.[textureInfo.index] : undefined;
144
+ const image = texture?.source === undefined ? undefined : gltf.json.images?.[texture.source];
135
145
  if (!image || image.uri === undefined || image.uri.startsWith('data:'))
136
- return mapMeshSourceTexture(textureInfo);
146
+ return mapMeshSourceTexture(gltf, textureInfo);
137
147
  if (!readExternalResource || image.bufferView !== undefined)
138
148
  throw new TileConversionError(
139
149
  'MESH_SOURCE_TEXTURE_UNSUPPORTED',
@@ -158,12 +168,32 @@ export async function resolveMeshSourceTexture(
158
168
  'External images require matching PNG/JPEG headers'
159
169
  );
160
170
  // Select encoded bytes without decoding pixels or modifying the caller-owned glTF image.
161
- const resolved = {
162
- ...textureInfo,
163
- texture: {
164
- ...textureInfo!.texture,
165
- source: {...image, uri: undefined, mimeType: metadata.mimeType, bufferView: {data}}
171
+ const resolvedImage = {
172
+ ...image,
173
+ uri: undefined,
174
+ mimeType: metadata.mimeType,
175
+ bufferView: (gltf.json.bufferViews || []).length
176
+ };
177
+ const resolved: GLTFWithBuffers = {
178
+ ...gltf,
179
+ buffers: [
180
+ ...gltf.buffers,
181
+ {
182
+ arrayBuffer: data.buffer as ArrayBuffer,
183
+ byteOffset: data.byteOffset,
184
+ byteLength: data.byteLength
185
+ }
186
+ ],
187
+ json: {
188
+ ...gltf.json,
189
+ images: gltf.json.images!.map((entry, index) =>
190
+ index === texture!.source ? resolvedImage : entry
191
+ ),
192
+ bufferViews: [
193
+ ...(gltf.json.bufferViews || []),
194
+ {buffer: gltf.buffers.length, byteLength: data.byteLength}
195
+ ]
166
196
  }
167
197
  };
168
- return mapMeshSourceTexture(resolved as typeof textureInfo);
198
+ return mapMeshSourceTexture(resolved, textureInfo);
169
199
  }