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