@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.
- package/README.md +211 -2
- package/bin/converter-v5.js +2 -0
- package/dist/converter.min.cjs +162 -163
- package/dist/i3s-server/bin/i3s-server.min.cjs +73 -73
- package/dist/index.cjs +1933 -2232
- package/dist/index.js +1925 -2235
- package/dist/v5/adapters.cjs +3186 -208
- package/dist/v5/adapters.d.ts +29 -2
- package/dist/v5/adapters.js +3202 -196
- package/dist/v5/archive-source.d.ts +36 -0
- package/dist/v5/browser-tileset-source.d.ts +55 -11
- package/dist/v5/browser.cjs +30 -0
- package/dist/v5/browser.d.ts +26 -2
- package/dist/v5/browser.js +45 -1
- package/dist/v5/conversion-api.d.ts +9 -0
- package/dist/v5/converter-cli.d.ts +1 -0
- package/dist/v5/converter-cli.js +1214 -0
- package/dist/v5/core.cjs +515 -50
- package/dist/v5/core.d.ts +2 -0
- package/dist/v5/core.js +515 -50
- package/dist/v5/i3s-mesh-conversion.d.ts +14 -4
- package/dist/v5/i3s-mesh-sink.d.ts +25 -0
- package/dist/v5/i3s-mesh-source-features.d.ts +13 -0
- package/dist/v5/i3s-mesh-source-material.d.ts +5 -0
- package/dist/v5/i3s-mesh-source.d.ts +30 -0
- package/dist/v5/i3s-selection-source.d.ts +70 -0
- package/dist/v5/index.cjs +20 -0
- package/dist/v5/index.d.ts +18 -2
- package/dist/v5/index.js +31 -1
- package/dist/v5/mesh-conversion.d.ts +29 -2
- package/dist/v5/mesh-features.d.ts +9 -0
- package/dist/v5/mesh-hierarchy.d.ts +47 -0
- package/dist/v5/mesh-source-features.d.ts +2 -1
- package/dist/v5/mesh-source-schema.d.ts +12 -0
- package/dist/v5/mesh-source-texture.d.ts +2 -0
- package/dist/v5/mesh-source.d.ts +13 -3
- package/dist/v5/mesh-tileset-sink.d.ts +5 -0
- package/dist/v5/mesh.d.ts +6 -1
- package/dist/v5/node-cli.d.ts +9 -0
- package/dist/v5/node-conversion.d.ts +32 -0
- package/dist/v5/node-feature-schema.d.ts +3 -0
- package/dist/v5/node-i3s-source.d.ts +41 -0
- package/dist/v5/node-source.d.ts +47 -0
- package/dist/v5/node.cjs +900 -0
- package/dist/v5/node.d.ts +6 -0
- package/dist/v5/node.js +877 -0
- package/dist/v5/point-cloud-file.d.ts +83 -0
- package/dist/v5/point-cloud-potree.d.ts +47 -0
- package/dist/v5/point-cloud-source-encoder.d.ts +4 -7
- package/dist/v5/point-cloud-spatial.d.ts +22 -0
- package/dist/v5/point-cloud-tileset-sink.d.ts +28 -0
- package/dist/v5/point-cloud-tileset.d.ts +44 -0
- package/dist/v5/point-cloud.d.ts +26 -1
- package/dist/v5/resource-fetcher.d.ts +18 -0
- package/dist/v5/resource-manifest.d.ts +10 -1
- package/dist/v5/selected-mesh-source.d.ts +53 -0
- package/dist/v5/spatial-conversion.d.ts +3 -1
- package/package.json +33 -25
- package/src/index.ts +4 -1
- package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +2 -4
- package/src/v4/converter-cli.ts +8 -2
- package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +3 -51
- package/src/v4/i3s-converter/helpers/node-index-document.ts +5 -2
- package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +1 -1
- package/src/v4/i3s-converter/json-templates/store.ts +2 -1
- package/src/v5/adapters.ts +69 -2
- package/src/v5/archive-source.ts +235 -0
- package/src/v5/browser-tileset-source.ts +418 -48
- package/src/v5/browser.ts +81 -2
- package/src/v5/conversion-api.ts +69 -9
- package/src/v5/converter-cli.ts +28 -0
- package/src/v5/core.ts +6 -0
- package/src/v5/i3s-mesh-conversion.ts +97 -19
- package/src/v5/i3s-mesh-sink.ts +351 -0
- package/src/v5/i3s-mesh-source-features.ts +279 -0
- package/src/v5/i3s-mesh-source-material.ts +157 -0
- package/src/v5/i3s-mesh-source.ts +228 -0
- package/src/v5/i3s-selection-source.ts +298 -0
- package/src/v5/index.ts +60 -2
- package/src/v5/mesh-conversion.ts +61 -11
- package/src/v5/mesh-draco.ts +47 -3
- package/src/v5/mesh-features.ts +319 -0
- package/src/v5/mesh-hierarchy.ts +648 -0
- package/src/v5/mesh-source-features.ts +73 -10
- package/src/v5/mesh-source-schema.ts +134 -0
- package/src/v5/mesh-source-texture.ts +47 -0
- package/src/v5/mesh-source.ts +99 -25
- package/src/v5/mesh-tileset-sink.ts +1 -1
- package/src/v5/mesh.ts +16 -7
- package/src/v5/node-cli.ts +197 -0
- package/src/v5/node-conversion.ts +345 -0
- package/src/v5/node-feature-schema.ts +155 -0
- package/src/v5/node-i3s-source.ts +181 -0
- package/src/v5/node-source.ts +328 -0
- package/src/v5/node.ts +16 -0
- package/src/v5/point-cloud-conversion.ts +9 -1
- package/src/v5/point-cloud-file.ts +306 -0
- package/src/v5/point-cloud-potree.ts +130 -0
- package/src/v5/point-cloud-source-encoder.ts +31 -20
- package/src/v5/point-cloud-spatial.ts +280 -0
- package/src/v5/point-cloud-tileset-sink.ts +234 -0
- package/src/v5/point-cloud-tileset.ts +137 -0
- package/src/v5/point-cloud.ts +159 -32
- package/src/v5/resource-fetcher.ts +113 -0
- package/src/v5/resource-manifest.ts +40 -5
- package/src/v5/selected-mesh-source.ts +384 -0
- 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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
217
|
-
corner =>
|
|
277
|
+
const corners = [0, 1, 2].map(
|
|
278
|
+
corner => indices?.[triangle * 3 + corner] ?? triangle * 3 + corner
|
|
218
279
|
);
|
|
219
|
-
|
|
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
|
+
}
|
package/src/v5/mesh-source.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
169
|
-
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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
|
|
321
|
-
material: GLTFMaterialPostprocessed | undefined
|
|
322
|
-
|
|
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.
|
|
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.
|
|
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
|
-
? {
|
|
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 =
|
|
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
|
-
/**
|
|
250
|
-
function
|
|
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;
|