@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.
- package/LICENSE +1 -1
- package/README.md +180 -4
- package/bin/converter-v5.js +2 -0
- package/dist/converter.min.cjs +172 -182
- package/dist/i3s-server/bin/i3s-server.min.cjs +113 -158
- package/dist/index.cjs +18832 -19228
- package/dist/index.js +19566 -19968
- package/dist/v5/adapters.cjs +2002 -230
- package/dist/v5/adapters.d.ts +20 -0
- package/dist/v5/adapters.js +2005 -214
- package/dist/v5/archive-source.d.ts +36 -0
- package/dist/v5/browser-tileset-source.d.ts +55 -11
- package/dist/v5/browser.cjs +18 -0
- package/dist/v5/browser.d.ts +15 -1
- package/dist/v5/browser.js +26 -0
- package/dist/v5/conversion-api.d.ts +34 -0
- package/dist/v5/converter-cli.d.ts +1 -0
- package/dist/v5/converter-cli.js +1230 -0
- package/dist/v5/core.cjs +547 -77
- package/dist/v5/core.d.ts +3 -1
- package/dist/v5/core.js +540 -70
- package/dist/v5/feature-arrow.d.ts +1 -1
- package/dist/v5/i3s-mesh-conversion.d.ts +5 -1
- package/dist/v5/i3s-mesh-source-features.d.ts +2 -2
- package/dist/v5/i3s-mesh-source-material.d.ts +1 -1
- package/dist/v5/i3s-mesh-source.d.ts +2 -2
- package/dist/v5/i3s-selection-source.d.ts +70 -0
- package/dist/v5/index.cjs +15 -0
- package/dist/v5/index.d.ts +13 -1
- package/dist/v5/index.js +22 -0
- package/dist/v5/mesh-conversion.d.ts +21 -3
- package/dist/v5/mesh-draco.d.ts +2 -2
- package/dist/v5/mesh-features.d.ts +2 -2
- package/dist/v5/mesh-hierarchy.d.ts +47 -0
- package/dist/v5/mesh-source-features.d.ts +6 -5
- package/dist/v5/mesh-source-schema.d.ts +12 -0
- package/dist/v5/mesh-source-texture.d.ts +3 -3
- package/dist/v5/mesh-source.d.ts +10 -7
- package/dist/v5/mesh-tileset-sink.d.ts +5 -0
- package/dist/v5/mesh.d.ts +1 -1
- package/dist/v5/node-cli.d.ts +9 -0
- package/dist/v5/node-conversion.d.ts +34 -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 +913 -0
- package/dist/v5/node.d.ts +6 -0
- package/dist/v5/node.js +890 -0
- package/dist/v5/point-cloud-attributes.d.ts +19 -0
- package/dist/v5/point-cloud-conversion.d.ts +1 -1
- package/dist/v5/point-cloud-copc.d.ts +44 -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 +1 -1
- package/dist/v5/point-cloud-source.d.ts +1 -1
- package/dist/v5/point-cloud-tileset.d.ts +44 -0
- package/dist/v5/point-cloud.d.ts +1 -1
- 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 +1 -1
- package/dist/v5/tileset-source.d.ts +1 -1
- package/package.json +25 -32
- package/src/index.ts +4 -1
- package/src/v4/3d-tiles-converter/3d-tiles-converter.ts +11 -12
- package/src/v4/3d-tiles-converter/helpers/3d-tiles-content-converter.ts +7 -3
- package/src/v4/3d-tiles-converter/helpers/i3s-obb-to-3d-tiles-obb.ts +2 -2
- package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +4 -6
- package/src/v4/3d-tiles-converter/helpers/texture-atlas.ts +1 -1
- package/src/v4/3d-tiles-converter/helpers/validate-i3s-spatial-reference.ts +2 -2
- package/src/v4/3d-tiles-converter/json-templates/tileset.ts +58 -39
- package/src/v4/converter-cli.ts +10 -4
- package/src/v4/deps-installer/deps-installer.ts +3 -3
- package/src/v4/i3s-converter/helpers/attribute-metadata-info.ts +1 -1
- package/src/v4/i3s-converter/helpers/batch-ids-extensions.ts +9 -10
- package/src/v4/i3s-converter/helpers/conversion-gltf.ts +108 -0
- package/src/v4/i3s-converter/helpers/coordinate-converter.ts +6 -5
- package/src/v4/i3s-converter/helpers/create-scene-server-path.ts +5 -6
- package/src/v4/i3s-converter/helpers/feature-attributes.ts +4 -4
- package/src/v4/i3s-converter/helpers/geometry-attributes.ts +1 -1
- package/src/v4/i3s-converter/helpers/geometry-converter.ts +69 -77
- package/src/v4/i3s-converter/helpers/gltf-attributes.ts +58 -74
- package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +6 -54
- package/src/v4/i3s-converter/helpers/node-debug.ts +7 -3
- package/src/v4/i3s-converter/helpers/node-index-document.ts +11 -9
- package/src/v4/i3s-converter/helpers/node-pages.ts +3 -4
- package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +2 -2
- package/src/v4/i3s-converter/helpers/tileset-traversal.ts +1 -1
- package/src/v4/i3s-converter/i3s-converter.ts +29 -31
- package/src/v4/i3s-converter/json-templates/geometry-definitions.ts +71 -66
- package/src/v4/i3s-converter/json-templates/layers.ts +97 -132
- package/src/v4/i3s-converter/json-templates/metadata.ts +31 -21
- package/src/v4/i3s-converter/json-templates/node.ts +60 -74
- package/src/v4/i3s-converter/json-templates/scene-server.ts +36 -27
- package/src/v4/i3s-converter/json-templates/shared-resources.ts +62 -123
- package/src/v4/i3s-converter/json-templates/store.ts +39 -57
- package/src/v4/i3s-converter/types.ts +4 -4
- package/src/v4/i3s-server/controllers/slpk-controller.ts +3 -3
- package/src/v4/i3s-server/utils/create-scene-server.ts +3 -3
- package/src/v4/lib/utils/builder-input.ts +8 -0
- package/src/v4/lib/utils/conversion-dump.ts +5 -1
- package/src/v4/lib/utils/file-utils.ts +1 -1
- package/src/v4/lib/utils/hash-string.ts +21 -0
- package/src/v4/lib/utils/lod-conversion-utils.ts +3 -3
- package/src/v4/pgm-loader.ts +2 -2
- package/src/v5/adapters.ts +53 -0
- package/src/v5/archive-source.ts +235 -0
- package/src/v5/browser-tileset-source.ts +418 -48
- package/src/v5/browser.ts +46 -0
- package/src/v5/conversion-api.ts +184 -17
- package/src/v5/conversion-archive.ts +2 -2
- package/src/v5/converter-cli.ts +28 -0
- package/src/v5/core.ts +7 -0
- package/src/v5/feature-arrow.ts +2 -2
- package/src/v5/i3s-mesh-conversion.ts +41 -3
- package/src/v5/i3s-mesh-sink.ts +1 -1
- package/src/v5/i3s-mesh-source-features.ts +3 -3
- package/src/v5/i3s-mesh-source-material.ts +18 -11
- package/src/v5/i3s-mesh-source.ts +3 -2
- package/src/v5/i3s-selection-source.ts +298 -0
- package/src/v5/index.ts +41 -0
- package/src/v5/mesh-conversion.ts +24 -4
- package/src/v5/mesh-draco.ts +2 -2
- package/src/v5/mesh-features.ts +3 -3
- package/src/v5/mesh-hierarchy.ts +648 -0
- package/src/v5/mesh-source-features.ts +165 -51
- package/src/v5/mesh-source-schema.ts +140 -0
- package/src/v5/mesh-source-texture.ts +49 -19
- package/src/v5/mesh-source.ts +80 -49
- package/src/v5/mesh-tileset-archive.ts +1 -1
- package/src/v5/mesh-tileset-sink.ts +2 -2
- package/src/v5/mesh.ts +1 -1
- package/src/v5/node-cli.ts +201 -0
- package/src/v5/node-conversion.ts +362 -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-attributes.ts +113 -0
- package/src/v5/point-cloud-conversion.ts +1 -1
- package/src/v5/point-cloud-copc.ts +289 -0
- package/src/v5/point-cloud-file.ts +306 -0
- package/src/v5/point-cloud-potree.ts +134 -0
- package/src/v5/point-cloud-source-encoder.ts +1 -1
- package/src/v5/point-cloud-source.ts +1 -1
- package/src/v5/point-cloud-spatial.ts +2 -2
- package/src/v5/point-cloud-tileset.ts +137 -0
- package/src/v5/point-cloud.ts +3 -3
- package/src/v5/resource-fetcher.ts +4 -1
- package/src/v5/resource-manifest.ts +40 -5
- package/src/v5/selected-mesh-source.ts +384 -0
- package/src/v5/spatial-conversion.ts +90 -19
- 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 {
|
|
7
|
-
|
|
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:
|
|
25
|
-
primitive:
|
|
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 =
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
113
|
+
attributeReference = primitive.attributes[`_FEATURE_ID_${declaration.attribute}`];
|
|
90
114
|
const table = metadata.propertyTables[0];
|
|
91
|
-
if (
|
|
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
|
|
123
|
+
'Feature class must agree and declared feature count must fit the property table'
|
|
95
124
|
);
|
|
96
125
|
count = table.count;
|
|
97
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
172
|
-
|
|
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
|
|
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
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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 (
|
|
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] =
|
|
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 {
|
|
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
|
-
|
|
13
|
+
gltf: GLTFWithBuffers,
|
|
14
|
+
textureInfo: NonNullable<GLTFMaterial['pbrMetallicRoughness']>['baseColorTexture']
|
|
13
15
|
): MeshTileTexture {
|
|
14
|
-
const texture = textureInfo?.
|
|
15
|
-
const image = texture?.source;
|
|
16
|
-
const 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
|
-
(
|
|
47
|
-
|
|
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
|
-
|
|
102
|
+
gltf: GLTFWithBuffers,
|
|
103
|
+
image: GLTFImage
|
|
99
104
|
): Pick<MeshTileTexture, 'data' | 'mimeType'> {
|
|
100
105
|
if (image.uri === undefined) {
|
|
101
|
-
return {
|
|
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
|
-
|
|
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
|
|
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
|
|
162
|
-
...
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
198
|
+
return mapMeshSourceTexture(resolved, textureInfo);
|
|
169
199
|
}
|