@loaders.gl/tile-converter 5.0.0-alpha.10 → 5.0.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +211 -2
  2. package/bin/converter-v5.js +2 -0
  3. package/dist/converter.min.cjs +162 -163
  4. package/dist/i3s-server/bin/i3s-server.min.cjs +73 -73
  5. package/dist/index.cjs +1933 -2232
  6. package/dist/index.js +1925 -2235
  7. package/dist/v5/adapters.cjs +3186 -208
  8. package/dist/v5/adapters.d.ts +29 -2
  9. package/dist/v5/adapters.js +3202 -196
  10. package/dist/v5/archive-source.d.ts +36 -0
  11. package/dist/v5/browser-tileset-source.d.ts +55 -11
  12. package/dist/v5/browser.cjs +30 -0
  13. package/dist/v5/browser.d.ts +26 -2
  14. package/dist/v5/browser.js +45 -1
  15. package/dist/v5/conversion-api.d.ts +9 -0
  16. package/dist/v5/converter-cli.d.ts +1 -0
  17. package/dist/v5/converter-cli.js +1214 -0
  18. package/dist/v5/core.cjs +515 -50
  19. package/dist/v5/core.d.ts +2 -0
  20. package/dist/v5/core.js +515 -50
  21. package/dist/v5/i3s-mesh-conversion.d.ts +14 -4
  22. package/dist/v5/i3s-mesh-sink.d.ts +25 -0
  23. package/dist/v5/i3s-mesh-source-features.d.ts +13 -0
  24. package/dist/v5/i3s-mesh-source-material.d.ts +5 -0
  25. package/dist/v5/i3s-mesh-source.d.ts +30 -0
  26. package/dist/v5/i3s-selection-source.d.ts +70 -0
  27. package/dist/v5/index.cjs +20 -0
  28. package/dist/v5/index.d.ts +18 -2
  29. package/dist/v5/index.js +31 -1
  30. package/dist/v5/mesh-conversion.d.ts +29 -2
  31. package/dist/v5/mesh-features.d.ts +9 -0
  32. package/dist/v5/mesh-hierarchy.d.ts +47 -0
  33. package/dist/v5/mesh-source-features.d.ts +2 -1
  34. package/dist/v5/mesh-source-schema.d.ts +12 -0
  35. package/dist/v5/mesh-source-texture.d.ts +2 -0
  36. package/dist/v5/mesh-source.d.ts +13 -3
  37. package/dist/v5/mesh-tileset-sink.d.ts +5 -0
  38. package/dist/v5/mesh.d.ts +6 -1
  39. package/dist/v5/node-cli.d.ts +9 -0
  40. package/dist/v5/node-conversion.d.ts +32 -0
  41. package/dist/v5/node-feature-schema.d.ts +3 -0
  42. package/dist/v5/node-i3s-source.d.ts +41 -0
  43. package/dist/v5/node-source.d.ts +47 -0
  44. package/dist/v5/node.cjs +900 -0
  45. package/dist/v5/node.d.ts +6 -0
  46. package/dist/v5/node.js +877 -0
  47. package/dist/v5/point-cloud-file.d.ts +83 -0
  48. package/dist/v5/point-cloud-potree.d.ts +47 -0
  49. package/dist/v5/point-cloud-source-encoder.d.ts +4 -7
  50. package/dist/v5/point-cloud-spatial.d.ts +22 -0
  51. package/dist/v5/point-cloud-tileset-sink.d.ts +28 -0
  52. package/dist/v5/point-cloud-tileset.d.ts +44 -0
  53. package/dist/v5/point-cloud.d.ts +26 -1
  54. package/dist/v5/resource-fetcher.d.ts +18 -0
  55. package/dist/v5/resource-manifest.d.ts +10 -1
  56. package/dist/v5/selected-mesh-source.d.ts +53 -0
  57. package/dist/v5/spatial-conversion.d.ts +3 -1
  58. package/package.json +33 -25
  59. package/src/index.ts +4 -1
  60. package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +2 -4
  61. package/src/v4/converter-cli.ts +8 -2
  62. package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +3 -51
  63. package/src/v4/i3s-converter/helpers/node-index-document.ts +5 -2
  64. package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +1 -1
  65. package/src/v4/i3s-converter/json-templates/store.ts +2 -1
  66. package/src/v5/adapters.ts +69 -2
  67. package/src/v5/archive-source.ts +235 -0
  68. package/src/v5/browser-tileset-source.ts +418 -48
  69. package/src/v5/browser.ts +81 -2
  70. package/src/v5/conversion-api.ts +69 -9
  71. package/src/v5/converter-cli.ts +28 -0
  72. package/src/v5/core.ts +6 -0
  73. package/src/v5/i3s-mesh-conversion.ts +97 -19
  74. package/src/v5/i3s-mesh-sink.ts +351 -0
  75. package/src/v5/i3s-mesh-source-features.ts +279 -0
  76. package/src/v5/i3s-mesh-source-material.ts +157 -0
  77. package/src/v5/i3s-mesh-source.ts +228 -0
  78. package/src/v5/i3s-selection-source.ts +298 -0
  79. package/src/v5/index.ts +60 -2
  80. package/src/v5/mesh-conversion.ts +61 -11
  81. package/src/v5/mesh-draco.ts +47 -3
  82. package/src/v5/mesh-features.ts +319 -0
  83. package/src/v5/mesh-hierarchy.ts +648 -0
  84. package/src/v5/mesh-source-features.ts +73 -10
  85. package/src/v5/mesh-source-schema.ts +134 -0
  86. package/src/v5/mesh-source-texture.ts +47 -0
  87. package/src/v5/mesh-source.ts +99 -25
  88. package/src/v5/mesh-tileset-sink.ts +1 -1
  89. package/src/v5/mesh.ts +16 -7
  90. package/src/v5/node-cli.ts +197 -0
  91. package/src/v5/node-conversion.ts +345 -0
  92. package/src/v5/node-feature-schema.ts +155 -0
  93. package/src/v5/node-i3s-source.ts +181 -0
  94. package/src/v5/node-source.ts +328 -0
  95. package/src/v5/node.ts +16 -0
  96. package/src/v5/point-cloud-conversion.ts +9 -1
  97. package/src/v5/point-cloud-file.ts +306 -0
  98. package/src/v5/point-cloud-potree.ts +130 -0
  99. package/src/v5/point-cloud-source-encoder.ts +31 -20
  100. package/src/v5/point-cloud-spatial.ts +280 -0
  101. package/src/v5/point-cloud-tileset-sink.ts +234 -0
  102. package/src/v5/point-cloud-tileset.ts +137 -0
  103. package/src/v5/point-cloud.ts +159 -32
  104. package/src/v5/resource-fetcher.ts +113 -0
  105. package/src/v5/resource-manifest.ts +40 -5
  106. package/src/v5/selected-mesh-source.ts +384 -0
  107. package/src/v5/spatial-conversion.ts +161 -6
@@ -7,6 +7,8 @@ import type {GLTFPostprocessed, GLTFMeshPrimitivePostprocessed} from '@loaders.g
7
7
  import type {I3SMeshFeatures} from '@loaders.gl/i3s';
8
8
  import {convertFeatureAttributesToArrowBatches} from './feature-arrow.js';
9
9
  import type {FeatureArrowBatchOptions} from './feature-arrow.js';
10
+ import {createMeshSourceFeatureOptions} from './mesh-source-schema.js';
11
+ import type {MeshSourceSchemaOptions} from './mesh-source-schema.js';
10
12
  import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
11
13
 
12
14
  /** Explicit source feature mapping; target type changes must be authorized separately. */
@@ -29,7 +31,7 @@ export function extractMeshFeatures(
29
31
  featureTableJson?: Record<string, unknown>;
30
32
  },
31
33
  mesh: MeshGeometry,
32
- options?: MeshSourceFeatureOptions
34
+ options?: MeshSourceFeatureOptions | MeshSourceSchemaOptions
33
35
  ): I3SMeshFeatures | undefined {
34
36
  const declarations = primitive.extensions?.EXT_mesh_features?.featureIds;
35
37
  const metadata = gltf.extensions?.EXT_structural_metadata;
@@ -51,13 +53,14 @@ export function extractMeshFeatures(
51
53
  if (!options)
52
54
  throw new TileConversionError(
53
55
  'MESH_FEATURE_SCHEMA_REQUIRED',
54
- 'Feature extraction requires an explicit Arrow schema and class mapping'
56
+ 'Feature extraction requires a class selection and a declared Arrow or inline source schema'
55
57
  );
56
58
  if (!options.metadataClass)
57
59
  throw new TileConversionError(
58
60
  'MESH_FEATURE_CLASS_REQUIRED',
59
61
  'Feature metadata class must be explicit'
60
62
  );
63
+ const mapping = 'schema' in options ? options : createMeshSourceFeatureOptions(gltf, options);
61
64
  let count: number;
62
65
  let attribute = legacy;
63
66
  let properties: Record<string, ArrayLike<unknown>>;
@@ -94,6 +97,17 @@ export function extractMeshFeatures(
94
97
  'Feature class and declared table counts must agree'
95
98
  );
96
99
  count = table.count;
100
+ // Qualified mesh writers require every feature row to own geometry. Check before defaults allocate.
101
+ if (
102
+ !Number.isSafeInteger(count) ||
103
+ count <= 0 ||
104
+ count > mesh.attributes.POSITION.value.length / 3
105
+ ) {
106
+ throw new TileConversionError(
107
+ 'MESH_FEATURE_IDS_INVALID',
108
+ 'Feature row count exceeds the available mesh vertices'
109
+ );
110
+ }
97
111
  properties = {};
98
112
  const classProperties = metadata.schema?.classes?.[table.class]?.properties;
99
113
  if (!classProperties)
@@ -110,6 +124,7 @@ export function extractMeshFeatures(
110
124
  string,
111
125
  {
112
126
  type: string;
127
+ required?: boolean;
113
128
  componentType?: string;
114
129
  array?: boolean;
115
130
  noData?: unknown;
@@ -123,7 +138,12 @@ export function extractMeshFeatures(
123
138
  if (
124
139
  !['SCALAR', 'STRING'].includes(property.type) ||
125
140
  property.array ||
126
- property.noData !== undefined ||
141
+ (property.noData !== undefined &&
142
+ (property.normalized ||
143
+ property.offset !== undefined ||
144
+ property.scale !== undefined ||
145
+ column?.offset !== undefined ||
146
+ column?.scale !== undefined)) ||
127
147
  (['INT64', 'UINT64'].includes(property.componentType || '') &&
128
148
  (property.normalized ||
129
149
  property.offset !== undefined ||
@@ -136,12 +156,53 @@ export function extractMeshFeatures(
136
156
  `Property ${name} requires a separately qualified semantic mapping`
137
157
  );
138
158
  const data = column?.data;
139
- if (data === undefined && property.default === undefined)
159
+ if (
160
+ (column !== undefined && (data === undefined || data === null)) ||
161
+ (data === undefined && property.required === true)
162
+ )
140
163
  throw new TileConversionError(
141
164
  'MESH_FEATURE_PROPERTY_UNAVAILABLE',
142
165
  `Property ${name} has not been decoded`
143
166
  );
144
- properties[name] = data ?? Array.from({length: count}, () => property.default);
167
+ const integer64 = ['INT64', 'UINT64'].includes(property.componentType || '');
168
+ if (
169
+ integer64 &&
170
+ property.default !== undefined &&
171
+ (typeof property.default !== 'number' || !Number.isSafeInteger(property.default))
172
+ ) {
173
+ throw new TileConversionError(
174
+ 'MESH_FEATURE_PROPERTY_UNSUPPORTED',
175
+ `Property ${name} requires an exact 64-bit default`
176
+ );
177
+ }
178
+ const defaultValue =
179
+ integer64 && property.default !== undefined
180
+ ? BigInt(property.default as number)
181
+ : (property.default ?? null);
182
+ const decoded = data ?? Array.from({length: count}, () => defaultValue);
183
+ if (property.noData === undefined || data === undefined) {
184
+ properties[name] = decoded;
185
+ continue;
186
+ }
187
+ const sentinel = property.noData;
188
+ if (
189
+ (property.type === 'STRING'
190
+ ? typeof sentinel !== 'string'
191
+ : typeof sentinel !== 'number' ||
192
+ !Number.isFinite(sentinel) ||
193
+ (integer64 && !Number.isSafeInteger(sentinel))) ||
194
+ !decoded ||
195
+ (!Array.isArray(decoded) && !ArrayBuffer.isView(decoded)) ||
196
+ (decoded as ArrayLike<unknown>).length !== count
197
+ )
198
+ throw new TileConversionError(
199
+ 'MESH_FEATURE_PROPERTY_UNSUPPORTED',
200
+ `Property ${name} requires a valid decoded noData mapping`
201
+ );
202
+ const comparable = integer64 ? BigInt(sentinel as number) : sentinel;
203
+ properties[name] = Array.from(decoded as ArrayLike<unknown>, value =>
204
+ value === comparable ? defaultValue : value
205
+ );
145
206
  }
146
207
  } else {
147
208
  if (metadata)
@@ -208,18 +269,20 @@ export function extractMeshFeatures(
208
269
  .map(([name, values]) => [name, values[index]])
209
270
  )
210
271
  })),
211
- options
272
+ mapping
212
273
  );
213
274
  const indices = mesh.indices?.value;
214
275
  const triangleFeatureIndices = new Uint32Array((indices?.length ?? vertexCount) / 3);
215
276
  for (let triangle = 0; triangle < triangleFeatureIndices.length; triangle++) {
216
- const rows = [0, 1, 2].map(
217
- corner => attribute.value[indices?.[triangle * 3 + corner] ?? triangle * 3 + corner]
277
+ const corners = [0, 1, 2].map(
278
+ corner => indices?.[triangle * 3 + corner] ?? triangle * 3 + corner
218
279
  );
219
- if (rows.some(row => row !== rows[0]))
280
+ const rows = corners.map(corner => attribute.value[corner]);
281
+ // Repeated-index strip connectors have no surface; retain the first corner's feature row.
282
+ if (rows.some(row => row !== rows[0]) && new Set(corners).size === 3)
220
283
  throw new TileConversionError(
221
284
  'MESH_FEATURE_TRIANGLE_MIXED',
222
- 'All vertices of a triangle must reference the same feature row'
285
+ 'All vertices of a nondegenerate triangle must reference the same feature row'
223
286
  );
224
287
  triangleFeatureIndices[triangle] = Number(rows[0]);
225
288
  }
@@ -0,0 +1,134 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {GLTFPostprocessed, GLTF_EXT_structural_metadata_ClassProperty} from '@loaders.gl/gltf';
6
+ import type {DataType, Field} from '@loaders.gl/schema';
7
+ import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
8
+ import type {MeshSourceFeatureOptions} from './mesh-source-features.js';
9
+
10
+ /** Class and identifier selection for a mapping derived from an inline glTF schema. */
11
+ export type MeshSourceSchemaOptions = Omit<MeshSourceFeatureOptions, 'schema' | 'rawMetadataField'>;
12
+
13
+ /** Declared scalar storage widths; no property values participate in type selection. */
14
+ const SCALAR_TYPES: Readonly<Record<string, DataType>> = {
15
+ INT8: 'int8',
16
+ UINT8: 'uint8',
17
+ INT16: 'int16',
18
+ UINT16: 'uint16',
19
+ INT32: 'int32',
20
+ UINT32: 'uint32',
21
+ INT64: 'int64',
22
+ UINT64: 'uint64',
23
+ FLOAT32: 'float32',
24
+ FLOAT64: 'float64'
25
+ };
26
+
27
+ /**
28
+ * Builds an explicit Arrow mapping from one inline structural-metadata class and property table.
29
+ * Scalar widths, strings and optional/default/noData declarations are retained. The caller selects
30
+ * the class and stable identifier property; otherwise identifiers are uint32 table row indices.
31
+ * Arrays, enums, vectors, transformed numeric properties and external schemas fail explicitly.
32
+ * This does not authorize destination type changes or loss of source class semantics.
33
+ */
34
+ export function createMeshSourceFeatureOptions(
35
+ gltf: GLTFPostprocessed,
36
+ options: MeshSourceSchemaOptions
37
+ ): MeshSourceFeatureOptions {
38
+ const metadata = gltf.extensions?.EXT_structural_metadata;
39
+ const table = metadata?.propertyTables?.[0];
40
+ const properties = metadata?.schema?.classes?.[options.metadataClass]?.properties;
41
+ if (
42
+ !options.metadataClass ||
43
+ metadata?.propertyTables?.length !== 1 ||
44
+ table?.class !== options.metadataClass ||
45
+ !properties ||
46
+ !Number.isSafeInteger(table.count) ||
47
+ table.count < 1 ||
48
+ table.count > 0xffffffff ||
49
+ metadata.propertyTextures?.length ||
50
+ metadata.propertyAttributes?.length
51
+ ) {
52
+ throw new TileConversionError(
53
+ 'MESH_FEATURE_SCHEMA_REQUIRED',
54
+ 'Schema mapping requires one inline class, one matching property table and a positive uint32 row count'
55
+ );
56
+ }
57
+ if (Object.keys(table.properties || {}).some(name => !Object.hasOwn(properties, name))) {
58
+ throw new TileConversionError(
59
+ 'MESH_FEATURE_PROPERTY_UNMAPPED',
60
+ 'Property table columns must be declared in the selected class'
61
+ );
62
+ }
63
+ const fields: Field[] = Object.entries(properties).map(([name, property]) => {
64
+ const declaration = property as GLTF_EXT_structural_metadata_ClassProperty;
65
+ const column = table.properties?.[name];
66
+ if (
67
+ declaration.array ||
68
+ !['SCALAR', 'STRING'].includes(declaration.type) ||
69
+ declaration.normalized ||
70
+ declaration.offset !== undefined ||
71
+ declaration.scale !== undefined ||
72
+ column?.offset !== undefined ||
73
+ column?.scale !== undefined ||
74
+ (declaration.required &&
75
+ (declaration.noData !== undefined || declaration.default !== undefined))
76
+ ) {
77
+ throw new TileConversionError(
78
+ 'MESH_FEATURE_PROPERTY_UNSUPPORTED',
79
+ `Property ${name} requires an explicit separately qualified mapping`
80
+ );
81
+ }
82
+ const type =
83
+ declaration.type === 'STRING'
84
+ ? 'utf8'
85
+ : Object.hasOwn(SCALAR_TYPES, declaration.componentType || '')
86
+ ? SCALAR_TYPES[declaration.componentType!]
87
+ : undefined;
88
+ if (!type) {
89
+ throw new TileConversionError(
90
+ 'MESH_FEATURE_PROPERTY_UNSUPPORTED',
91
+ `Property ${name} has no supported declared scalar width`
92
+ );
93
+ }
94
+ return {
95
+ name,
96
+ type,
97
+ nullable: declaration.required !== true,
98
+ metadata: {'loaders.gl:structural-property': JSON.stringify(declaration)}
99
+ };
100
+ });
101
+ const identifierName = options.featureIdField || 'feature_id';
102
+ const sourceIdentifier = options.sourceFeatureIdProperty;
103
+ const sourceField = sourceIdentifier
104
+ ? fields.find(field => field.name === sourceIdentifier)
105
+ : undefined;
106
+ if (
107
+ (sourceIdentifier && !sourceField) ||
108
+ fields.some(field => field.name === identifierName && field !== sourceField) ||
109
+ (sourceField &&
110
+ !['utf8', 'int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'int64', 'uint64'].includes(
111
+ String(sourceField.type)
112
+ ))
113
+ ) {
114
+ throw new TileConversionError(
115
+ 'MESH_FEATURE_ID_PROPERTY_REQUIRED',
116
+ 'Select a declared string/integer identifier and a distinct output identifier name'
117
+ );
118
+ }
119
+ return {
120
+ ...options,
121
+ schema: {
122
+ fields: [
123
+ {
124
+ ...sourceField,
125
+ name: identifierName,
126
+ type: sourceField?.type || 'uint32',
127
+ nullable: false
128
+ },
129
+ ...fields.filter(field => field !== sourceField)
130
+ ],
131
+ metadata: {'loaders.gl:source-schema': JSON.stringify(metadata.schema)}
132
+ }
133
+ };
134
+ }
@@ -2,6 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
+ import {getBinaryImageMetadata} from '@loaders.gl/images';
5
6
  import type {GLTFMaterialPostprocessed, GLTFImagePostprocessed} from '@loaders.gl/gltf';
6
7
  import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
7
8
  import type {MeshTileSampler, MeshTileTexture, MeshTileTextureTransform} from './mesh.js';
@@ -120,3 +121,49 @@ function mapMeshSourceImage(
120
121
  mimeType: match[1] as MeshTileTexture['mimeType']
121
122
  };
122
123
  }
124
+
125
+ /** Resolves one external encoded image through an explicitly supplied application reader. */
126
+ export async function resolveMeshSourceTexture(
127
+ textureInfo: NonNullable<GLTFMaterialPostprocessed['pbrMetallicRoughness']>['baseColorTexture'],
128
+ readExternalResource:
129
+ | ((uri: string, contentUri: string | undefined, signal?: AbortSignal) => Promise<Uint8Array>)
130
+ | undefined,
131
+ contentUri: string | undefined,
132
+ signal?: AbortSignal
133
+ ): Promise<MeshTileTexture> {
134
+ const image = textureInfo?.texture?.source;
135
+ if (!image || image.uri === undefined || image.uri.startsWith('data:'))
136
+ return mapMeshSourceTexture(textureInfo);
137
+ if (!readExternalResource || image.bufferView !== undefined)
138
+ throw new TileConversionError(
139
+ 'MESH_SOURCE_TEXTURE_UNSUPPORTED',
140
+ 'External images require an explicit bounded resource reader'
141
+ );
142
+ signal?.throwIfAborted();
143
+ const data = await readExternalResource(image.uri, contentUri, signal);
144
+ signal?.throwIfAborted();
145
+ let metadata;
146
+ try {
147
+ metadata = getBinaryImageMetadata(new DataView(data.buffer, data.byteOffset, data.byteLength));
148
+ } catch {
149
+ /* Report an unsupported encoded image consistently. */
150
+ }
151
+ if (
152
+ !metadata ||
153
+ !['image/png', 'image/jpeg'].includes(metadata.mimeType) ||
154
+ (image.mimeType !== undefined && image.mimeType !== metadata.mimeType)
155
+ )
156
+ throw new TileConversionError(
157
+ 'MESH_SOURCE_TEXTURE_UNSUPPORTED',
158
+ 'External images require matching PNG/JPEG headers'
159
+ );
160
+ // 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}}
166
+ }
167
+ };
168
+ return mapMeshSourceTexture(resolved as typeof textureInfo);
169
+ }
@@ -17,10 +17,11 @@ import {createTilesetConversionSource} from '@loaders.gl/tile-converter/v5/core'
17
17
  import type {TilesetConversionSourceOptions} from '@loaders.gl/tile-converter/v5/core';
18
18
  import type {MeshConversionInput} from './mesh-conversion.js';
19
19
  import type {MeshTileMaterial} from './mesh.js';
20
- import {validateMeshGeometry} from './mesh.js';
21
- import {mapMeshSourceTexture} from './mesh-source-texture.js';
20
+ import {validateMeshGeometry, validateMeshIndices} from './mesh.js';
21
+ import {resolveMeshSourceTexture} from './mesh-source-texture.js';
22
22
  import {extractMeshFeatures} from './mesh-source-features.js';
23
23
  import type {MeshSourceFeatureOptions} from './mesh-source-features.js';
24
+ import type {MeshSourceSchemaOptions} from './mesh-source-schema.js';
24
25
  import type {I3SMeshFeatures} from '@loaders.gl/i3s';
25
26
  export type {MeshSourceFeatureOptions} from './mesh-source-features.js';
26
27
 
@@ -28,12 +29,20 @@ export type {MeshSourceFeatureOptions} from './mesh-source-features.js';
28
29
  export interface MeshSourceResource extends MeshConversionInput {
29
30
  /** Arrow features associated with the selected primitive; IDs are distinct from table row indices. */
30
31
  readonly features?: I3SMeshFeatures;
32
+ /** Original I3S layer semantics, retained for destination policy and diagnostics. */
33
+ readonly sourceLayerType?: '3DObject' | 'IntegratedMesh';
31
34
  }
32
35
 
33
36
  /** Narrow source profile and content lifetime controls. */
34
37
  export interface MeshTilesetSourceOptions extends TilesetConversionSourceOptions {
35
- /** Required when the source has batch/structural metadata; no schema is inferred. */
36
- readonly features?: MeshSourceFeatureOptions;
38
+ /** Required for feature-bearing content; omit schema only to map an inline structural metadata class. */
39
+ readonly features?: MeshSourceFeatureOptions | MeshSourceSchemaOptions;
40
+ /** Explicit bounded reader for external encoded images, relative to the content URI. */
41
+ readonly readExternalResource?: (
42
+ uri: string,
43
+ contentUri: string | undefined,
44
+ signal?: AbortSignal
45
+ ) => Promise<Uint8Array>;
37
46
  }
38
47
 
39
48
  /**
@@ -41,13 +50,14 @@ export interface MeshTilesetSourceOptions extends TilesetConversionSourceOptions
41
50
  * optional buffer-view or base64 data-URI PNG/JPEG images, TEXCOORD_0, glTF sampling and
42
51
  * KHR_texture_transform controls on TEXCOORD_0. Decode content with
43
52
  * `gltf.excludeExtensions: {KHR_texture_transform: false}` to retain authored UVs/transforms.
53
+ * Triangle strips are expanded with alternating winding; the glTF loader already expands fans.
44
54
  * Shared traversal retains placement identity and unloads content according to the supplied policy.
45
55
  * The adapter applies node transforms, glTF up-axis correction, RTC translation, and tile placement
46
56
  * once, retaining Float64 absolute positions and inverse-transpose unit normals. Animation, skins,
47
57
  * morphs, instancing, broader texture semantics, unknown extensions, non-affine/mirrored
48
58
  * placements, and
49
59
  * metadata outside the declared feature profile fail explicitly. Multiple resources can be read;
50
- * the single-mesh I3S sink rejects a second resource and aborts the entire output.
60
+ * collection sinks accept bounded multiple placements; the compatibility single-mesh sink rejects a second resource.
51
61
  * @param tileset - Dedicated native EPSG:4978 runtime with decoded glTF content enabled.
52
62
  * @param options - Explicit feature schema/mapping and optional decoded-content cleanup.
53
63
  * @returns Portable conversion source; COLOR_0 is preserved for GLB, while I3S rejects vertex colors.
@@ -165,11 +175,18 @@ export function createMeshTilesetConversionSource(
165
175
  'A default scene or exactly one scene is required'
166
176
  );
167
177
  let primitiveIndex = 0;
168
- for (const selected of traverseMeshNodes(roots, placement)) {
169
- const resource = extractPrimitive(
178
+ const primitives = traverseMeshNodes(roots, placement);
179
+ let current = primitives.next();
180
+ while (!current.done) {
181
+ const selected = current.value;
182
+ const next = primitives.next();
183
+ const resource = await extractPrimitive(
170
184
  selected.primitive,
171
185
  selected.transform,
172
- `${item.tile.id}/${content.index}/${primitiveIndex++}`
186
+ `${item.tile.id}/${content.index}/${primitiveIndex++}`,
187
+ options.readExternalResource,
188
+ content.uri,
189
+ signal
173
190
  );
174
191
  const features = extractMeshFeatures(
175
192
  gltf,
@@ -178,7 +195,12 @@ export function createMeshTilesetConversionSource(
178
195
  resource.mesh,
179
196
  options.features
180
197
  );
181
- yield {...resource, features};
198
+ yield {
199
+ ...resource,
200
+ features,
201
+ contentPrimitive: {index: primitiveIndex - 1, last: Boolean(next.done)}
202
+ };
203
+ current = next;
182
204
  }
183
205
  if (!primitiveIndex)
184
206
  throw new TileConversionError(
@@ -229,11 +251,14 @@ function* traverseMeshNodes(
229
251
  }
230
252
 
231
253
  /** Copies one packed primitive, places it once, and derives a useful ECEF local origin. */
232
- function extractPrimitive(
254
+ async function extractPrimitive(
233
255
  primitive: GLTFMeshPrimitivePostprocessed,
234
256
  transform: Matrix4,
235
- id: string
236
- ): MeshSourceResource {
257
+ id: string,
258
+ readExternalResource: MeshTilesetSourceOptions['readExternalResource'],
259
+ contentUri: string | undefined,
260
+ signal?: AbortSignal
261
+ ): Promise<MeshSourceResource> {
237
262
  if (
238
263
  primitive.targets?.length ||
239
264
  Object.keys(primitive.extensions || {}).some(name => name !== 'EXT_mesh_features')
@@ -271,11 +296,9 @@ function extractPrimitive(
271
296
  const mesh = validateMeshGeometry(
272
297
  {
273
298
  topology: 'triangle-list',
274
- mode: primitive.mode ?? 4,
299
+ mode: primitive.mode === 5 ? 4 : (primitive.mode ?? 4),
275
300
  attributes,
276
- indices: primitive.indices
277
- ? {value: primitive.indices.value as MeshAttribute['value'], size: 1}
278
- : undefined
301
+ indices: createTriangleIndices(primitive, (attributes.POSITION?.value.length ?? 0) / 3)
279
302
  },
280
303
  true
281
304
  );
@@ -312,19 +335,28 @@ function extractPrimitive(
312
335
  id,
313
336
  mesh,
314
337
  origin: minimum.map((value, axis) => value / 2 + maximum[axis] / 2) as [number, number, number],
315
- material: mapMaterial(primitive.material)
338
+ material: await mapMeshSourceMaterial(
339
+ primitive.material,
340
+ readExternalResource,
341
+ contentUri,
342
+ signal
343
+ )
316
344
  };
317
345
  }
318
346
 
319
347
  /** Maps supported PBR controls and an embedded base-color image, rejecting unmapped semantics. */
320
- function mapMaterial(
321
- material: GLTFMaterialPostprocessed | undefined
322
- ): MeshTileMaterial | undefined {
348
+ export async function mapMeshSourceMaterial(
349
+ material: GLTFMaterialPostprocessed | undefined,
350
+ readExternalResource: MeshTilesetSourceOptions['readExternalResource'],
351
+ contentUri: string | undefined,
352
+ signal?: AbortSignal
353
+ ): Promise<MeshTileMaterial | undefined> {
323
354
  if (!material) return undefined;
324
355
  const pbr = material.pbrMetallicRoughness;
325
356
  if (
326
- Object.keys(material).some(
327
- name =>
357
+ Object.entries(material).some(
358
+ ([name, value]) =>
359
+ value !== undefined &&
328
360
  ![
329
361
  'id',
330
362
  'name',
@@ -336,8 +368,9 @@ function mapMaterial(
336
368
  'extras'
337
369
  ].includes(name)
338
370
  ) ||
339
- Object.keys(pbr || {}).some(
340
- name =>
371
+ Object.entries(pbr || {}).some(
372
+ ([name, value]) =>
373
+ value !== undefined &&
341
374
  !['baseColorFactor', 'baseColorTexture', 'metallicFactor', 'roughnessFactor'].includes(name)
342
375
  ) ||
343
376
  material.emissiveFactor?.some(value => value !== 0)
@@ -349,7 +382,14 @@ function mapMaterial(
349
382
  return {
350
383
  baseColorFactor: pbr?.baseColorFactor as [number, number, number, number] | undefined,
351
384
  ...('baseColorTexture' in (pbr || {})
352
- ? {baseColorTexture: mapMeshSourceTexture(pbr!.baseColorTexture)}
385
+ ? {
386
+ baseColorTexture: await resolveMeshSourceTexture(
387
+ pbr!.baseColorTexture,
388
+ readExternalResource,
389
+ contentUri,
390
+ signal
391
+ )
392
+ }
353
393
  : {}),
354
394
  metallicFactor: pbr?.metallicFactor,
355
395
  roughnessFactor: pbr?.roughnessFactor,
@@ -358,3 +398,37 @@ function mapMaterial(
358
398
  doubleSided: material.doubleSided
359
399
  };
360
400
  }
401
+
402
+ /** Expands a strip without changing vertex storage, feature IDs, source indices, or parity. */
403
+ function createTriangleIndices(
404
+ primitive: GLTFMeshPrimitivePostprocessed,
405
+ vertexCount: number
406
+ ): MeshAttribute | undefined {
407
+ const attribute = primitive.indices
408
+ ? {
409
+ value: primitive.indices.value as MeshAttribute['value'],
410
+ size: primitive.indices.components,
411
+ normalized: primitive.indices.normalized
412
+ }
413
+ : undefined;
414
+ if (primitive.mode !== 5) return attribute;
415
+ const sourceIndices = validateMeshIndices(attribute, vertexCount, 5);
416
+ const indexCount = sourceIndices?.length ?? vertexCount;
417
+ if (!Number.isSafeInteger(indexCount) || indexCount < 3) {
418
+ throw new TileConversionError(
419
+ 'MESH_TRIANGLE_COUNT_INVALID',
420
+ 'Triangle strips require at least three vertices'
421
+ );
422
+ }
423
+ const IndexArray = vertexCount < 65536 ? Uint16Array : Uint32Array;
424
+ const indices = new IndexArray((indexCount - 2) * 3);
425
+ for (let triangle = 0; triangle < indexCount - 2; triangle++) {
426
+ // Match the glTF strip equation, including degenerate connectors; they advance parity too.
427
+ const second = triangle + 1 + (triangle % 2);
428
+ const third = triangle + 2 - (triangle % 2);
429
+ indices[triangle * 3] = sourceIndices?.[triangle] ?? triangle;
430
+ indices[triangle * 3 + 1] = sourceIndices?.[second] ?? second;
431
+ indices[triangle * 3 + 2] = sourceIndices?.[third] ?? third;
432
+ }
433
+ return {value: indices, size: 1};
434
+ }
@@ -96,7 +96,7 @@ export function createSingleMeshTilesetSink(
96
96
  }
97
97
 
98
98
  /** Builds placement and local bounds for a trusted single-mesh codec output. */
99
- function createMeshTileset(
99
+ export function createMeshTileset(
100
100
  resource: EncodedMeshConversionResource,
101
101
  geometricError: number
102
102
  ): Omit<Tiles3DTilesetJSON, 'root'> & {root: Omit<Tiles3DTileJSON, 'children'>} {
package/src/v5/mesh.ts CHANGED
@@ -5,6 +5,11 @@
5
5
  import {GLTFScenegraph, GLTFWriter} from '@loaders.gl/gltf';
6
6
  import {getBinaryImageMetadata} from '@loaders.gl/images';
7
7
  import type {MeshAttribute, MeshGeometry} from '@loaders.gl/schema';
8
+ import {
9
+ prepareMeshFeatureGeometry,
10
+ encodeMeshFeatures,
11
+ type MeshTileFeatures
12
+ } from './mesh-features.js';
8
13
  import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
9
14
 
10
15
  /** Explicit glTF wrapping and filtering for the selected base-color image. */
@@ -61,6 +66,8 @@ export interface MeshTileMaterial {
61
66
 
62
67
  /** Optional appearance for the single-mesh GLB profile. */
63
68
  export interface MeshTileOptions {
69
+ /** One explicit Arrow feature table with a row index per triangle. */
70
+ readonly features?: MeshTileFeatures;
64
71
  /** Explicitly selected material; other texture maps and material extensions are unsupported. */
65
72
  readonly material?: MeshTileMaterial;
66
73
  }
@@ -82,7 +89,7 @@ export interface MeshTileOptions {
82
89
  * @returns An embedded-buffer GLB containing one mesh, node, and default scene.
83
90
  */
84
91
  export function encodeMeshTile(mesh: MeshGeometry, options: MeshTileOptions = {}): ArrayBuffer {
85
- const geometry = validateMeshGeometry(mesh);
92
+ const geometry = prepareMeshFeatureGeometry(validateMeshGeometry(mesh), options.features);
86
93
  const {scenegraph, materialIndex} = createMeshTileScenegraph(options, geometry);
87
94
  const meshIndex = scenegraph.addMesh({
88
95
  attributes: geometry.attributes,
@@ -90,6 +97,7 @@ export function encodeMeshTile(mesh: MeshGeometry, options: MeshTileOptions = {}
90
97
  material: materialIndex,
91
98
  mode: 4
92
99
  });
100
+ encodeMeshFeatures(scenegraph, meshIndex, options.features);
93
101
  return finalizeMeshTileScenegraph(scenegraph, meshIndex);
94
102
  }
95
103
 
@@ -203,7 +211,7 @@ export function validateMeshGeometry(
203
211
  ...(normalized ? {normalized: true} : {})
204
212
  };
205
213
  }
206
- const indices = getMeshIndices(mesh.indices, positions.length / 3);
214
+ const indices = validateMeshIndices(mesh.indices, positions.length / 3);
207
215
  if (!indices && positions.length % 9 !== 0) {
208
216
  throw new TileConversionError(
209
217
  'MESH_TRIANGLE_COUNT_INVALID',
@@ -246,10 +254,11 @@ function getFloatAttribute(
246
254
  return attribute.value;
247
255
  }
248
256
 
249
- /** Reads optional packed triangle indices and checks glTF range and primitive-restart rules. */
250
- function getMeshIndices(
257
+ /** Validates packed indices for triangle lists or strips, including glTF primitive-restart rules. */
258
+ export function validateMeshIndices(
251
259
  attribute: MeshAttribute | undefined,
252
- vertexCount: number
260
+ vertexCount: number,
261
+ primitiveMode: 4 | 5 = 4
253
262
  ): Uint8Array | Uint16Array | Uint32Array | undefined {
254
263
  if (!attribute) return undefined;
255
264
  validateAttributeLayout(attribute, 'indices');
@@ -267,10 +276,10 @@ function getMeshIndices(
267
276
  'Indices must contain packed unsigned integer scalars'
268
277
  );
269
278
  }
270
- if (values.length === 0 || values.length % 3 !== 0) {
279
+ if (primitiveMode === 5 ? values.length < 3 : values.length === 0 || values.length % 3 !== 0) {
271
280
  throw new TileConversionError(
272
281
  'MESH_TRIANGLE_COUNT_INVALID',
273
- 'Indices must contain complete triangles'
282
+ 'Indices must contain complete triangles or at least three strip vertices'
274
283
  );
275
284
  }
276
285
  const restartIndex = 2 ** (values.BYTES_PER_ELEMENT * 8) - 1;