@loaders.gl/tile-converter 4.5.3 → 5.0.0-alpha.10
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 +464 -0
- package/dist/converter.min.cjs +199 -260
- package/dist/i3s-server/bin/i3s-server.min.cjs +106 -341
- package/dist/index.cjs +38482 -0
- package/dist/index.js +38453 -0
- package/dist/v5/adapters.cjs +1992 -0
- package/dist/v5/adapters.d.ts +22 -0
- package/dist/v5/adapters.js +1972 -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 +64 -0
- package/dist/v5/browser.d.ts +34 -0
- package/dist/v5/browser.js +57 -0
- package/dist/v5/conversion-api.d.ts +122 -0
- package/dist/v5/conversion-archive.d.ts +32 -0
- package/dist/v5/core.cjs +863 -0
- package/dist/v5/core.d.ts +16 -0
- package/dist/v5/core.js +845 -0
- package/dist/v5/feature-arrow.d.ts +36 -0
- package/dist/v5/i3s-mesh-conversion.d.ts +26 -0
- package/dist/v5/index.cjs +63 -0
- package/dist/v5/index.d.ts +32 -0
- package/dist/v5/index.js +59 -0
- package/dist/v5/mesh-conversion.d.ts +62 -0
- package/dist/v5/mesh-draco.d.ts +8 -0
- package/dist/v5/mesh-source-features.d.ts +21 -0
- package/dist/v5/mesh-source-texture.d.ts +4 -0
- package/dist/v5/mesh-source.d.ts +34 -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 +81 -0
- package/dist/v5/point-cloud-conversion.d.ts +31 -0
- package/dist/v5/point-cloud-source-encoder.d.ts +49 -0
- package/dist/v5/point-cloud-source.d.ts +25 -0
- package/dist/v5/point-cloud.d.ts +24 -0
- package/dist/v5/resource-manifest.d.ts +58 -0
- package/dist/v5/spatial-conversion.d.ts +59 -0
- package/dist/v5/tileset-source.d.ts +21 -0
- package/package.json +53 -27
- 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 +14 -3
- package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/3d-tiles-content-converter.ts +2 -2
- package/src/v4/3d-tiles-converter/helpers/validate-i3s-spatial-reference.ts +124 -0
- package/src/{3d-tiles-converter → v4/3d-tiles-converter}/json-templates/tileset.ts +4 -4
- package/src/{converter-cli.ts → v4/converter-cli.ts} +4 -4
- package/src/{deps-installer → v4/deps-installer}/deps-installer.ts +10 -7
- package/src/{i3s-converter → v4/i3s-converter}/helpers/attribute-metadata-info.ts +1 -1
- 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-attributes.ts +1 -1
- package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-converter.ts +34 -59
- package/src/{i3s-converter → v4/i3s-converter}/helpers/gltf-attributes.ts +7 -6
- package/src/{i3s-converter → v4/i3s-converter}/helpers/load-3d-tiles.ts +5 -9
- package/src/{i3s-converter → v4/i3s-converter}/helpers/node-debug.ts +5 -6
- package/src/{i3s-converter → v4/i3s-converter}/i3s-converter.ts +16 -19
- package/src/{i3s-converter → v4/i3s-converter}/json-templates/geometry-definitions.ts +4 -4
- package/src/{i3s-converter → v4/i3s-converter}/json-templates/layers.ts +6 -6
- package/src/{i3s-converter → v4/i3s-converter}/json-templates/node.ts +1 -1
- package/src/{i3s-converter → v4/i3s-converter}/json-templates/scene-server.ts +1 -1
- package/src/{i3s-converter → v4/i3s-converter}/json-templates/shared-resources.ts +1 -1
- package/src/{i3s-converter → v4/i3s-converter}/json-templates/store.ts +1 -1
- package/src/{i3s-server → v4/i3s-server}/README.md +2 -2
- package/src/{i3s-server → v4/i3s-server}/controllers/slpk-controller.ts +1 -1
- package/src/{i3s-server → v4/i3s-server}/routes/slpk-router.ts +2 -1
- package/src/v4/index.ts +2 -0
- package/src/{lib → v4/lib}/utils/compress-util.ts +8 -7
- package/src/{lib → v4/lib}/utils/conversion-dump.ts +4 -4
- package/src/{lib → v4/lib}/utils/file-utils.ts +2 -2
- package/src/{lib → v4/lib}/utils/lod-conversion-utils.ts +2 -4
- package/src/v5/adapters.ts +56 -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 +105 -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 +152 -0
- package/src/v5/index.ts +99 -0
- package/src/v5/mesh-conversion.ts +200 -0
- package/src/v5/mesh-draco.ts +138 -0
- package/src/v5/mesh-source-features.ts +232 -0
- package/src/v5/mesh-source-texture.ts +122 -0
- package/src/v5/mesh-source.ts +360 -0
- package/src/v5/mesh-tileset-archive.ts +50 -0
- package/src/v5/mesh-tileset-sink.ts +324 -0
- package/src/v5/mesh.ts +517 -0
- package/src/v5/point-cloud-conversion.ts +80 -0
- package/src/v5/point-cloud-source-encoder.ts +105 -0
- package/src/v5/point-cloud-source.ts +84 -0
- package/src/v5/point-cloud.ts +271 -0
- package/src/v5/resource-manifest.ts +213 -0
- package/src/v5/spatial-conversion.ts +305 -0
- package/src/v5/tileset-source.ts +54 -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/{constants.ts → v4/constants.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/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/metadata.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}/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/geometry-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,1972 @@
|
|
|
1
|
+
// src/v5/feature-arrow.ts
|
|
2
|
+
import { ArrowTableBuilder } from "@loaders.gl/schema-utils";
|
|
3
|
+
import { TileConversionError } from "@loaders.gl/tile-converter/v5/core";
|
|
4
|
+
var DEFAULT_FEATURE_ID_FIELD = "feature_id";
|
|
5
|
+
var FEATURE_CLASS_METADATA_KEY = "loaders.gl:feature-class";
|
|
6
|
+
function convertFeatureAttributesToArrowBatches(features, options) {
|
|
7
|
+
const featureIdField = options.featureIdField || DEFAULT_FEATURE_ID_FIELD;
|
|
8
|
+
const batchSize = options.batchSize ?? 65536;
|
|
9
|
+
if (!Number.isInteger(batchSize) || batchSize <= 0) {
|
|
10
|
+
throw new TileConversionError(
|
|
11
|
+
"INVALID_FEATURE_BATCH_SIZE",
|
|
12
|
+
"Feature Arrow batch size must be a positive integer"
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
const featureClasses = new Set(features.map((feature) => feature.metadataClass));
|
|
16
|
+
if (featureClasses.size > 1) {
|
|
17
|
+
throw new TileConversionError(
|
|
18
|
+
"MULTIPLE_FEATURE_CLASSES",
|
|
19
|
+
"Build separate Arrow batches for each source metadata class"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
const metadataClass = features[0]?.metadataClass;
|
|
23
|
+
const schema = makeFeatureSchema(
|
|
24
|
+
options.schema,
|
|
25
|
+
featureIdField,
|
|
26
|
+
options.rawMetadataField,
|
|
27
|
+
metadataClass
|
|
28
|
+
);
|
|
29
|
+
const fieldsByName = new Map(schema.fields.map((field) => [field.name, field]));
|
|
30
|
+
const propertyFieldNames = new Set(
|
|
31
|
+
schema.fields.filter((field) => field.name !== featureIdField && field.name !== options.rawMetadataField).map((field) => field.name)
|
|
32
|
+
);
|
|
33
|
+
for (const feature of features) {
|
|
34
|
+
validateFeatureIdentifier(feature.featureId, fieldsByName.get(featureIdField)?.type);
|
|
35
|
+
if (feature.rawMetadata && !options.rawMetadataField) {
|
|
36
|
+
throw new TileConversionError(
|
|
37
|
+
"RAW_METADATA_FIELD_REQUIRED",
|
|
38
|
+
"A rawMetadataField must be declared to preserve encoded source metadata"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
for (const propertyName of Object.keys(feature.properties)) {
|
|
42
|
+
if (!propertyFieldNames.has(propertyName)) {
|
|
43
|
+
throw new TileConversionError(
|
|
44
|
+
"FEATURE_PROPERTY_NOT_MAPPED",
|
|
45
|
+
`Feature property "${propertyName}" is not declared in the Arrow schema`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const batches = [];
|
|
51
|
+
for (let batchStart = 0; batchStart < features.length; batchStart += batchSize) {
|
|
52
|
+
const tableBuilder = new ArrowTableBuilder(schema);
|
|
53
|
+
const batchFeatures = features.slice(batchStart, batchStart + batchSize);
|
|
54
|
+
for (const feature of batchFeatures) {
|
|
55
|
+
const row = {
|
|
56
|
+
...feature.properties,
|
|
57
|
+
[featureIdField]: feature.featureId
|
|
58
|
+
};
|
|
59
|
+
if (options.rawMetadataField) {
|
|
60
|
+
row[options.rawMetadataField] = feature.rawMetadata || null;
|
|
61
|
+
}
|
|
62
|
+
for (const field of schema.fields) {
|
|
63
|
+
if (row[field.name] === void 0) {
|
|
64
|
+
row[field.name] = null;
|
|
65
|
+
}
|
|
66
|
+
validateFeatureValue(field, row[field.name], field.name);
|
|
67
|
+
}
|
|
68
|
+
tableBuilder.addObjectRow(row);
|
|
69
|
+
}
|
|
70
|
+
const batch = tableBuilder.finishBatch();
|
|
71
|
+
if (batch) {
|
|
72
|
+
batches.push(batch);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return batches;
|
|
76
|
+
}
|
|
77
|
+
function makeFeatureSchema(sourceSchema, featureIdField, rawMetadataField, metadataClass) {
|
|
78
|
+
const fieldsByName = new Map(sourceSchema.fields.map((field) => [field.name, field]));
|
|
79
|
+
if (fieldsByName.size !== sourceSchema.fields.length) {
|
|
80
|
+
throw new TileConversionError(
|
|
81
|
+
"DUPLICATE_FEATURE_FIELDS",
|
|
82
|
+
"Arrow schema fields must have unique names"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
for (const field of sourceSchema.fields) {
|
|
86
|
+
validateFeatureSchemaType(field);
|
|
87
|
+
}
|
|
88
|
+
const featureIdType = fieldsByName.get(featureIdField)?.type;
|
|
89
|
+
if (!featureIdType) {
|
|
90
|
+
throw new TileConversionError(
|
|
91
|
+
"FEATURE_ID_FIELD_REQUIRED",
|
|
92
|
+
`Arrow schema must declare the feature identifier field "${featureIdField}"`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (rawMetadataField && !fieldsByName.has(rawMetadataField)) {
|
|
96
|
+
throw new TileConversionError(
|
|
97
|
+
"RAW_METADATA_FIELD_NOT_DECLARED",
|
|
98
|
+
`Arrow schema does not declare the raw metadata field "${rawMetadataField}"`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (rawMetadataField === featureIdField) {
|
|
102
|
+
throw new TileConversionError(
|
|
103
|
+
"FEATURE_FIELDS_COLLIDE",
|
|
104
|
+
"The feature identifier and raw metadata require separate Arrow fields"
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (rawMetadataField && !isBinaryType(fieldsByName.get(rawMetadataField)?.type)) {
|
|
108
|
+
throw new TileConversionError(
|
|
109
|
+
"RAW_METADATA_FIELD_NOT_BINARY",
|
|
110
|
+
`Arrow schema field "${rawMetadataField}" must use binary or fixed-size-binary type`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
...sourceSchema,
|
|
115
|
+
metadata: {
|
|
116
|
+
...sourceSchema.metadata || {},
|
|
117
|
+
...metadataClass ? { [FEATURE_CLASS_METADATA_KEY]: metadataClass } : {}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function validateFeatureIdentifier(featureId, dataType) {
|
|
122
|
+
const isStringIdentifier = typeof featureId === "string";
|
|
123
|
+
const isIntegerIdentifier2 = typeof featureId === "bigint" || Number.isSafeInteger(featureId);
|
|
124
|
+
const isStringType = dataType === "utf8" || dataType === "utf8-view";
|
|
125
|
+
if (isStringIdentifier && isStringType || isIntegerIdentifier2 && isIntegerDataType(dataType)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
throw new TileConversionError(
|
|
129
|
+
"FEATURE_ID_TYPE_MISMATCH",
|
|
130
|
+
`Feature identifier ${String(featureId)} is not compatible with the declared Arrow type`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
function validateFeatureValue(field, value, fieldPath) {
|
|
134
|
+
if (value === null || value === void 0) {
|
|
135
|
+
if (field.nullable !== true) {
|
|
136
|
+
throw new TileConversionError(
|
|
137
|
+
"FEATURE_VALUE_REQUIRED",
|
|
138
|
+
`Required Arrow field "${fieldPath}" has no value`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const dataType = field.type;
|
|
144
|
+
if (typeof dataType === "object") {
|
|
145
|
+
if (dataType.type === "dictionary") {
|
|
146
|
+
validateFeatureValue({ ...field, type: dataType.dictionary }, value, fieldPath);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (dataType.type === "list" || dataType.type === "large-list") {
|
|
150
|
+
if (!Array.isArray(value)) {
|
|
151
|
+
throw createFeatureValueTypeError(fieldPath, "an array");
|
|
152
|
+
}
|
|
153
|
+
const childField = dataType.children[0];
|
|
154
|
+
for (let index = 0; index < value.length; index++) {
|
|
155
|
+
validateFeatureValue(childField, value[index], `${fieldPath}[${index}]`);
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (dataType.type === "fixed-size-list") {
|
|
160
|
+
if (!Array.isArray(value) || value.length !== dataType.listSize) {
|
|
161
|
+
throw createFeatureValueTypeError(fieldPath, `an array of length ${dataType.listSize}`);
|
|
162
|
+
}
|
|
163
|
+
const childField = dataType.children[0];
|
|
164
|
+
for (let index = 0; index < value.length; index++) {
|
|
165
|
+
validateFeatureValue(childField, value[index], `${fieldPath}[${index}]`);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (dataType.type === "struct") {
|
|
170
|
+
if (!isRecord(value)) {
|
|
171
|
+
throw createFeatureValueTypeError(fieldPath, "an object");
|
|
172
|
+
}
|
|
173
|
+
const childNames = new Set(dataType.children.map((childField) => childField.name));
|
|
174
|
+
const unknownNames = Object.keys(value).filter((name) => !childNames.has(name));
|
|
175
|
+
if (unknownNames.length > 0) {
|
|
176
|
+
throw new TileConversionError(
|
|
177
|
+
"FEATURE_PROPERTY_NOT_MAPPED",
|
|
178
|
+
`Nested feature properties "${unknownNames.join(", ")}" are not declared under "${fieldPath}"`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
for (const childField of dataType.children) {
|
|
182
|
+
validateFeatureValue(childField, value[childField.name], `${fieldPath}.${childField.name}`);
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (dataType.type === "decimal") {
|
|
187
|
+
if (typeof value !== "bigint") {
|
|
188
|
+
throw createFeatureValueTypeError(fieldPath, "an unscaled bigint decimal value");
|
|
189
|
+
}
|
|
190
|
+
if (value <= -(10n ** BigInt(dataType.precision)) || value >= 10n ** BigInt(dataType.precision)) {
|
|
191
|
+
throw new TileConversionError(
|
|
192
|
+
"FEATURE_VALUE_OUT_OF_RANGE",
|
|
193
|
+
`Value for Arrow decimal field "${fieldPath}" exceeds precision ${dataType.precision}`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (dataType.type === "fixed-size-binary") {
|
|
199
|
+
const byteLength = getByteLength(value);
|
|
200
|
+
if (byteLength !== dataType.byteWidth) {
|
|
201
|
+
throw createFeatureValueTypeError(fieldPath, `exactly ${dataType.byteWidth} bytes`);
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
throw new TileConversionError(
|
|
206
|
+
"FEATURE_VALUE_MAPPING_UNSUPPORTED",
|
|
207
|
+
`Arrow schema type "${dataType.type}" has no feature value mapping for "${fieldPath}"`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (isIntegerDataType(dataType)) {
|
|
211
|
+
if (!isIntegerIdentifier(value)) {
|
|
212
|
+
throw createFeatureValueTypeError(fieldPath, "an integer");
|
|
213
|
+
}
|
|
214
|
+
const [minimum, maximum] = getIntegerBounds(dataType);
|
|
215
|
+
const integerValue = typeof value === "bigint" ? value : BigInt(value);
|
|
216
|
+
if (integerValue < minimum || integerValue > maximum) {
|
|
217
|
+
throw new TileConversionError(
|
|
218
|
+
"FEATURE_VALUE_OUT_OF_RANGE",
|
|
219
|
+
`Value for Arrow field "${fieldPath}" is outside the range of ${dataType}`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (dataType === "utf8" || dataType === "utf8-view") {
|
|
225
|
+
if (typeof value !== "string") throw createFeatureValueTypeError(fieldPath, "a string");
|
|
226
|
+
if (new TextDecoder().decode(new TextEncoder().encode(value)) !== value)
|
|
227
|
+
throw createFeatureValueTypeError(fieldPath, "a valid Unicode string");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (dataType === "bool") {
|
|
231
|
+
if (typeof value !== "boolean") throw createFeatureValueTypeError(fieldPath, "a boolean");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (dataType === "binary" || dataType === "binary-view") {
|
|
235
|
+
if (getByteLength(value) < 0) throw createFeatureValueTypeError(fieldPath, "binary data");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (dataType === "float" || dataType === "float16" || dataType === "float32" || dataType === "float64") {
|
|
239
|
+
if (typeof value !== "number") throw createFeatureValueTypeError(fieldPath, "a number");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (dataType === "null") {
|
|
243
|
+
throw createFeatureValueTypeError(fieldPath, "null");
|
|
244
|
+
}
|
|
245
|
+
if (dataType === "date-day" || dataType === "date-millisecond" || dataType.startsWith("timestamp-")) {
|
|
246
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
|
|
247
|
+
throw createFeatureValueTypeError(fieldPath, "a valid Date");
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
throw new TileConversionError(
|
|
252
|
+
"FEATURE_VALUE_MAPPING_UNSUPPORTED",
|
|
253
|
+
`Arrow schema type "${dataType}" has no feature value mapping for "${fieldPath}"`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
function isIntegerIdentifier(value) {
|
|
257
|
+
return typeof value === "bigint" || typeof value === "number" && Number.isSafeInteger(value);
|
|
258
|
+
}
|
|
259
|
+
function isIntegerDataType(dataType) {
|
|
260
|
+
return dataType === "int8" || dataType === "int16" || dataType === "int32" || dataType === "int64" || dataType === "uint8" || dataType === "uint16" || dataType === "uint32" || dataType === "uint64";
|
|
261
|
+
}
|
|
262
|
+
function validateFeatureSchemaType(field) {
|
|
263
|
+
const dataType = field.type;
|
|
264
|
+
if (dataType === "int") {
|
|
265
|
+
throw new TileConversionError(
|
|
266
|
+
"FEATURE_INTEGER_TYPE_UNSUPPORTED",
|
|
267
|
+
`Arrow field "${field.name}" must declare an explicit integer width and signedness`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (typeof dataType !== "object") {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
let childFields = [];
|
|
274
|
+
if (dataType.type === "dictionary" && dataType.indices) {
|
|
275
|
+
childFields = [{ name: `${field.name} index`, type: dataType.indices, nullable: false }];
|
|
276
|
+
} else if ("children" in dataType) {
|
|
277
|
+
childFields = dataType.children;
|
|
278
|
+
}
|
|
279
|
+
for (const childField of childFields) {
|
|
280
|
+
validateFeatureSchemaType(childField);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function getIntegerBounds(dataType) {
|
|
284
|
+
switch (dataType) {
|
|
285
|
+
case "int8":
|
|
286
|
+
return [-128n, 127n];
|
|
287
|
+
case "uint8":
|
|
288
|
+
return [0n, 255n];
|
|
289
|
+
case "int16":
|
|
290
|
+
return [-32768n, 32767n];
|
|
291
|
+
case "uint16":
|
|
292
|
+
return [0n, 65535n];
|
|
293
|
+
case "int32":
|
|
294
|
+
return [-2147483648n, 2147483647n];
|
|
295
|
+
case "uint32":
|
|
296
|
+
return [0n, 4294967295n];
|
|
297
|
+
case "int64":
|
|
298
|
+
return [-(2n ** 63n), 2n ** 63n - 1n];
|
|
299
|
+
case "uint64":
|
|
300
|
+
return [0n, 2n ** 64n - 1n];
|
|
301
|
+
default:
|
|
302
|
+
return [-(2n ** 31n), 2n ** 31n - 1n];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function createFeatureValueTypeError(fieldPath, expected) {
|
|
306
|
+
return new TileConversionError(
|
|
307
|
+
"FEATURE_VALUE_TYPE_MISMATCH",
|
|
308
|
+
`Arrow field "${fieldPath}" requires ${expected}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
function isRecord(value) {
|
|
312
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
313
|
+
}
|
|
314
|
+
function getByteLength(value) {
|
|
315
|
+
if (value instanceof Uint8Array) return value.byteLength;
|
|
316
|
+
if (value instanceof ArrayBuffer) return value.byteLength;
|
|
317
|
+
return -1;
|
|
318
|
+
}
|
|
319
|
+
function isBinaryType(dataType) {
|
|
320
|
+
return dataType === "binary" || dataType === "binary-view" || typeof dataType === "object" && dataType.type === "fixed-size-binary";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/v5/point-cloud.ts
|
|
324
|
+
import { Tile3DWriter, TILE3D_TYPE } from "@loaders.gl/3d-tiles";
|
|
325
|
+
import { convertTableToMesh } from "@loaders.gl/schema-utils";
|
|
326
|
+
import { TileConversionError as TileConversionError2 } from "@loaders.gl/tile-converter/v5/core";
|
|
327
|
+
function encodePointCloudTile(pointBatch, options = {}) {
|
|
328
|
+
const mesh = "attributes" in pointBatch ? pointBatch : convertTableToMesh(pointBatch);
|
|
329
|
+
validatePointAttributes(mesh.attributes);
|
|
330
|
+
const positionAttribute = mesh.attributes.POSITION;
|
|
331
|
+
if (!positionAttribute || positionAttribute.size !== 3) {
|
|
332
|
+
throw new TileConversionError2(
|
|
333
|
+
"POINT_CLOUD_POSITION_REQUIRED",
|
|
334
|
+
"Point cloud batches must contain a three-component POSITION attribute"
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const positions = getPositionValues(positionAttribute, options.rtcCenter);
|
|
338
|
+
const pointCount = positions.length / 3;
|
|
339
|
+
if (pointCount === 0 || !Number.isInteger(pointCount)) {
|
|
340
|
+
throw new TileConversionError2(
|
|
341
|
+
"POINT_CLOUD_POSITION_COUNT_INVALID",
|
|
342
|
+
"Point cloud POSITION must contain one or more xyz triples"
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const colors = getColorAttribute(mesh.attributes.COLOR_0 || mesh.attributes.COLOR, pointCount);
|
|
346
|
+
const constantRGBA = colors ? null : getConstantRgba(options.constantRGBA);
|
|
347
|
+
const normals = getNormalAttribute(mesh.attributes.NORMAL, pointCount);
|
|
348
|
+
const batchIds = getBatchIds(mesh.attributes.BATCH_ID, pointCount);
|
|
349
|
+
const batchTableJson = validateBatchTable(options.batchTableJson, batchIds);
|
|
350
|
+
return Tile3DWriter.encodeSync(
|
|
351
|
+
{
|
|
352
|
+
type: TILE3D_TYPE.POINT_CLOUD,
|
|
353
|
+
featureTableJson: options.rtcCenter || constantRGBA ? {
|
|
354
|
+
...options.rtcCenter ? { RTC_CENTER: [...options.rtcCenter] } : {},
|
|
355
|
+
...constantRGBA ? { CONSTANT_RGBA: constantRGBA } : {}
|
|
356
|
+
} : void 0,
|
|
357
|
+
batchTableJson,
|
|
358
|
+
attributes: { positions, colors, normals, batchIds }
|
|
359
|
+
},
|
|
360
|
+
{}
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
function validatePointAttributes(attributes) {
|
|
364
|
+
const supportedAttributes = /* @__PURE__ */ new Set([
|
|
365
|
+
"POSITION",
|
|
366
|
+
"NORMAL",
|
|
367
|
+
"BATCH_ID",
|
|
368
|
+
attributes.COLOR_0 ? "COLOR_0" : "COLOR"
|
|
369
|
+
]);
|
|
370
|
+
const unsupportedAttributes = Object.keys(attributes).filter(
|
|
371
|
+
(name) => !supportedAttributes.has(name)
|
|
372
|
+
);
|
|
373
|
+
if (unsupportedAttributes.length > 0) {
|
|
374
|
+
throw new TileConversionError2(
|
|
375
|
+
"POINT_CLOUD_ATTRIBUTE_UNSUPPORTED",
|
|
376
|
+
`PNTS encoding does not support these point attributes: ${unsupportedAttributes.join(", ")}`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function getConstantRgba(constantRGBA) {
|
|
381
|
+
if (!constantRGBA) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
if (constantRGBA.length !== 4 || constantRGBA.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) {
|
|
385
|
+
throw new TileConversionError2(
|
|
386
|
+
"POINT_CLOUD_CONSTANT_RGBA_INVALID",
|
|
387
|
+
"Point cloud constantRGBA must contain four unsigned byte values"
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
return [constantRGBA[0], constantRGBA[1], constantRGBA[2], constantRGBA[3]];
|
|
391
|
+
}
|
|
392
|
+
function getPositionValues(attribute, rtcCenter) {
|
|
393
|
+
if (rtcCenter && (rtcCenter.length !== 3 || rtcCenter.some((value) => !Number.isFinite(value)))) {
|
|
394
|
+
throw new TileConversionError2(
|
|
395
|
+
"POINT_CLOUD_RTC_CENTER_INVALID",
|
|
396
|
+
"Point cloud RTC_CENTER must contain three finite values"
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
const values = attribute.value;
|
|
400
|
+
const result = new Float32Array(values.length);
|
|
401
|
+
const transform = attribute.transform;
|
|
402
|
+
for (let index = 0; index < values.length; index++) {
|
|
403
|
+
const component = index % 3;
|
|
404
|
+
const value = values[index];
|
|
405
|
+
const absoluteValue = transform?.type === "quantization" ? transform.origin[component] + value / (2 ** transform.bits - 1) * transform.range : value;
|
|
406
|
+
result[index] = absoluteValue - (rtcCenter?.[component] ?? 0);
|
|
407
|
+
}
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
410
|
+
function getColorAttribute(attribute, pointCount) {
|
|
411
|
+
if (!attribute) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
if (attribute.size !== 3 && attribute.size !== 4) {
|
|
415
|
+
throw new TileConversionError2(
|
|
416
|
+
"POINT_CLOUD_COLOR_SIZE_UNSUPPORTED",
|
|
417
|
+
"Point cloud color attributes must contain three or four components"
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
if (attribute.value.length !== pointCount * attribute.size) {
|
|
421
|
+
throw new TileConversionError2(
|
|
422
|
+
"POINT_CLOUD_COLOR_COUNT_MISMATCH",
|
|
423
|
+
"Point cloud color count does not match the POSITION count"
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
const colors = new Uint8Array(attribute.value.length);
|
|
427
|
+
const maximum = getIntegerColorMaximum(attribute.value);
|
|
428
|
+
for (let index = 0; index < attribute.value.length; index++) {
|
|
429
|
+
const value = attribute.value[index];
|
|
430
|
+
const scaledValue = maximum ? value / maximum * 255 : value <= 1 ? value * 255 : value;
|
|
431
|
+
if (!Number.isFinite(scaledValue) || scaledValue < 0 || scaledValue > 255) {
|
|
432
|
+
throw new TileConversionError2(
|
|
433
|
+
"POINT_CLOUD_COLOR_VALUE_INVALID",
|
|
434
|
+
"Point cloud color components must be finite values from 0 through 255"
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
colors[index] = Math.round(scaledValue);
|
|
438
|
+
}
|
|
439
|
+
return { type: 5121, value: colors, size: attribute.size, normalized: false };
|
|
440
|
+
}
|
|
441
|
+
function getIntegerColorMaximum(values) {
|
|
442
|
+
if (values instanceof Uint8Array || values instanceof Uint8ClampedArray) {
|
|
443
|
+
return 255;
|
|
444
|
+
}
|
|
445
|
+
if (values instanceof Uint16Array) {
|
|
446
|
+
return 65535;
|
|
447
|
+
}
|
|
448
|
+
if (values instanceof Uint32Array) {
|
|
449
|
+
return 4294967295;
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
function getNormalAttribute(attribute, pointCount) {
|
|
454
|
+
if (!attribute) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
if (attribute.size !== 3 || attribute.value.length !== pointCount * 3) {
|
|
458
|
+
throw new TileConversionError2(
|
|
459
|
+
"POINT_CLOUD_NORMAL_COUNT_MISMATCH",
|
|
460
|
+
"Point cloud normals must contain one xyz triple per position"
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
return { type: 5126, value: Float32Array.from(attribute.value), size: 3 };
|
|
464
|
+
}
|
|
465
|
+
function getBatchIds(attribute, pointCount) {
|
|
466
|
+
if (!attribute) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
if (attribute.size !== 1 || attribute.value.length !== pointCount) {
|
|
470
|
+
throw new TileConversionError2(
|
|
471
|
+
"POINT_CLOUD_BATCH_ID_COUNT_MISMATCH",
|
|
472
|
+
"Point cloud BATCH_ID must contain one value per position"
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
const batchIds = new Uint16Array(pointCount);
|
|
476
|
+
for (let index = 0; index < pointCount; index++) {
|
|
477
|
+
const batchId = attribute.value[index];
|
|
478
|
+
if (!Number.isInteger(batchId) || batchId < 0 || batchId > 65535) {
|
|
479
|
+
throw new TileConversionError2(
|
|
480
|
+
"POINT_CLOUD_BATCH_ID_OUT_OF_RANGE",
|
|
481
|
+
"Point cloud BATCH_ID values must be unsigned 16-bit integers"
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
batchIds[index] = batchId;
|
|
485
|
+
}
|
|
486
|
+
return batchIds;
|
|
487
|
+
}
|
|
488
|
+
function validateBatchTable(batchTableJson, batchIds) {
|
|
489
|
+
if (!batchTableJson) {
|
|
490
|
+
if (batchIds) {
|
|
491
|
+
throw new TileConversionError2(
|
|
492
|
+
"POINT_CLOUD_BATCH_TABLE_REQUIRED",
|
|
493
|
+
"BATCH_ID values require matching batchTableJson properties"
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
return void 0;
|
|
497
|
+
}
|
|
498
|
+
if (!batchIds) {
|
|
499
|
+
throw new TileConversionError2(
|
|
500
|
+
"POINT_CLOUD_BATCH_ID_REQUIRED",
|
|
501
|
+
"batchTableJson requires a BATCH_ID attribute"
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
let batchLength = 0;
|
|
505
|
+
for (const batchId of batchIds) {
|
|
506
|
+
batchLength = Math.max(batchLength, batchId + 1);
|
|
507
|
+
}
|
|
508
|
+
const normalizedBatchTable = {};
|
|
509
|
+
for (const [name, values] of Object.entries(batchTableJson)) {
|
|
510
|
+
if (values.length !== batchLength) {
|
|
511
|
+
throw new TileConversionError2(
|
|
512
|
+
"POINT_CLOUD_BATCH_TABLE_LENGTH_MISMATCH",
|
|
513
|
+
`Batch table property "${name}" must contain ${batchLength} values`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
normalizedBatchTable[name] = [...values];
|
|
517
|
+
}
|
|
518
|
+
return normalizedBatchTable;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/v5/point-cloud-source-encoder.ts
|
|
522
|
+
import { traversePointCloudSource } from "@loaders.gl/tile-converter/v5/core";
|
|
523
|
+
async function* encodePointCloudSource(source, options = {}) {
|
|
524
|
+
for await (const sourceTile of traversePointCloudSource(source, options)) {
|
|
525
|
+
const encodingOptions = sourceTile.content ? options.getTileEncodingOptions?.(sourceTile) : void 0;
|
|
526
|
+
const encodedTile = encodePointCloudSourceTile(sourceTile, encodingOptions);
|
|
527
|
+
if (encodedTile) {
|
|
528
|
+
yield encodedTile;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function encodePointCloudSourceTile(sourceTile, options = {}) {
|
|
533
|
+
const { header, content } = sourceTile;
|
|
534
|
+
if (!content) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
id: header.id,
|
|
539
|
+
header,
|
|
540
|
+
pnts: encodePointCloudTile(content.data, {
|
|
541
|
+
...options,
|
|
542
|
+
constantRGBA: options.constantRGBA ?? content.constantRGBA
|
|
543
|
+
}),
|
|
544
|
+
pointCount: content.pointCount,
|
|
545
|
+
coordinateSystem: content.coordinateSystem,
|
|
546
|
+
cartographicOrigin: [...content.cartographicOrigin],
|
|
547
|
+
modelMatrix: content.modelMatrix ? Array.from(content.modelMatrix) : void 0,
|
|
548
|
+
spatialReference: content.spatialReference,
|
|
549
|
+
spatialBoundingVolume: content.spatialBoundingVolume || header.spatialBoundingVolume
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// src/v5/point-cloud-conversion.ts
|
|
554
|
+
import { convertTileset } from "@loaders.gl/tile-converter/v5/core";
|
|
555
|
+
import { traversePointCloudSource as traversePointCloudSource2 } from "@loaders.gl/tile-converter/v5/core";
|
|
556
|
+
function convertPointCloudSource(source, options) {
|
|
557
|
+
return convertTileset({
|
|
558
|
+
source: {
|
|
559
|
+
async inspect() {
|
|
560
|
+
if (!source.isReady) {
|
|
561
|
+
await source.initialize();
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
read: (_inspection, signal) => traversePointCloudSource2(source, { maxDepth: options.maxDepth, signal })
|
|
565
|
+
},
|
|
566
|
+
codec: {
|
|
567
|
+
async *convert(sourceTile) {
|
|
568
|
+
const encodingOptions = sourceTile.content ? options.getTileEncodingOptions?.(sourceTile) : void 0;
|
|
569
|
+
const encodedTile = encodePointCloudSourceTile(sourceTile, encodingOptions);
|
|
570
|
+
if (encodedTile) {
|
|
571
|
+
yield encodedTile;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
sink: options.sink,
|
|
576
|
+
measureInputBytes: options.measureInputBytes,
|
|
577
|
+
measureOutputBytes: (resource) => resource.pnts.byteLength,
|
|
578
|
+
maxInputResourceBytes: options.maxInputResourceBytes,
|
|
579
|
+
maxOutputResourceBytes: options.maxOutputResourceBytes,
|
|
580
|
+
signal: options.signal,
|
|
581
|
+
onProgress: options.onProgress
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/v5/mesh.ts
|
|
586
|
+
import { GLTFScenegraph, GLTFWriter } from "@loaders.gl/gltf";
|
|
587
|
+
import { getBinaryImageMetadata } from "@loaders.gl/images";
|
|
588
|
+
import { TileConversionError as TileConversionError3 } from "@loaders.gl/tile-converter/v5/core";
|
|
589
|
+
function encodeMeshTile(mesh, options = {}) {
|
|
590
|
+
const geometry = validateMeshGeometry(mesh);
|
|
591
|
+
const { scenegraph, materialIndex } = createMeshTileScenegraph(options, geometry);
|
|
592
|
+
const meshIndex = scenegraph.addMesh({
|
|
593
|
+
attributes: geometry.attributes,
|
|
594
|
+
indices: geometry.indices?.value,
|
|
595
|
+
material: materialIndex,
|
|
596
|
+
mode: 4
|
|
597
|
+
});
|
|
598
|
+
return finalizeMeshTileScenegraph(scenegraph, meshIndex);
|
|
599
|
+
}
|
|
600
|
+
function createMeshTileScenegraph(options, geometry) {
|
|
601
|
+
const scenegraph = new GLTFScenegraph({ json: { asset: { version: "2.0", generator: "loaders.gl" } } });
|
|
602
|
+
const material = options.material === void 0 ? void 0 : validateMeshMaterial(options.material, scenegraph, "TEXCOORD_0" in geometry.attributes);
|
|
603
|
+
return { scenegraph, materialIndex: material ? scenegraph.addMaterial(material) : void 0 };
|
|
604
|
+
}
|
|
605
|
+
function finalizeMeshTileScenegraph(scenegraph, meshIndex) {
|
|
606
|
+
const nodeIndex = scenegraph.addNode({ meshIndex });
|
|
607
|
+
const sceneIndex = scenegraph.addScene({ nodeIndices: [nodeIndex] });
|
|
608
|
+
scenegraph.setDefaultScene(sceneIndex);
|
|
609
|
+
scenegraph.createBinaryChunk();
|
|
610
|
+
return GLTFWriter.encodeSync(scenegraph.gltf);
|
|
611
|
+
}
|
|
612
|
+
function validateMeshGeometry(mesh, allowFloat64Positions = false) {
|
|
613
|
+
if (mesh.topology !== "triangle-list" || mesh.mode !== 4) {
|
|
614
|
+
throw new TileConversionError3(
|
|
615
|
+
"MESH_TOPOLOGY_UNSUPPORTED",
|
|
616
|
+
"Only triangle-list mode 4 is supported"
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
for (const name of Object.keys(mesh.attributes)) {
|
|
620
|
+
if (name !== "POSITION" && name !== "NORMAL" && name !== "COLOR_0" && name !== "TEXCOORD_0") {
|
|
621
|
+
throw new TileConversionError3(
|
|
622
|
+
"MESH_ATTRIBUTE_UNSUPPORTED",
|
|
623
|
+
`Mesh attribute ${name} is unsupported`
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const positions = getFloatAttribute(mesh.attributes.POSITION, "POSITION", allowFloat64Positions);
|
|
628
|
+
const attributes = {
|
|
629
|
+
POSITION: { value: positions, size: 3 }
|
|
630
|
+
};
|
|
631
|
+
if (mesh.attributes.NORMAL) {
|
|
632
|
+
const normals = getFloatAttribute(mesh.attributes.NORMAL, "NORMAL");
|
|
633
|
+
if (normals.length !== positions.length) {
|
|
634
|
+
throw new TileConversionError3(
|
|
635
|
+
"MESH_NORMAL_COUNT_MISMATCH",
|
|
636
|
+
"NORMAL count must match POSITION count"
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
for (let index = 0; index < normals.length; index += 3) {
|
|
640
|
+
if (Math.abs(Math.hypot(normals[index], normals[index + 1], normals[index + 2]) - 1) > 1e-4) {
|
|
641
|
+
throw new TileConversionError3(
|
|
642
|
+
"MESH_NORMAL_INVALID",
|
|
643
|
+
"NORMAL vectors must have unit length"
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
attributes.NORMAL = { value: normals, size: 3 };
|
|
648
|
+
}
|
|
649
|
+
if ("COLOR_0" in mesh.attributes) {
|
|
650
|
+
const colors = mesh.attributes.COLOR_0;
|
|
651
|
+
const normalized = isNormalizedUnsignedAttribute(colors);
|
|
652
|
+
if (!colors || !(colors.value instanceof Float32Array || normalized) || colors.size !== 3 && colors.size !== 4 || colors.value.length / colors.size !== positions.length / 3 || colors.value instanceof Float32Array && colors.value.some((value) => !Number.isFinite(value) || value < 0 || value > 1)) {
|
|
653
|
+
throw new TileConversionError3(
|
|
654
|
+
"MESH_COLOR_INVALID",
|
|
655
|
+
"COLOR_0 requires packed linear RGB/RGBA: Float32 in [0, 1] or normalized Uint8/Uint16, one per vertex"
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
validateAttributeLayout(colors, "COLOR_0", normalized);
|
|
659
|
+
attributes.COLOR_0 = {
|
|
660
|
+
value: colors.value,
|
|
661
|
+
size: colors.size,
|
|
662
|
+
...normalized ? { normalized: true } : {}
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
if ("TEXCOORD_0" in mesh.attributes) {
|
|
666
|
+
const textureCoordinates = mesh.attributes.TEXCOORD_0;
|
|
667
|
+
const normalized = isNormalizedUnsignedAttribute(textureCoordinates);
|
|
668
|
+
if (!textureCoordinates || !(textureCoordinates.value instanceof Float32Array || normalized) || textureCoordinates.size !== 2 || textureCoordinates.value.length / 2 !== positions.length / 3 || textureCoordinates.value.some((value) => !Number.isFinite(value))) {
|
|
669
|
+
throw new TileConversionError3(
|
|
670
|
+
"MESH_TEXCOORD_INVALID",
|
|
671
|
+
"TEXCOORD_0 requires packed finite Float32 or normalized Uint8/Uint16 UV pairs, one per vertex"
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
validateAttributeLayout(textureCoordinates, "TEXCOORD_0", normalized);
|
|
675
|
+
attributes.TEXCOORD_0 = {
|
|
676
|
+
value: textureCoordinates.value,
|
|
677
|
+
size: 2,
|
|
678
|
+
...normalized ? { normalized: true } : {}
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
const indices = getMeshIndices(mesh.indices, positions.length / 3);
|
|
682
|
+
if (!indices && positions.length % 9 !== 0) {
|
|
683
|
+
throw new TileConversionError3(
|
|
684
|
+
"MESH_TRIANGLE_COUNT_INVALID",
|
|
685
|
+
"Nonindexed meshes require complete triangles"
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
return {
|
|
689
|
+
topology: "triangle-list",
|
|
690
|
+
mode: 4,
|
|
691
|
+
attributes,
|
|
692
|
+
indices: indices ? { value: indices, size: 1 } : void 0
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function getFloatAttribute(attribute, name, allowFloat64 = false) {
|
|
696
|
+
if (!attribute || !(attribute.value instanceof Float32Array || allowFloat64 && attribute.value instanceof Float64Array) || attribute.size !== 3 || attribute.value.length === 0 || attribute.value.length % 3 !== 0) {
|
|
697
|
+
throw new TileConversionError3(
|
|
698
|
+
"MESH_ATTRIBUTE_INVALID",
|
|
699
|
+
`${name} must contain packed ${allowFloat64 ? "Float32 or Float64" : "Float32"} xyz triples`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
validateAttributeLayout(attribute, name);
|
|
703
|
+
if (attribute.value.some((value) => !Number.isFinite(value))) {
|
|
704
|
+
throw new TileConversionError3("MESH_ATTRIBUTE_NONFINITE", `${name} must contain finite values`);
|
|
705
|
+
}
|
|
706
|
+
return attribute.value;
|
|
707
|
+
}
|
|
708
|
+
function getMeshIndices(attribute, vertexCount) {
|
|
709
|
+
if (!attribute) return void 0;
|
|
710
|
+
validateAttributeLayout(attribute, "indices");
|
|
711
|
+
const values = attribute.value;
|
|
712
|
+
if (attribute.size !== 1 || !(values instanceof Uint8Array || values instanceof Uint16Array || values instanceof Uint32Array)) {
|
|
713
|
+
throw new TileConversionError3(
|
|
714
|
+
"MESH_INDICES_INVALID",
|
|
715
|
+
"Indices must contain packed unsigned integer scalars"
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
if (values.length === 0 || values.length % 3 !== 0) {
|
|
719
|
+
throw new TileConversionError3(
|
|
720
|
+
"MESH_TRIANGLE_COUNT_INVALID",
|
|
721
|
+
"Indices must contain complete triangles"
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
const restartIndex = 2 ** (values.BYTES_PER_ELEMENT * 8) - 1;
|
|
725
|
+
if (values.some((value) => value >= vertexCount || value === restartIndex)) {
|
|
726
|
+
throw new TileConversionError3(
|
|
727
|
+
"MESH_INDEX_OUT_OF_RANGE",
|
|
728
|
+
"Indices must reference existing vertices and exclude primitive-restart values"
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
return values;
|
|
732
|
+
}
|
|
733
|
+
function isNormalizedUnsignedAttribute(attribute) {
|
|
734
|
+
return attribute?.normalized === true && (attribute.value instanceof Uint8Array || attribute.value instanceof Uint16Array);
|
|
735
|
+
}
|
|
736
|
+
function validateAttributeLayout(attribute, name, allowNormalized = false) {
|
|
737
|
+
if ((attribute.byteOffset ?? 0) !== 0 || (attribute.byteStride ?? 0) !== 0 || attribute.normalized && !allowNormalized || attribute.transform || attribute.componentType) {
|
|
738
|
+
throw new TileConversionError3(
|
|
739
|
+
"MESH_ATTRIBUTE_LAYOUT_UNSUPPORTED",
|
|
740
|
+
`${name} must use packed contiguous storage with supported normalization`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function validateMeshMaterial(material, scenegraph, hasTextureCoordinates) {
|
|
745
|
+
if (!material || typeof material !== "object" || Array.isArray(material)) {
|
|
746
|
+
throw new TileConversionError3("MESH_MATERIAL_INVALID", "Material must be an object");
|
|
747
|
+
}
|
|
748
|
+
const allowedProperties = [
|
|
749
|
+
"baseColorFactor",
|
|
750
|
+
"baseColorTexture",
|
|
751
|
+
"metallicFactor",
|
|
752
|
+
"roughnessFactor",
|
|
753
|
+
"alphaMode",
|
|
754
|
+
"alphaCutoff",
|
|
755
|
+
"doubleSided"
|
|
756
|
+
];
|
|
757
|
+
for (const name of Object.keys(material)) {
|
|
758
|
+
if (!allowedProperties.includes(name)) {
|
|
759
|
+
throw new TileConversionError3(
|
|
760
|
+
"MESH_MATERIAL_UNSUPPORTED",
|
|
761
|
+
`Material property ${name} is unsupported`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const { baseColorFactor, metallicFactor, roughnessFactor, alphaMode, alphaCutoff, doubleSided } = material;
|
|
766
|
+
if (baseColorFactor !== void 0 && (!Array.isArray(baseColorFactor) || baseColorFactor.length !== 4 || Array.from(baseColorFactor).some(
|
|
767
|
+
(value) => !Number.isFinite(value) || value < 0 || value > 1
|
|
768
|
+
)) || [metallicFactor, roughnessFactor].some(
|
|
769
|
+
(value) => value !== void 0 && (!Number.isFinite(value) || value < 0 || value > 1)
|
|
770
|
+
) || alphaMode !== void 0 && !["OPAQUE", "MASK", "BLEND"].includes(alphaMode) || alphaCutoff !== void 0 && (alphaMode !== "MASK" || !Number.isFinite(alphaCutoff) || alphaCutoff < 0) || doubleSided !== void 0 && typeof doubleSided !== "boolean") {
|
|
771
|
+
throw new TileConversionError3(
|
|
772
|
+
"MESH_MATERIAL_INVALID",
|
|
773
|
+
"Material values must satisfy the selected glTF profile"
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
return {
|
|
777
|
+
pbrMetallicRoughness: {
|
|
778
|
+
baseColorFactor: baseColorFactor ? [...baseColorFactor] : void 0,
|
|
779
|
+
metallicFactor,
|
|
780
|
+
roughnessFactor,
|
|
781
|
+
baseColorTexture: "baseColorTexture" in material ? encodeBaseColorTexture(material.baseColorTexture, scenegraph, hasTextureCoordinates) : void 0
|
|
782
|
+
},
|
|
783
|
+
alphaMode,
|
|
784
|
+
alphaCutoff,
|
|
785
|
+
doubleSided
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
function encodeBaseColorTexture(texture, scenegraph, hasTextureCoordinates) {
|
|
789
|
+
if (!texture || Object.keys(texture).some(
|
|
790
|
+
(name) => name !== "data" && name !== "mimeType" && name !== "sampler" && name !== "transform"
|
|
791
|
+
) || !(texture.data instanceof Uint8Array) || !["image/png", "image/jpeg"].includes(texture.mimeType)) {
|
|
792
|
+
throw new TileConversionError3(
|
|
793
|
+
"MESH_TEXTURE_INVALID",
|
|
794
|
+
"Base-color texture must supply Uint8Array data, image/png or image/jpeg mimeType, and optional sampling/transform controls"
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
if (!hasTextureCoordinates) {
|
|
798
|
+
throw new TileConversionError3(
|
|
799
|
+
"MESH_TEXCOORD_REQUIRED",
|
|
800
|
+
"Base-color texture requires TEXCOORD_0"
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
const image = new DataView(texture.data.buffer, texture.data.byteOffset, texture.data.byteLength);
|
|
804
|
+
let metadata;
|
|
805
|
+
try {
|
|
806
|
+
metadata = getBinaryImageMetadata(image);
|
|
807
|
+
} catch {
|
|
808
|
+
}
|
|
809
|
+
if (!metadata || metadata.mimeType !== texture.mimeType || metadata.width <= 0 || metadata.height <= 0) {
|
|
810
|
+
throw new TileConversionError3(
|
|
811
|
+
"MESH_TEXTURE_INVALID",
|
|
812
|
+
"Image header must match mimeType and declare positive dimensions"
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
const samplerIndex = "sampler" in texture ? scenegraph.addSampler(validateMeshSampler(texture.sampler)) : void 0;
|
|
816
|
+
const transform = "transform" in texture ? validateMeshTextureTransform(texture.transform) : void 0;
|
|
817
|
+
if (transform) scenegraph.registerRequiredExtension("KHR_texture_transform");
|
|
818
|
+
return {
|
|
819
|
+
...transform ? { extensions: { KHR_texture_transform: transform } } : {},
|
|
820
|
+
index: scenegraph.addTexture({
|
|
821
|
+
imageIndex: scenegraph.addImage(image, texture.mimeType),
|
|
822
|
+
samplerIndex
|
|
823
|
+
})
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function validateMeshSampler(sampler) {
|
|
827
|
+
if (!sampler || typeof sampler !== "object" || Array.isArray(sampler)) {
|
|
828
|
+
throw new TileConversionError3("MESH_SAMPLER_INVALID", "Sampler must be an object");
|
|
829
|
+
}
|
|
830
|
+
const allowedValues = {
|
|
831
|
+
wrapS: [33071, 33648, 10497],
|
|
832
|
+
wrapT: [33071, 33648, 10497],
|
|
833
|
+
magFilter: [9728, 9729],
|
|
834
|
+
minFilter: [9728, 9729, 9984, 9985, 9986, 9987]
|
|
835
|
+
};
|
|
836
|
+
for (const name in sampler) {
|
|
837
|
+
if (!Object.hasOwn(allowedValues, name)) {
|
|
838
|
+
throw new TileConversionError3(
|
|
839
|
+
"MESH_SAMPLER_UNSUPPORTED",
|
|
840
|
+
`Sampler property ${name} is unsupported`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const selectedSampler = {
|
|
845
|
+
wrapS: sampler.wrapS,
|
|
846
|
+
wrapT: sampler.wrapT,
|
|
847
|
+
minFilter: sampler.minFilter,
|
|
848
|
+
magFilter: sampler.magFilter
|
|
849
|
+
};
|
|
850
|
+
for (const name of Object.keys(allowedValues)) {
|
|
851
|
+
const property = name;
|
|
852
|
+
if (selectedSampler[property] !== void 0 && !allowedValues[property].includes(selectedSampler[property])) {
|
|
853
|
+
throw new TileConversionError3(
|
|
854
|
+
"MESH_SAMPLER_INVALID",
|
|
855
|
+
`Sampler ${name} must use a glTF wrapping/filtering value`
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return selectedSampler;
|
|
860
|
+
}
|
|
861
|
+
function validateMeshTextureTransform(transform) {
|
|
862
|
+
if (!transform || typeof transform !== "object" || Array.isArray(transform)) {
|
|
863
|
+
throw new TileConversionError3(
|
|
864
|
+
"MESH_TEXTURE_TRANSFORM_INVALID",
|
|
865
|
+
"Texture transform must be an object"
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
for (const name in transform) {
|
|
869
|
+
if (name !== "offset" && name !== "rotation" && name !== "scale") {
|
|
870
|
+
throw new TileConversionError3(
|
|
871
|
+
"MESH_TEXTURE_TRANSFORM_UNSUPPORTED",
|
|
872
|
+
`Texture transform property ${name} is unsupported`
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const { offset, rotation, scale } = transform;
|
|
877
|
+
const offsetValues = Array.isArray(offset) && offset.length === 2 ? [offset[0], offset[1]] : offset;
|
|
878
|
+
const scaleValues = Array.isArray(scale) && scale.length === 2 ? [scale[0], scale[1]] : scale;
|
|
879
|
+
if ([offsetValues, scaleValues].some(
|
|
880
|
+
(value) => value !== void 0 && (!Array.isArray(value) || value.length !== 2 || value.some((component) => !Number.isFinite(component)))
|
|
881
|
+
) || rotation !== void 0 && !Number.isFinite(rotation)) {
|
|
882
|
+
throw new TileConversionError3(
|
|
883
|
+
"MESH_TEXTURE_TRANSFORM_INVALID",
|
|
884
|
+
"Texture transform requires finite UV pairs and a finite rotation"
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
return {
|
|
888
|
+
offset: offsetValues === void 0 ? void 0 : [offsetValues[0], offsetValues[1]],
|
|
889
|
+
rotation,
|
|
890
|
+
scale: scaleValues === void 0 ? void 0 : [scaleValues[0], scaleValues[1]]
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/v5/mesh-conversion.ts
|
|
895
|
+
import { getSpatialCoordinateFrame } from "@loaders.gl/tiles";
|
|
896
|
+
import { TileConversionError as TileConversionError5 } from "@loaders.gl/tile-converter/v5/core";
|
|
897
|
+
|
|
898
|
+
// src/v5/mesh-draco.ts
|
|
899
|
+
import { DracoLoader, encodeDraco } from "@loaders.gl/draco";
|
|
900
|
+
import { TileConversionError as TileConversionError4 } from "@loaders.gl/tile-converter/v5/core";
|
|
901
|
+
var DRACO_EXTENSION = "KHR_draco_mesh_compression";
|
|
902
|
+
async function encodeDracoMeshTile(geometry, options, libraryOptions = {}) {
|
|
903
|
+
geometry = validateMeshGeometry(geometry);
|
|
904
|
+
if (!geometry.indices) {
|
|
905
|
+
geometry.indices = {
|
|
906
|
+
size: 1,
|
|
907
|
+
value: Uint32Array.from(
|
|
908
|
+
{ length: geometry.attributes.POSITION.value.length / 3 },
|
|
909
|
+
(_, index) => index
|
|
910
|
+
)
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
const { modules, useLocalLibraries, CDN: contentDeliveryNetwork } = libraryOptions;
|
|
914
|
+
const runtimeOptions = { modules, core: { useLocalLibraries, CDN: contentDeliveryNetwork } };
|
|
915
|
+
const { scenegraph, materialIndex } = createMeshTileScenegraph(options, geometry);
|
|
916
|
+
const encoded = await encodeDraco(geometry, {
|
|
917
|
+
...runtimeOptions,
|
|
918
|
+
draco: { method: "MESH_EDGEBREAKER_ENCODING" }
|
|
919
|
+
});
|
|
920
|
+
const loaderOptions = { ...runtimeOptions, draco: { shape: "mesh" } };
|
|
921
|
+
const loader = await DracoLoader.preload("", loaderOptions);
|
|
922
|
+
const decoded = await loader.parse(encoded.data, loaderOptions);
|
|
923
|
+
const positions = decoded.attributes.POSITION.value;
|
|
924
|
+
if (!(positions instanceof Float32Array) || positions.length !== encoded.report.pointCount * 3 || encoded.report.pointCount === 0 || !decoded.indices || decoded.indices.value.length !== encoded.report.faceCount * 3 || encoded.report.faceCount * 3 !== (geometry.indices?.value.length ?? geometry.attributes.POSITION.value.length / 3)) {
|
|
925
|
+
throw new TileConversionError4(
|
|
926
|
+
"MESH_DRACO_ENCODING_INVALID",
|
|
927
|
+
"Draco must preserve the triangle count and emit finite positions"
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
const inputPositions = geometry.attributes.POSITION.value;
|
|
931
|
+
const positionKeys = /* @__PURE__ */ new Set();
|
|
932
|
+
for (let index = 0; index < inputPositions.length; index += 3) {
|
|
933
|
+
positionKeys.add(getPositionKey(inputPositions, index));
|
|
934
|
+
}
|
|
935
|
+
for (let index = 0; index < positions.length; index += 3) {
|
|
936
|
+
if (!positionKeys.has(getPositionKey(positions, index))) {
|
|
937
|
+
throw new TileConversionError4(
|
|
938
|
+
"MESH_DRACO_POSITION_CHANGED",
|
|
939
|
+
"Lossless Draco must preserve the encoded Float32 positions"
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const bufferView = scenegraph.addBufferView(encoded.data);
|
|
944
|
+
const attributes = {};
|
|
945
|
+
const attributeIdentifiers = {};
|
|
946
|
+
scenegraph.json.accessors = [];
|
|
947
|
+
for (const [name, attribute] of Object.entries(geometry.attributes)) {
|
|
948
|
+
const decodedAttribute = decoded.attributes[name];
|
|
949
|
+
if (!decodedAttribute || decodedAttribute.value.length !== encoded.report.pointCount * attribute.size) {
|
|
950
|
+
throw new TileConversionError4("MESH_DRACO_ENCODING_INVALID", `Draco must preserve ${name}`);
|
|
951
|
+
}
|
|
952
|
+
const accessor = {
|
|
953
|
+
componentType: attribute.value instanceof Uint8Array ? 5121 : attribute.value instanceof Uint16Array ? 5123 : 5126,
|
|
954
|
+
count: encoded.report.pointCount,
|
|
955
|
+
type: attribute.size === 2 ? "VEC2" : attribute.size === 3 ? "VEC3" : "VEC4",
|
|
956
|
+
...attribute.normalized ? { normalized: true } : {}
|
|
957
|
+
};
|
|
958
|
+
if (name === "POSITION") {
|
|
959
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
960
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
961
|
+
for (let index = 0; index < positions.length; index++) {
|
|
962
|
+
const axis = index % 3;
|
|
963
|
+
minimum[axis] = Math.min(minimum[axis], positions[index]);
|
|
964
|
+
maximum[axis] = Math.max(maximum[axis], positions[index]);
|
|
965
|
+
}
|
|
966
|
+
accessor.min = minimum;
|
|
967
|
+
accessor.max = maximum;
|
|
968
|
+
}
|
|
969
|
+
attributes[name] = scenegraph.json.accessors.push(accessor) - 1;
|
|
970
|
+
attributeIdentifiers[name] = encoded.report.attributes[name].id;
|
|
971
|
+
}
|
|
972
|
+
const indices = scenegraph.json.accessors.push({
|
|
973
|
+
componentType: decoded.indices.value instanceof Uint16Array ? 5123 : 5125,
|
|
974
|
+
count: encoded.report.faceCount * 3,
|
|
975
|
+
type: "SCALAR"
|
|
976
|
+
}) - 1;
|
|
977
|
+
scenegraph.json.meshes = [
|
|
978
|
+
{
|
|
979
|
+
primitives: [
|
|
980
|
+
{
|
|
981
|
+
attributes,
|
|
982
|
+
indices,
|
|
983
|
+
mode: 4,
|
|
984
|
+
...materialIndex === void 0 ? {} : { material: materialIndex },
|
|
985
|
+
extensions: { [DRACO_EXTENSION]: { bufferView, attributes: attributeIdentifiers } }
|
|
986
|
+
}
|
|
987
|
+
]
|
|
988
|
+
}
|
|
989
|
+
];
|
|
990
|
+
scenegraph.registerRequiredExtension(DRACO_EXTENSION);
|
|
991
|
+
return { glb: finalizeMeshTileScenegraph(scenegraph, 0), positions };
|
|
992
|
+
}
|
|
993
|
+
function getPositionKey(positions, index) {
|
|
994
|
+
return `${positions[index]},${positions[index + 1]},${positions[index + 2]}`;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// src/v5/mesh-conversion.ts
|
|
998
|
+
function createMeshConversionCodec(options) {
|
|
999
|
+
const { spatialContext, maxPositionError, draco = true, dracoLibraryOptions } = options;
|
|
1000
|
+
if (typeof draco !== "boolean") {
|
|
1001
|
+
throw new TileConversionError5("MESH_DRACO_OPTIONS_INVALID", "draco must be a boolean");
|
|
1002
|
+
}
|
|
1003
|
+
if (!Number.isFinite(maxPositionError) || maxPositionError < 0) {
|
|
1004
|
+
throw new TileConversionError5(
|
|
1005
|
+
"MESH_PRECISION_LIMIT_INVALID",
|
|
1006
|
+
"maxPositionError must be finite and nonnegative"
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
const spatialReference = spatialContext.spatialReference;
|
|
1010
|
+
const outputCrs = spatialReference.targetCrs || spatialReference.sourceCrs;
|
|
1011
|
+
const outputFrame = spatialReference.status === "native" ? spatialReference.coordinateFrame : outputCrs ? getSpatialCoordinateFrame(outputCrs) : "unknown";
|
|
1012
|
+
if (!["native", "transformed"].includes(spatialReference.status) || !["projected", "geocentric", "local", "cartesian"].includes(outputFrame)) {
|
|
1013
|
+
throw new TileConversionError5(
|
|
1014
|
+
"MESH_OUTPUT_FRAME_UNSUPPORTED",
|
|
1015
|
+
"Mesh output requires a resolved Cartesian coordinate frame"
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
return {
|
|
1019
|
+
/** Prepares and encodes one geometry while honoring cancellation. */
|
|
1020
|
+
async *convert(resource, _inspection, signal) {
|
|
1021
|
+
signal?.throwIfAborted();
|
|
1022
|
+
if ("features" in resource && resource.features) {
|
|
1023
|
+
throw new TileConversionError5(
|
|
1024
|
+
"MESH_FEATURE_OUTPUT_UNSUPPORTED",
|
|
1025
|
+
"The GLB codec requires an explicit feature metadata writer; use the I3S codec for supported feature-bearing meshes"
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
const geometry = validateMeshGeometry(resource.mesh, true);
|
|
1029
|
+
const sourcePositions = geometry.attributes.POSITION.value;
|
|
1030
|
+
const origin = [...resource.origin];
|
|
1031
|
+
if (origin.length !== 3 || origin.some((value) => !Number.isFinite(value))) {
|
|
1032
|
+
throw new TileConversionError5(
|
|
1033
|
+
"MESH_ORIGIN_INVALID",
|
|
1034
|
+
"Target origin must contain three finite coordinates"
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
const targetPositions = spatialContext.transformPositions(sourcePositions);
|
|
1038
|
+
if (targetPositions.length !== sourcePositions.length || targetPositions.some((value) => !Number.isFinite(value))) {
|
|
1039
|
+
throw new TileConversionError5(
|
|
1040
|
+
"MESH_TRANSFORM_INVALID",
|
|
1041
|
+
"Spatial transformation must return one finite xyz position per vertex"
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
const localPositions = new Float32Array(targetPositions.length);
|
|
1045
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
1046
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
1047
|
+
const localMinimum = [Infinity, Infinity, Infinity];
|
|
1048
|
+
const localMaximum = [-Infinity, -Infinity, -Infinity];
|
|
1049
|
+
let maximumPositionError = 0;
|
|
1050
|
+
for (let index = 0; index < targetPositions.length; index += 3) {
|
|
1051
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
1052
|
+
localPositions[index + axis] = targetPositions[index + axis] - origin[axis];
|
|
1053
|
+
}
|
|
1054
|
+
const positionError = Math.hypot(
|
|
1055
|
+
localPositions[index] + origin[0] - targetPositions[index],
|
|
1056
|
+
localPositions[index + 1] + origin[1] - targetPositions[index + 1],
|
|
1057
|
+
localPositions[index + 2] + origin[2] - targetPositions[index + 2]
|
|
1058
|
+
);
|
|
1059
|
+
if (!Number.isFinite(positionError) || positionError > maxPositionError) {
|
|
1060
|
+
throw new TileConversionError5(
|
|
1061
|
+
"MESH_POSITION_PRECISION_EXCEEDED",
|
|
1062
|
+
"Float32 mesh positions exceed the configured reconstruction error budget"
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
maximumPositionError = Math.max(maximumPositionError, positionError);
|
|
1066
|
+
}
|
|
1067
|
+
geometry.attributes.POSITION = { value: localPositions, size: 3 };
|
|
1068
|
+
if (geometry.attributes.NORMAL) {
|
|
1069
|
+
geometry.attributes.NORMAL = {
|
|
1070
|
+
value: spatialContext.transformNormals(geometry.attributes.NORMAL.value, sourcePositions),
|
|
1071
|
+
size: 3
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
const appearance = { material: resource.material };
|
|
1075
|
+
const encoded = draco ? await encodeDracoMeshTile(geometry, appearance, dracoLibraryOptions) : { glb: encodeMeshTile(geometry, appearance), positions: localPositions };
|
|
1076
|
+
signal?.throwIfAborted();
|
|
1077
|
+
for (let index = 0; index < encoded.positions.length; index++) {
|
|
1078
|
+
const axis = index % 3;
|
|
1079
|
+
const position = encoded.positions[index];
|
|
1080
|
+
localMinimum[axis] = Math.min(localMinimum[axis], position);
|
|
1081
|
+
localMaximum[axis] = Math.max(localMaximum[axis], position);
|
|
1082
|
+
minimum[axis] = Math.min(minimum[axis], position + origin[axis]);
|
|
1083
|
+
maximum[axis] = Math.max(maximum[axis], position + origin[axis]);
|
|
1084
|
+
}
|
|
1085
|
+
yield {
|
|
1086
|
+
id: resource.id,
|
|
1087
|
+
glb: encoded.glb,
|
|
1088
|
+
origin,
|
|
1089
|
+
boundingBox: [minimum, maximum],
|
|
1090
|
+
localBoundingBox: [localMinimum, localMaximum],
|
|
1091
|
+
spatialReference,
|
|
1092
|
+
maximumPositionError
|
|
1093
|
+
};
|
|
1094
|
+
},
|
|
1095
|
+
/** Reports position rounding authorized by the configured precision budget. */
|
|
1096
|
+
async validateOutput(resource) {
|
|
1097
|
+
return resource.maximumPositionError > 0 ? [
|
|
1098
|
+
{
|
|
1099
|
+
code: "MESH_POSITION_ROUNDING",
|
|
1100
|
+
message: `Encoded positions have up to ${resource.maximumPositionError} target-unit reconstruction error`,
|
|
1101
|
+
severity: "info",
|
|
1102
|
+
resourceId: resource.id
|
|
1103
|
+
}
|
|
1104
|
+
] : [];
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// src/v5/mesh-tileset-sink.ts
|
|
1110
|
+
import { TileConversionError as TileConversionError6 } from "@loaders.gl/tile-converter/v5/core";
|
|
1111
|
+
import { createBoundedMemoryTileConversionSink } from "@loaders.gl/tile-converter/v5/core";
|
|
1112
|
+
function createSingleMeshTilesetSink(options) {
|
|
1113
|
+
const { geometricError } = options;
|
|
1114
|
+
if (!Number.isFinite(geometricError) || geometricError < 0) {
|
|
1115
|
+
throw new TileConversionError6(
|
|
1116
|
+
"MESH_GEOMETRIC_ERROR_INVALID",
|
|
1117
|
+
"geometricError must be finite and nonnegative, in meters"
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
const memorySink = createBoundedMemoryTileConversionSink(options);
|
|
1121
|
+
let state = "empty";
|
|
1122
|
+
let completed = false;
|
|
1123
|
+
return {
|
|
1124
|
+
getFiles() {
|
|
1125
|
+
return completed ? memorySink.getFiles() : [];
|
|
1126
|
+
},
|
|
1127
|
+
async write(resource, signal) {
|
|
1128
|
+
signal?.throwIfAborted();
|
|
1129
|
+
if (state !== "empty") {
|
|
1130
|
+
throw new TileConversionError6(
|
|
1131
|
+
"SINGLE_MESH_SINK_UNAVAILABLE",
|
|
1132
|
+
"The sink accepts exactly one mesh before finalization or abort"
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
const json = JSON.stringify(createMeshTileset(resource, geometricError));
|
|
1136
|
+
state = "writing";
|
|
1137
|
+
await memorySink.write(
|
|
1138
|
+
{ resourceId: "mesh.glb", parts: [resource.glb], contentType: "model/gltf-binary" },
|
|
1139
|
+
signal
|
|
1140
|
+
);
|
|
1141
|
+
await memorySink.write(
|
|
1142
|
+
{ resourceId: "tileset.json", parts: [json], contentType: "application/json" },
|
|
1143
|
+
signal
|
|
1144
|
+
);
|
|
1145
|
+
state = "written";
|
|
1146
|
+
},
|
|
1147
|
+
async finalize(report) {
|
|
1148
|
+
if (state !== "written") {
|
|
1149
|
+
throw new TileConversionError6(
|
|
1150
|
+
"SINGLE_MESH_SINK_INCOMPLETE",
|
|
1151
|
+
"Finalization requires exactly one successfully written mesh"
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
await memorySink.finalize(report);
|
|
1155
|
+
state = "closed";
|
|
1156
|
+
completed = true;
|
|
1157
|
+
},
|
|
1158
|
+
async abort(error) {
|
|
1159
|
+
state = "closed";
|
|
1160
|
+
completed = false;
|
|
1161
|
+
await memorySink.abort(error);
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
function createMeshTileset(resource, geometricError) {
|
|
1166
|
+
const reference = resource.spatialReference;
|
|
1167
|
+
const outputCrs = reference.targetCrs || reference.sourceCrs;
|
|
1168
|
+
const heightReference = reference.targetHeightReference === "native" ? reference.heightReference : reference.targetHeightReference;
|
|
1169
|
+
if (!["native", "transformed"].includes(reference.status) || outputCrs !== "EPSG:4978" || heightReference !== "ellipsoidal" || reference.status === "native" && (reference.coordinateFrame !== "geocentric" || reference.axisOrder !== "xyz" || reference.verticalUnitScale !== 1 || reference.units?.some((unit) => unit !== "meter"))) {
|
|
1170
|
+
throw new TileConversionError6(
|
|
1171
|
+
"MESH_TILESET_FRAME_UNSUPPORTED",
|
|
1172
|
+
"Single-mesh tilesets require resolved EPSG:4978 xyz coordinates in meters with ellipsoidal heights"
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
if (!Number.isFinite(resource.maximumPositionError) || resource.maximumPositionError < 0 || resource.maximumPositionError > geometricError) {
|
|
1176
|
+
throw new TileConversionError6(
|
|
1177
|
+
"MESH_GEOMETRIC_ERROR_INVALID",
|
|
1178
|
+
"geometricError must include the measured position reconstruction error"
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
const origin = [...resource.origin];
|
|
1182
|
+
const [minimum, maximum] = resource.localBoundingBox.map((bound) => [...bound]);
|
|
1183
|
+
if (origin.length !== 3 || minimum.length !== 3 || maximum.length !== 3 || [...origin, ...minimum, ...maximum].some((value) => !Number.isFinite(value)) || minimum.some((value, axis) => value > maximum[axis])) {
|
|
1184
|
+
throw new TileConversionError6(
|
|
1185
|
+
"MESH_TILESET_BOUNDS_INVALID",
|
|
1186
|
+
"Origin and local bounds must be finite xyz coordinates with ordered bounds"
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
const center = minimum.map((value, axis) => value / 2 + maximum[axis] / 2);
|
|
1190
|
+
const halfSize = minimum.map((value, axis) => maximum[axis] / 2 - value / 2);
|
|
1191
|
+
return {
|
|
1192
|
+
asset: { version: "1.1" },
|
|
1193
|
+
geometricError,
|
|
1194
|
+
root: {
|
|
1195
|
+
// 3D Tiles applies y-up -> z-up to GLB content before the tile transform.
|
|
1196
|
+
transform: [1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, ...origin, 1],
|
|
1197
|
+
boundingVolume: {
|
|
1198
|
+
// Bounds use the same y-up -> z-up rotation before the inverse tile rotation.
|
|
1199
|
+
box: [
|
|
1200
|
+
center[0],
|
|
1201
|
+
-center[2],
|
|
1202
|
+
center[1],
|
|
1203
|
+
halfSize[0],
|
|
1204
|
+
0,
|
|
1205
|
+
0,
|
|
1206
|
+
0,
|
|
1207
|
+
0,
|
|
1208
|
+
halfSize[1],
|
|
1209
|
+
0,
|
|
1210
|
+
-halfSize[2],
|
|
1211
|
+
0
|
|
1212
|
+
]
|
|
1213
|
+
},
|
|
1214
|
+
geometricError,
|
|
1215
|
+
refine: "REPLACE",
|
|
1216
|
+
content: { uri: "mesh.glb" }
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
function createMeshTilesetSink(options) {
|
|
1221
|
+
const { geometricError, maxMeshes } = options;
|
|
1222
|
+
if (!Number.isFinite(geometricError) || geometricError < 0 || !Number.isSafeInteger(maxMeshes) || maxMeshes < 1) {
|
|
1223
|
+
throw new TileConversionError6(
|
|
1224
|
+
"MESH_TILESET_OPTIONS_INVALID",
|
|
1225
|
+
"A leaf collection requires finite nonnegative geometricError and positive maxMeshes"
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
const memory = createBoundedMemoryTileConversionSink(options);
|
|
1229
|
+
const children = [];
|
|
1230
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
1231
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
1232
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
1233
|
+
let state = "open";
|
|
1234
|
+
let completed = false;
|
|
1235
|
+
return {
|
|
1236
|
+
/** Exposes the complete package atomically. */
|
|
1237
|
+
getFiles: () => completed ? memory.getFiles() : [],
|
|
1238
|
+
/** Retains one placement and its corresponding encoded GLB. */
|
|
1239
|
+
async write(resource, signal) {
|
|
1240
|
+
signal?.throwIfAborted();
|
|
1241
|
+
if (state !== "open")
|
|
1242
|
+
throw new TileConversionError6(
|
|
1243
|
+
"MESH_TILESET_SINK_UNAVAILABLE",
|
|
1244
|
+
"The mesh sink is busy or closed"
|
|
1245
|
+
);
|
|
1246
|
+
if (children.length >= maxMeshes || identifiers.has(resource.id))
|
|
1247
|
+
throw new TileConversionError6(
|
|
1248
|
+
"MESH_TILESET_PLACEMENT_INVALID",
|
|
1249
|
+
"Mesh count exceeds maxMeshes or a placement identifier is repeated"
|
|
1250
|
+
);
|
|
1251
|
+
const tile = createMeshTileset(resource, geometricError).root;
|
|
1252
|
+
const lower = resource.localBoundingBox[0].map(
|
|
1253
|
+
(value, axis) => value + resource.origin[axis]
|
|
1254
|
+
);
|
|
1255
|
+
const upper = resource.localBoundingBox[1].map(
|
|
1256
|
+
(value, axis) => value + resource.origin[axis]
|
|
1257
|
+
);
|
|
1258
|
+
if ([...lower, ...upper].some((value) => !Number.isFinite(value)))
|
|
1259
|
+
throw new TileConversionError6(
|
|
1260
|
+
"MESH_TILESET_BOUNDS_INVALID",
|
|
1261
|
+
"Absolute bounds must be finite"
|
|
1262
|
+
);
|
|
1263
|
+
const resourceId = `meshes/${children.length}.glb`;
|
|
1264
|
+
tile.content = { uri: resourceId };
|
|
1265
|
+
state = "writing";
|
|
1266
|
+
await memory.write(
|
|
1267
|
+
{ resourceId, parts: [resource.glb], contentType: "model/gltf-binary" },
|
|
1268
|
+
signal
|
|
1269
|
+
);
|
|
1270
|
+
children.push(tile);
|
|
1271
|
+
identifiers.add(resource.id);
|
|
1272
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
1273
|
+
minimum[axis] = Math.min(minimum[axis], lower[axis]);
|
|
1274
|
+
maximum[axis] = Math.max(maximum[axis], upper[axis]);
|
|
1275
|
+
}
|
|
1276
|
+
state = "open";
|
|
1277
|
+
},
|
|
1278
|
+
/** Writes the enclosing root only when every leaf has completed. */
|
|
1279
|
+
async finalize(report) {
|
|
1280
|
+
if (state !== "open" || !children.length)
|
|
1281
|
+
throw new TileConversionError6(
|
|
1282
|
+
"MESH_TILESET_SINK_INCOMPLETE",
|
|
1283
|
+
"Finalization requires written mesh leaves"
|
|
1284
|
+
);
|
|
1285
|
+
state = "writing";
|
|
1286
|
+
const center = minimum.map((value, axis) => value / 2 + maximum[axis] / 2);
|
|
1287
|
+
const halfSize = minimum.map((value, axis) => maximum[axis] / 2 - value / 2);
|
|
1288
|
+
const rootGeometricError = Math.max(geometricError, 2 * Math.hypot(...halfSize));
|
|
1289
|
+
if (!Number.isFinite(rootGeometricError))
|
|
1290
|
+
throw new TileConversionError6(
|
|
1291
|
+
"MESH_TILESET_BOUNDS_INVALID",
|
|
1292
|
+
"Root bounds must have a finite diagonal"
|
|
1293
|
+
);
|
|
1294
|
+
const json = JSON.stringify({
|
|
1295
|
+
asset: { version: "1.1" },
|
|
1296
|
+
geometricError: rootGeometricError,
|
|
1297
|
+
root: {
|
|
1298
|
+
boundingVolume: {
|
|
1299
|
+
box: [
|
|
1300
|
+
center[0],
|
|
1301
|
+
center[1],
|
|
1302
|
+
center[2],
|
|
1303
|
+
halfSize[0],
|
|
1304
|
+
0,
|
|
1305
|
+
0,
|
|
1306
|
+
0,
|
|
1307
|
+
halfSize[1],
|
|
1308
|
+
0,
|
|
1309
|
+
0,
|
|
1310
|
+
0,
|
|
1311
|
+
halfSize[2]
|
|
1312
|
+
]
|
|
1313
|
+
},
|
|
1314
|
+
geometricError: rootGeometricError,
|
|
1315
|
+
refine: "ADD",
|
|
1316
|
+
children
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
await memory.write({
|
|
1320
|
+
resourceId: "tileset.json",
|
|
1321
|
+
parts: [json],
|
|
1322
|
+
contentType: "application/json"
|
|
1323
|
+
});
|
|
1324
|
+
await memory.finalize(report);
|
|
1325
|
+
state = "closed";
|
|
1326
|
+
completed = true;
|
|
1327
|
+
},
|
|
1328
|
+
/** Discards files and hierarchy metadata on failure or cancellation. */
|
|
1329
|
+
async abort(error) {
|
|
1330
|
+
state = "closed";
|
|
1331
|
+
completed = false;
|
|
1332
|
+
children.length = 0;
|
|
1333
|
+
identifiers.clear();
|
|
1334
|
+
await memory.abort(error);
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// src/v5/conversion-archive.ts
|
|
1340
|
+
import {
|
|
1341
|
+
Tiles3DArchiveWriter,
|
|
1342
|
+
encodeTiles3DArchiveInBatches
|
|
1343
|
+
} from "@loaders.gl/3d-tiles/3d-tiles-archive-writer";
|
|
1344
|
+
import { SLPKWriter, encodeSLPKArchiveInBatches } from "@loaders.gl/i3s/i3s-slpk-writer";
|
|
1345
|
+
import { TileConversionError as TileConversionError7 } from "@loaders.gl/tile-converter/v5/core";
|
|
1346
|
+
async function createTileConversionArchive(files, options) {
|
|
1347
|
+
const { resources, format, maxArchiveBytes, signal } = prepareArchiveInput(files, options);
|
|
1348
|
+
const writer = format === "3tz" ? Tiles3DArchiveWriter : SLPKWriter;
|
|
1349
|
+
const archive = await writer.encode(resources, { [format]: { maxArchiveBytes, signal } });
|
|
1350
|
+
return new Blob([archive], { type: writer.mimeTypes[0] });
|
|
1351
|
+
}
|
|
1352
|
+
async function* encodeTileConversionArchiveInBatches(files, options) {
|
|
1353
|
+
const { resources, format, maxArchiveBytes, signal } = prepareArchiveInput(files, options);
|
|
1354
|
+
const encode = format === "3tz" ? encodeTiles3DArchiveInBatches : encodeSLPKArchiveInBatches;
|
|
1355
|
+
yield* encode(resources, { [format]: { maxArchiveBytes, signal } });
|
|
1356
|
+
}
|
|
1357
|
+
function prepareArchiveInput(files, options) {
|
|
1358
|
+
const { format, maxArchiveBytes, signal } = options;
|
|
1359
|
+
if (!Number.isSafeInteger(maxArchiveBytes) || maxArchiveBytes < 0) {
|
|
1360
|
+
throw new TileConversionError7(
|
|
1361
|
+
"INVALID_ARCHIVE_BYTE_LIMIT",
|
|
1362
|
+
"Archive byte limit must be a non-negative safe integer"
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
if (format !== "3tz" && format !== "slpk") {
|
|
1366
|
+
throw new TileConversionError7("INVALID_ARCHIVE_FORMAT", "Expected 3tz or slpk archive format");
|
|
1367
|
+
}
|
|
1368
|
+
const resources = Object.fromEntries(files.map((file) => [file.resourceId, file.blob]));
|
|
1369
|
+
if (Object.keys(resources).length !== files.length) {
|
|
1370
|
+
throw new TileConversionError7(
|
|
1371
|
+
"DUPLICATE_ARCHIVE_RESOURCE",
|
|
1372
|
+
"Archive resource identifiers must be unique"
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
return { resources, format, maxArchiveBytes, signal };
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// src/v5/mesh-tileset-archive.ts
|
|
1379
|
+
import { Tiles3DArchiveWriter as Tiles3DArchiveWriter2 } from "@loaders.gl/3d-tiles/3d-tiles-archive-writer";
|
|
1380
|
+
import { TileConversionError as TileConversionError8 } from "@loaders.gl/tile-converter/v5/core";
|
|
1381
|
+
async function createSingleMeshTilesetArchive(files, options) {
|
|
1382
|
+
const { maxArchiveBytes, signal } = options;
|
|
1383
|
+
if (!Number.isSafeInteger(maxArchiveBytes) || maxArchiveBytes < 0) {
|
|
1384
|
+
throw new TileConversionError8(
|
|
1385
|
+
"INVALID_ARCHIVE_BYTE_LIMIT",
|
|
1386
|
+
"Archive byte limit must be a non-negative safe integer"
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
if (files.length !== 2 || !files.some((file) => file.resourceId === "mesh.glb") || !files.some((file) => file.resourceId === "tileset.json")) {
|
|
1390
|
+
throw new TileConversionError8(
|
|
1391
|
+
"INVALID_SINGLE_MESH_ARCHIVE",
|
|
1392
|
+
"Expected mesh.glb and tileset.json from a finalized single-mesh sink"
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
const resources = Object.fromEntries(files.map((file) => [file.resourceId, file.blob]));
|
|
1396
|
+
const archive = await Tiles3DArchiveWriter2.encode(resources, { "3tz": { maxArchiveBytes, signal } });
|
|
1397
|
+
return new Blob([archive], { type: Tiles3DArchiveWriter2.mimeTypes[0] });
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// src/v5/mesh-source.ts
|
|
1401
|
+
import { Matrix4 } from "@math.gl/core";
|
|
1402
|
+
import { TileConversionError as TileConversionError11 } from "@loaders.gl/tile-converter/v5/core";
|
|
1403
|
+
import { createTilesetConversionSource } from "@loaders.gl/tile-converter/v5/core";
|
|
1404
|
+
|
|
1405
|
+
// src/v5/mesh-source-texture.ts
|
|
1406
|
+
import { TileConversionError as TileConversionError9 } from "@loaders.gl/tile-converter/v5/core";
|
|
1407
|
+
function mapMeshSourceTexture(textureInfo) {
|
|
1408
|
+
const texture = textureInfo?.texture;
|
|
1409
|
+
const image = texture?.source;
|
|
1410
|
+
const sampler = texture?.sampler;
|
|
1411
|
+
const extensions = textureInfo?.extensions || {};
|
|
1412
|
+
const hasTransform = Object.hasOwn(extensions, "KHR_texture_transform");
|
|
1413
|
+
const transform = extensions.KHR_texture_transform;
|
|
1414
|
+
if (!textureInfo || textureInfo.texCoord !== void 0 && textureInfo.texCoord !== 0 || Object.keys(extensions).some((name) => name !== "KHR_texture_transform") || hasTransform && (!transform || typeof transform !== "object" || Array.isArray(transform) || transform.texCoord !== void 0 && transform.texCoord !== 0 || Object.keys(transform.extensions || {}).length || Object.keys(transform).some(
|
|
1415
|
+
(name) => !["offset", "rotation", "scale", "texCoord", "extensions", "extras"].includes(name)
|
|
1416
|
+
)) || Object.keys(textureInfo).some(
|
|
1417
|
+
(name) => !["id", "index", "texture", "texCoord", "extensions", "extras"].includes(name)
|
|
1418
|
+
) || !texture || Object.keys(texture).some(
|
|
1419
|
+
(name) => !["id", "source", "sampler", "name", "extensions", "extras"].includes(name)
|
|
1420
|
+
) || Object.keys(texture.extensions || {}).length || !image || image.uri !== void 0 && image.bufferView !== void 0 || Object.keys(image.extensions || {}).length || image.uri === void 0 && (!(image.bufferView?.data instanceof Uint8Array) || Object.keys(image.bufferView.extensions || {}).length || !["image/png", "image/jpeg"].includes(image.mimeType || "")) || sampler && (Object.keys(sampler.extensions || {}).length || Object.keys(sampler).some(
|
|
1421
|
+
(name) => ![
|
|
1422
|
+
"id",
|
|
1423
|
+
"parameters",
|
|
1424
|
+
"wrapS",
|
|
1425
|
+
"wrapT",
|
|
1426
|
+
"magFilter",
|
|
1427
|
+
"minFilter",
|
|
1428
|
+
"name",
|
|
1429
|
+
"extensions",
|
|
1430
|
+
"extras"
|
|
1431
|
+
].includes(name)
|
|
1432
|
+
))) {
|
|
1433
|
+
throw new TileConversionError9(
|
|
1434
|
+
"MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
1435
|
+
"Base-color textures require a PNG/JPEG bufferView or base64 data URI, TEXCOORD_0, glTF sampling and an optional KHR_texture_transform on TEXCOORD_0"
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
const selectedSampler = sampler ? {
|
|
1439
|
+
wrapS: sampler.wrapS,
|
|
1440
|
+
wrapT: sampler.wrapT,
|
|
1441
|
+
magFilter: sampler.magFilter,
|
|
1442
|
+
minFilter: sampler.minFilter
|
|
1443
|
+
} : void 0;
|
|
1444
|
+
return {
|
|
1445
|
+
...mapMeshSourceImage(image),
|
|
1446
|
+
...hasTransform ? {
|
|
1447
|
+
transform: {
|
|
1448
|
+
offset: transform.offset,
|
|
1449
|
+
rotation: transform.rotation,
|
|
1450
|
+
scale: transform.scale
|
|
1451
|
+
}
|
|
1452
|
+
} : {},
|
|
1453
|
+
...selectedSampler && Object.values(selectedSampler).some((value) => value !== void 0) ? { sampler: selectedSampler } : {}
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
function mapMeshSourceImage(image) {
|
|
1457
|
+
if (image.uri === void 0) {
|
|
1458
|
+
return { data: image.bufferView.data, mimeType: image.mimeType };
|
|
1459
|
+
}
|
|
1460
|
+
const match = typeof image.uri === "string" ? /^data:(image\/(?:png|jpeg));base64,([A-Za-z0-9+/]+={0,2})$/.exec(image.uri) : null;
|
|
1461
|
+
if (!match || image.mimeType !== void 0 && image.mimeType !== match[1]) {
|
|
1462
|
+
throw new TileConversionError9(
|
|
1463
|
+
"MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
1464
|
+
"Inline images require a PNG/JPEG base64 data URI and matching optional mimeType"
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
let encodedBytes;
|
|
1468
|
+
try {
|
|
1469
|
+
encodedBytes = atob(match[2]);
|
|
1470
|
+
} catch {
|
|
1471
|
+
throw new TileConversionError9("MESH_SOURCE_TEXTURE_UNSUPPORTED", "Invalid inline image base64");
|
|
1472
|
+
}
|
|
1473
|
+
return {
|
|
1474
|
+
data: Uint8Array.from(encodedBytes, (character) => character.charCodeAt(0)),
|
|
1475
|
+
mimeType: match[1]
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// src/v5/mesh-source-features.ts
|
|
1480
|
+
import { TileConversionError as TileConversionError10 } from "@loaders.gl/tile-converter/v5/core";
|
|
1481
|
+
function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
|
|
1482
|
+
const declarations = primitive.extensions?.EXT_mesh_features?.featureIds;
|
|
1483
|
+
const metadata = gltf.extensions?.EXT_structural_metadata;
|
|
1484
|
+
const legacy = primitive.attributes._BATCHID;
|
|
1485
|
+
if (!declarations && !legacy && !metadata && !Object.keys(payload.batchTableJson || {}).length && !payload.header?.batchLength) {
|
|
1486
|
+
if (Object.keys(primitive.attributes).some((name) => /^_FEATURE_ID_\d+$/.test(name)))
|
|
1487
|
+
throw new TileConversionError10(
|
|
1488
|
+
"MESH_FEATURE_MAPPING_REQUIRED",
|
|
1489
|
+
"Feature attributes require an EXT_mesh_features declaration"
|
|
1490
|
+
);
|
|
1491
|
+
return void 0;
|
|
1492
|
+
}
|
|
1493
|
+
if (!options)
|
|
1494
|
+
throw new TileConversionError10(
|
|
1495
|
+
"MESH_FEATURE_SCHEMA_REQUIRED",
|
|
1496
|
+
"Feature extraction requires an explicit Arrow schema and class mapping"
|
|
1497
|
+
);
|
|
1498
|
+
if (!options.metadataClass)
|
|
1499
|
+
throw new TileConversionError10(
|
|
1500
|
+
"MESH_FEATURE_CLASS_REQUIRED",
|
|
1501
|
+
"Feature metadata class must be explicit"
|
|
1502
|
+
);
|
|
1503
|
+
let count;
|
|
1504
|
+
let attribute = legacy;
|
|
1505
|
+
let properties;
|
|
1506
|
+
if (declarations) {
|
|
1507
|
+
if (legacy || declarations.length !== 1 || metadata?.propertyTables?.length !== 1 || metadata.propertyTextures?.length || metadata.propertyAttributes?.length)
|
|
1508
|
+
throw new TileConversionError10(
|
|
1509
|
+
"MESH_FEATURE_MAPPING_UNSUPPORTED",
|
|
1510
|
+
"Only one attribute-backed feature set and one property table are supported"
|
|
1511
|
+
);
|
|
1512
|
+
const declaration = declarations[0];
|
|
1513
|
+
if (!Number.isInteger(declaration.attribute) || declaration.attribute < 0 || declaration.propertyTable !== 0 || declaration.texture || declaration.implicit || declaration.nullFeatureId !== void 0)
|
|
1514
|
+
throw new TileConversionError10(
|
|
1515
|
+
"MESH_FEATURE_MAPPING_UNSUPPORTED",
|
|
1516
|
+
"Features require an attribute and property table 0 without null/texture/implicit IDs"
|
|
1517
|
+
);
|
|
1518
|
+
attribute = primitive.attributes[`_FEATURE_ID_${declaration.attribute}`];
|
|
1519
|
+
const table = metadata.propertyTables[0];
|
|
1520
|
+
if (table.class !== options.metadataClass || table.count !== declaration.featureCount)
|
|
1521
|
+
throw new TileConversionError10(
|
|
1522
|
+
"MESH_FEATURE_CLASS_MISMATCH",
|
|
1523
|
+
"Feature class and declared table counts must agree"
|
|
1524
|
+
);
|
|
1525
|
+
count = table.count;
|
|
1526
|
+
properties = {};
|
|
1527
|
+
const classProperties = metadata.schema?.classes?.[table.class]?.properties;
|
|
1528
|
+
if (!classProperties)
|
|
1529
|
+
throw new TileConversionError10(
|
|
1530
|
+
"MESH_FEATURE_SCHEMA_REQUIRED",
|
|
1531
|
+
"A decoded inline structural metadata class is required"
|
|
1532
|
+
);
|
|
1533
|
+
if (Object.keys(table.properties || {}).some((name) => !Object.hasOwn(classProperties, name)))
|
|
1534
|
+
throw new TileConversionError10(
|
|
1535
|
+
"MESH_FEATURE_PROPERTY_UNMAPPED",
|
|
1536
|
+
"Property table columns must be declared in the metadata class"
|
|
1537
|
+
);
|
|
1538
|
+
for (const [name, property] of Object.entries(classProperties)) {
|
|
1539
|
+
const column = table.properties?.[name];
|
|
1540
|
+
if (!["SCALAR", "STRING"].includes(property.type) || property.array || property.noData !== void 0 || ["INT64", "UINT64"].includes(property.componentType || "") && (property.normalized || property.offset !== void 0 || property.scale !== void 0 || column?.offset !== void 0 || column?.scale !== void 0))
|
|
1541
|
+
throw new TileConversionError10(
|
|
1542
|
+
"MESH_FEATURE_PROPERTY_UNSUPPORTED",
|
|
1543
|
+
`Property ${name} requires a separately qualified semantic mapping`
|
|
1544
|
+
);
|
|
1545
|
+
const data = column?.data;
|
|
1546
|
+
if (data === void 0 && property.default === void 0)
|
|
1547
|
+
throw new TileConversionError10(
|
|
1548
|
+
"MESH_FEATURE_PROPERTY_UNAVAILABLE",
|
|
1549
|
+
`Property ${name} has not been decoded`
|
|
1550
|
+
);
|
|
1551
|
+
properties[name] = data ?? Array.from({ length: count }, () => property.default);
|
|
1552
|
+
}
|
|
1553
|
+
} else {
|
|
1554
|
+
if (metadata)
|
|
1555
|
+
throw new TileConversionError10(
|
|
1556
|
+
"MESH_FEATURE_MAPPING_UNSUPPORTED",
|
|
1557
|
+
"Structural metadata requires an attribute-backed feature declaration"
|
|
1558
|
+
);
|
|
1559
|
+
count = payload.header?.batchLength ?? Number(payload.featureTableJson?.BATCH_LENGTH);
|
|
1560
|
+
properties = payload.batchTableJson || {};
|
|
1561
|
+
}
|
|
1562
|
+
const vertexCount = mesh.attributes.POSITION.value.length / 3;
|
|
1563
|
+
if (!Number.isSafeInteger(count) || count <= 0 || !attribute || attribute.components !== 1 || attribute.normalized || attribute.value.length !== vertexCount || Array.from(attribute.value).some(
|
|
1564
|
+
(value) => !Number.isSafeInteger(value) || value < 0 || value >= count
|
|
1565
|
+
))
|
|
1566
|
+
throw new TileConversionError10(
|
|
1567
|
+
"MESH_FEATURE_IDS_INVALID",
|
|
1568
|
+
"Feature IDs must be one valid property-table row index per vertex"
|
|
1569
|
+
);
|
|
1570
|
+
const selectedAttribute = declarations ? `_FEATURE_ID_${declarations[0].attribute}` : "_BATCHID";
|
|
1571
|
+
if (Object.keys(primitive.attributes).some(
|
|
1572
|
+
(name) => (name === "_BATCHID" || /^_FEATURE_ID_\d+$/.test(name)) && name !== selectedAttribute
|
|
1573
|
+
))
|
|
1574
|
+
throw new TileConversionError10(
|
|
1575
|
+
"MESH_FEATURE_MAPPING_UNSUPPORTED",
|
|
1576
|
+
"Additional feature ID sets require separate mappings"
|
|
1577
|
+
);
|
|
1578
|
+
for (const [name, values] of Object.entries(properties)) {
|
|
1579
|
+
if (!values || !Array.isArray(values) && !ArrayBuffer.isView(values) || values.length !== count)
|
|
1580
|
+
throw new TileConversionError10(
|
|
1581
|
+
"MESH_FEATURE_PROPERTY_UNAVAILABLE",
|
|
1582
|
+
`Property ${name} must be a decoded column matching the table count`
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
const sourceIdentifier = options.sourceFeatureIdProperty;
|
|
1586
|
+
if (sourceIdentifier && !properties[sourceIdentifier])
|
|
1587
|
+
throw new TileConversionError10(
|
|
1588
|
+
"MESH_FEATURE_ID_PROPERTY_REQUIRED",
|
|
1589
|
+
"Stable source identifier property is missing"
|
|
1590
|
+
);
|
|
1591
|
+
const batches = convertFeatureAttributesToArrowBatches(
|
|
1592
|
+
Array.from({ length: count }, (_, index) => ({
|
|
1593
|
+
featureId: sourceIdentifier ? properties[sourceIdentifier][index] : index,
|
|
1594
|
+
metadataClass: options.metadataClass,
|
|
1595
|
+
properties: Object.fromEntries(
|
|
1596
|
+
Object.entries(properties).filter(([name]) => name !== sourceIdentifier).map(([name, values]) => [name, values[index]])
|
|
1597
|
+
)
|
|
1598
|
+
})),
|
|
1599
|
+
options
|
|
1600
|
+
);
|
|
1601
|
+
const indices = mesh.indices?.value;
|
|
1602
|
+
const triangleFeatureIndices = new Uint32Array((indices?.length ?? vertexCount) / 3);
|
|
1603
|
+
for (let triangle = 0; triangle < triangleFeatureIndices.length; triangle++) {
|
|
1604
|
+
const rows = [0, 1, 2].map(
|
|
1605
|
+
(corner) => attribute.value[indices?.[triangle * 3 + corner] ?? triangle * 3 + corner]
|
|
1606
|
+
);
|
|
1607
|
+
if (rows.some((row) => row !== rows[0]))
|
|
1608
|
+
throw new TileConversionError10(
|
|
1609
|
+
"MESH_FEATURE_TRIANGLE_MIXED",
|
|
1610
|
+
"All vertices of a triangle must reference the same feature row"
|
|
1611
|
+
);
|
|
1612
|
+
triangleFeatureIndices[triangle] = Number(rows[0]);
|
|
1613
|
+
}
|
|
1614
|
+
return {
|
|
1615
|
+
batches,
|
|
1616
|
+
triangleFeatureIndices,
|
|
1617
|
+
featureIdField: options.featureIdField || "feature_id",
|
|
1618
|
+
integer64Encoding: options.integer64Encoding
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// src/v5/mesh-source.ts
|
|
1623
|
+
function createMeshTilesetConversionSource(tileset, options = {}) {
|
|
1624
|
+
const source = createTilesetConversionSource(tileset, options);
|
|
1625
|
+
return {
|
|
1626
|
+
/** Validates the source frame and supported dataset metadata before traversal. */
|
|
1627
|
+
async inspect(signal) {
|
|
1628
|
+
const metadata = await source.inspect(signal);
|
|
1629
|
+
const reference = metadata.spatialReference;
|
|
1630
|
+
if (metadata.type !== "TILES3D" || !reference || reference.status !== "native" || reference.sourceCrs !== "EPSG:4978" || reference.heightReference !== "ellipsoidal" || reference.axisOrder !== "xyz" || reference.verticalUnitScale !== 1 || reference.units?.some((unit) => unit !== "meter")) {
|
|
1631
|
+
throw new TileConversionError11(
|
|
1632
|
+
"MESH_SOURCE_FRAME_UNSUPPORTED",
|
|
1633
|
+
"Mesh extraction requires native EPSG:4978 xyz coordinates in meters and ellipsoidal heights"
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
if (metadata.tileset.metadata || metadata.tileset.groups?.length) {
|
|
1637
|
+
throw new TileConversionError11(
|
|
1638
|
+
"MESH_SOURCE_METADATA_UNSUPPORTED",
|
|
1639
|
+
"Tileset/group metadata requires a separate mapping"
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
return metadata;
|
|
1643
|
+
},
|
|
1644
|
+
/** Yields each primitive placement while the shared traversal owns content lifetime. */
|
|
1645
|
+
async *read(inspection, signal) {
|
|
1646
|
+
for await (const item of source.read(inspection, signal)) {
|
|
1647
|
+
const placementReference = item.tile.header._spatialReference || tileset.spatialReference;
|
|
1648
|
+
if (placementReference && (placementReference.status !== "native" || placementReference.sourceCrs !== "EPSG:4978"))
|
|
1649
|
+
throw new TileConversionError11(
|
|
1650
|
+
"MESH_SOURCE_FRAME_UNSUPPORTED",
|
|
1651
|
+
"Nested mesh placements require native ECEF coordinates"
|
|
1652
|
+
);
|
|
1653
|
+
if (item.tile.header.metadata || item.tile.header.implicitMetadata)
|
|
1654
|
+
throw new TileConversionError11(
|
|
1655
|
+
"MESH_SOURCE_METADATA_UNSUPPORTED",
|
|
1656
|
+
"Tile metadata requires a separate mapping"
|
|
1657
|
+
);
|
|
1658
|
+
for (const content of item.contents) {
|
|
1659
|
+
signal?.throwIfAborted();
|
|
1660
|
+
if (content.metadata || content.group !== void 0)
|
|
1661
|
+
throw new TileConversionError11(
|
|
1662
|
+
"MESH_SOURCE_METADATA_UNSUPPORTED",
|
|
1663
|
+
"Content metadata requires a separate mapping"
|
|
1664
|
+
);
|
|
1665
|
+
const payload = content.payload;
|
|
1666
|
+
if (!payload?.gltf || !["glTF", "gltf", "b3dm"].includes(payload.type || content.type || "")) {
|
|
1667
|
+
throw new TileConversionError11(
|
|
1668
|
+
"MESH_SOURCE_CONTENT_UNSUPPORTED",
|
|
1669
|
+
"Mesh extraction requires decoded GLB/glTF or B3DM content"
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
const gltf = payload.gltf;
|
|
1673
|
+
if (gltf.animations?.length || gltf.skins?.length || Object.keys(gltf.extensions || {}).some(
|
|
1674
|
+
(name) => !["EXT_structural_metadata", "CESIUM_RTC"].includes(name)
|
|
1675
|
+
)) {
|
|
1676
|
+
throw new TileConversionError11(
|
|
1677
|
+
"MESH_SOURCE_SCENE_UNSUPPORTED",
|
|
1678
|
+
"Animated/skinned scenes and unmapped scene extensions are unsupported"
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
const axis = payload.gltfUpAxis || "Y";
|
|
1682
|
+
if (!["X", "Y", "Z"].includes(axis))
|
|
1683
|
+
throw new TileConversionError11("MESH_SOURCE_AXIS_UNSUPPORTED", "Unknown glTF up axis");
|
|
1684
|
+
const placement = new Matrix4(item.tile.computedTransform);
|
|
1685
|
+
const rtc = payload.rtcCenter || gltf.extensions?.CESIUM_RTC?.center;
|
|
1686
|
+
if (rtc) {
|
|
1687
|
+
if (rtc.length !== 3 || rtc.some((value) => !Number.isFinite(value)))
|
|
1688
|
+
throw new TileConversionError11(
|
|
1689
|
+
"MESH_SOURCE_TRANSFORM_INVALID",
|
|
1690
|
+
"RTC center must contain finite xyz coordinates"
|
|
1691
|
+
);
|
|
1692
|
+
placement.translate(rtc);
|
|
1693
|
+
}
|
|
1694
|
+
if (axis === "Y")
|
|
1695
|
+
placement.multiplyRight(new Matrix4([1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1]));
|
|
1696
|
+
if (axis === "X")
|
|
1697
|
+
placement.multiplyRight(new Matrix4([0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 1]));
|
|
1698
|
+
const roots = gltf.scene?.nodes || (gltf.scenes?.length === 1 ? gltf.scenes[0].nodes : void 0);
|
|
1699
|
+
if (!roots?.length)
|
|
1700
|
+
throw new TileConversionError11(
|
|
1701
|
+
"MESH_SOURCE_SCENE_UNSUPPORTED",
|
|
1702
|
+
"A default scene or exactly one scene is required"
|
|
1703
|
+
);
|
|
1704
|
+
let primitiveIndex = 0;
|
|
1705
|
+
for (const selected of traverseMeshNodes(roots, placement)) {
|
|
1706
|
+
const resource = extractPrimitive(
|
|
1707
|
+
selected.primitive,
|
|
1708
|
+
selected.transform,
|
|
1709
|
+
`${item.tile.id}/${content.index}/${primitiveIndex++}`
|
|
1710
|
+
);
|
|
1711
|
+
const features = extractMeshFeatures(
|
|
1712
|
+
gltf,
|
|
1713
|
+
selected.primitive,
|
|
1714
|
+
payload,
|
|
1715
|
+
resource.mesh,
|
|
1716
|
+
options.features
|
|
1717
|
+
);
|
|
1718
|
+
yield { ...resource, features };
|
|
1719
|
+
}
|
|
1720
|
+
if (!primitiveIndex)
|
|
1721
|
+
throw new TileConversionError11(
|
|
1722
|
+
"MESH_SOURCE_CONTENT_EMPTY",
|
|
1723
|
+
"Declared content contains no supported mesh primitive"
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
function* traverseMeshNodes(nodes, parent, ancestors = /* @__PURE__ */ new Set()) {
|
|
1731
|
+
for (const node of nodes) {
|
|
1732
|
+
if (ancestors.has(node) || node.skin || node.weights || Object.keys(node.extensions || {}).length)
|
|
1733
|
+
throw new TileConversionError11(
|
|
1734
|
+
"MESH_SOURCE_NODE_UNSUPPORTED",
|
|
1735
|
+
"Cyclic, skinned, morphed, or extended nodes are unsupported"
|
|
1736
|
+
);
|
|
1737
|
+
const transform = new Matrix4(parent).multiplyRight(
|
|
1738
|
+
node.matrix ? new Matrix4(node.matrix) : new Matrix4().translate(node.translation || [0, 0, 0]).multiplyRight(new Matrix4().fromQuaternion(node.rotation || [0, 0, 0, 1])).scale(node.scale || [1, 1, 1])
|
|
1739
|
+
);
|
|
1740
|
+
if (node.mesh) {
|
|
1741
|
+
if (node.mesh.weights || Object.keys(node.mesh.extensions || {}).length)
|
|
1742
|
+
throw new TileConversionError11(
|
|
1743
|
+
"MESH_SOURCE_NODE_UNSUPPORTED",
|
|
1744
|
+
"Morph weights and mesh extensions are unsupported"
|
|
1745
|
+
);
|
|
1746
|
+
for (const primitive of node.mesh.primitives) yield { primitive, transform };
|
|
1747
|
+
}
|
|
1748
|
+
yield* traverseMeshNodes(node.children || [], transform, /* @__PURE__ */ new Set([...ancestors, node]));
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
function extractPrimitive(primitive, transform, id) {
|
|
1752
|
+
if (primitive.targets?.length || Object.keys(primitive.extensions || {}).some((name) => name !== "EXT_mesh_features"))
|
|
1753
|
+
throw new TileConversionError11(
|
|
1754
|
+
"MESH_SOURCE_PRIMITIVE_UNSUPPORTED",
|
|
1755
|
+
"Morph targets and unmapped primitive extensions are unsupported"
|
|
1756
|
+
);
|
|
1757
|
+
if (Array.from(transform).some((value) => !Number.isFinite(value)) || transform[3] !== 0 || transform[7] !== 0 || transform[11] !== 0 || transform[15] !== 1 || transform.determinant() <= 0)
|
|
1758
|
+
throw new TileConversionError11(
|
|
1759
|
+
"MESH_SOURCE_TRANSFORM_INVALID",
|
|
1760
|
+
"Mesh placement requires a finite invertible affine transform with positive determinant"
|
|
1761
|
+
);
|
|
1762
|
+
const attributes = {};
|
|
1763
|
+
for (const [name, accessor] of Object.entries(primitive.attributes)) {
|
|
1764
|
+
if (name === "_BATCHID" || /^_FEATURE_ID_\d+$/.test(name)) continue;
|
|
1765
|
+
if (!["POSITION", "NORMAL", "COLOR_0", "TEXCOORD_0"].includes(name))
|
|
1766
|
+
throw new TileConversionError11(
|
|
1767
|
+
"MESH_SOURCE_ATTRIBUTE_UNSUPPORTED",
|
|
1768
|
+
`Unsupported mesh attribute ${name}`
|
|
1769
|
+
);
|
|
1770
|
+
attributes[name] = {
|
|
1771
|
+
value: accessor.value,
|
|
1772
|
+
size: accessor.components,
|
|
1773
|
+
...accessor.normalized ? { normalized: true } : {}
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
const mesh = validateMeshGeometry(
|
|
1777
|
+
{
|
|
1778
|
+
topology: "triangle-list",
|
|
1779
|
+
mode: primitive.mode ?? 4,
|
|
1780
|
+
attributes,
|
|
1781
|
+
indices: primitive.indices ? { value: primitive.indices.value, size: 1 } : void 0
|
|
1782
|
+
},
|
|
1783
|
+
true
|
|
1784
|
+
);
|
|
1785
|
+
const sourcePositions = mesh.attributes.POSITION.value;
|
|
1786
|
+
const positions = new Float64Array(sourcePositions.length);
|
|
1787
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
1788
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
1789
|
+
for (let index = 0; index < positions.length; index += 3) {
|
|
1790
|
+
const placed = transform.transformAsPoint(
|
|
1791
|
+
Array.from(sourcePositions.subarray(index, index + 3))
|
|
1792
|
+
);
|
|
1793
|
+
positions.set(placed, index);
|
|
1794
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
1795
|
+
minimum[axis] = Math.min(minimum[axis], placed[axis]);
|
|
1796
|
+
maximum[axis] = Math.max(maximum[axis], placed[axis]);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
mesh.attributes.POSITION = { value: positions, size: 3 };
|
|
1800
|
+
if (mesh.attributes.NORMAL) {
|
|
1801
|
+
const values = mesh.attributes.NORMAL.value;
|
|
1802
|
+
const normalMatrix = new Matrix4(transform).invert().transpose();
|
|
1803
|
+
const normals = new Float32Array(values.length);
|
|
1804
|
+
for (let index = 0; index < values.length; index += 3) {
|
|
1805
|
+
const normal = normalMatrix.transformAsVector(Array.from(values.subarray(index, index + 3)));
|
|
1806
|
+
const length = Math.hypot(...normal);
|
|
1807
|
+
normals.set(
|
|
1808
|
+
normal.map((value) => value / length),
|
|
1809
|
+
index
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
mesh.attributes.NORMAL = { value: normals, size: 3 };
|
|
1813
|
+
}
|
|
1814
|
+
return {
|
|
1815
|
+
id,
|
|
1816
|
+
mesh,
|
|
1817
|
+
origin: minimum.map((value, axis) => value / 2 + maximum[axis] / 2),
|
|
1818
|
+
material: mapMaterial(primitive.material)
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
function mapMaterial(material) {
|
|
1822
|
+
if (!material) return void 0;
|
|
1823
|
+
const pbr = material.pbrMetallicRoughness;
|
|
1824
|
+
if (Object.keys(material).some(
|
|
1825
|
+
(name) => ![
|
|
1826
|
+
"id",
|
|
1827
|
+
"name",
|
|
1828
|
+
"pbrMetallicRoughness",
|
|
1829
|
+
"alphaMode",
|
|
1830
|
+
"alphaCutoff",
|
|
1831
|
+
"doubleSided",
|
|
1832
|
+
"emissiveFactor",
|
|
1833
|
+
"extras"
|
|
1834
|
+
].includes(name)
|
|
1835
|
+
) || Object.keys(pbr || {}).some(
|
|
1836
|
+
(name) => !["baseColorFactor", "baseColorTexture", "metallicFactor", "roughnessFactor"].includes(name)
|
|
1837
|
+
) || material.emissiveFactor?.some((value) => value !== 0))
|
|
1838
|
+
throw new TileConversionError11(
|
|
1839
|
+
"MESH_SOURCE_MATERIAL_UNSUPPORTED",
|
|
1840
|
+
"Only metallic-roughness factors and one embedded base-color image are supported"
|
|
1841
|
+
);
|
|
1842
|
+
return {
|
|
1843
|
+
baseColorFactor: pbr?.baseColorFactor,
|
|
1844
|
+
..."baseColorTexture" in (pbr || {}) ? { baseColorTexture: mapMeshSourceTexture(pbr.baseColorTexture) } : {},
|
|
1845
|
+
metallicFactor: pbr?.metallicFactor,
|
|
1846
|
+
roughnessFactor: pbr?.roughnessFactor,
|
|
1847
|
+
alphaMode: material.alphaMode,
|
|
1848
|
+
...material.alphaMode === "MASK" ? { alphaCutoff: material.alphaCutoff ?? 0.5 } : {},
|
|
1849
|
+
doubleSided: material.doubleSided
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// src/v5/i3s-mesh-conversion.ts
|
|
1854
|
+
import { encodeI3SMeshLayer } from "@loaders.gl/i3s";
|
|
1855
|
+
import { TileConversionError as TileConversionError12 } from "@loaders.gl/tile-converter/v5/core";
|
|
1856
|
+
import { createBoundedMemoryTileConversionSink as createBoundedMemoryTileConversionSink2 } from "@loaders.gl/tile-converter/v5/core";
|
|
1857
|
+
function createI3SMeshConversionCodec(options) {
|
|
1858
|
+
const reference = options.spatialContext.spatialReference;
|
|
1859
|
+
const height = reference.targetHeightReference === "native" ? reference.heightReference : reference.targetHeightReference;
|
|
1860
|
+
if (!["native", "transformed"].includes(reference.status) || (reference.targetCrs || reference.sourceCrs) !== "EPSG:4978" || height !== "ellipsoidal" || reference.status === "native" && (!["xyz", "unknown"].includes(reference.axisOrder) || reference.verticalUnitScale !== 1 || reference.units?.some((unit) => unit !== "meter")))
|
|
1861
|
+
throw new TileConversionError12(
|
|
1862
|
+
"I3S_MESH_FRAME_UNSUPPORTED",
|
|
1863
|
+
"I3S mesh authoring requires resolved ECEF coordinates and ellipsoidal heights"
|
|
1864
|
+
);
|
|
1865
|
+
return {
|
|
1866
|
+
/** Places a source mesh in ECEF and authors one layer without modifying source arrays. */
|
|
1867
|
+
async *convert(resource, _inspection, signal) {
|
|
1868
|
+
signal?.throwIfAborted();
|
|
1869
|
+
const mesh = validateMeshGeometry(resource.mesh, true);
|
|
1870
|
+
const positions = mesh.attributes.POSITION.value;
|
|
1871
|
+
mesh.attributes.POSITION = {
|
|
1872
|
+
value: options.spatialContext.transformPositions(positions),
|
|
1873
|
+
size: 3
|
|
1874
|
+
};
|
|
1875
|
+
if (mesh.attributes.NORMAL)
|
|
1876
|
+
mesh.attributes.NORMAL = {
|
|
1877
|
+
value: options.spatialContext.transformNormals(mesh.attributes.NORMAL.value, positions),
|
|
1878
|
+
size: 3
|
|
1879
|
+
};
|
|
1880
|
+
let layer;
|
|
1881
|
+
try {
|
|
1882
|
+
layer = encodeI3SMeshLayer(mesh, {
|
|
1883
|
+
...options,
|
|
1884
|
+
material: resource.material,
|
|
1885
|
+
features: resource.features
|
|
1886
|
+
});
|
|
1887
|
+
} catch (error) {
|
|
1888
|
+
throw new TileConversionError12(
|
|
1889
|
+
"I3S_MESH_PROFILE_UNSUPPORTED",
|
|
1890
|
+
error instanceof Error ? error.message : String(error)
|
|
1891
|
+
);
|
|
1892
|
+
}
|
|
1893
|
+
signal?.throwIfAborted();
|
|
1894
|
+
yield { id: resource.id, ...layer };
|
|
1895
|
+
},
|
|
1896
|
+
/** Reports measured precision and explicitly authorized integer representation changes. */
|
|
1897
|
+
async validateOutput(resource) {
|
|
1898
|
+
return [
|
|
1899
|
+
...resource.maximumPositionError > 0 ? [
|
|
1900
|
+
{
|
|
1901
|
+
code: "I3S_MESH_POSITION_ROUNDING",
|
|
1902
|
+
message: `I3S positions have up to ${resource.maximumPositionError} meters reconstruction error`,
|
|
1903
|
+
severity: "info",
|
|
1904
|
+
resourceId: resource.id
|
|
1905
|
+
}
|
|
1906
|
+
] : [],
|
|
1907
|
+
...resource.decimalStringFields.map((field) => ({
|
|
1908
|
+
code: "I3S_INTEGER_DECIMAL_STRING",
|
|
1909
|
+
message: `Explicitly represented 64-bit integer field ${field} as exact decimal text`,
|
|
1910
|
+
severity: "info",
|
|
1911
|
+
resourceId: resource.id
|
|
1912
|
+
}))
|
|
1913
|
+
];
|
|
1914
|
+
}
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
function createSingleMeshI3SSink(options) {
|
|
1918
|
+
const memory = createBoundedMemoryTileConversionSink2(options);
|
|
1919
|
+
let state = "empty";
|
|
1920
|
+
let completed = false;
|
|
1921
|
+
return {
|
|
1922
|
+
/** Exposes resources only after successful finalization. */
|
|
1923
|
+
getFiles: () => completed ? memory.getFiles() : [],
|
|
1924
|
+
/** Writes exactly one layer within the retained-resource budget. */
|
|
1925
|
+
async write(resource, signal) {
|
|
1926
|
+
signal?.throwIfAborted();
|
|
1927
|
+
if (state !== "empty")
|
|
1928
|
+
throw new TileConversionError12(
|
|
1929
|
+
"SINGLE_I3S_MESH_UNAVAILABLE",
|
|
1930
|
+
"The initial I3S sink accepts exactly one mesh layer"
|
|
1931
|
+
);
|
|
1932
|
+
state = "writing";
|
|
1933
|
+
for (const [resourceId, bytes] of Object.entries(resource.files))
|
|
1934
|
+
await memory.write({ resourceId, parts: [bytes], contentType: "application/gzip" }, signal);
|
|
1935
|
+
state = "written";
|
|
1936
|
+
},
|
|
1937
|
+
/** Commits a complete single-layer result. */
|
|
1938
|
+
async finalize(report) {
|
|
1939
|
+
if (state !== "written")
|
|
1940
|
+
throw new TileConversionError12(
|
|
1941
|
+
"SINGLE_I3S_MESH_INCOMPLETE",
|
|
1942
|
+
"Finalization requires exactly one successfully written layer"
|
|
1943
|
+
);
|
|
1944
|
+
await memory.finalize(report);
|
|
1945
|
+
state = "closed";
|
|
1946
|
+
completed = true;
|
|
1947
|
+
},
|
|
1948
|
+
/** Discards all resources after any conversion failure. */
|
|
1949
|
+
async abort(error) {
|
|
1950
|
+
completed = false;
|
|
1951
|
+
state = "closed";
|
|
1952
|
+
await memory.abort(error);
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
export {
|
|
1957
|
+
convertFeatureAttributesToArrowBatches,
|
|
1958
|
+
convertPointCloudSource,
|
|
1959
|
+
createI3SMeshConversionCodec,
|
|
1960
|
+
createMeshConversionCodec,
|
|
1961
|
+
createMeshTilesetConversionSource,
|
|
1962
|
+
createMeshTilesetSink,
|
|
1963
|
+
createSingleMeshI3SSink,
|
|
1964
|
+
createSingleMeshTilesetArchive,
|
|
1965
|
+
createSingleMeshTilesetSink,
|
|
1966
|
+
createTileConversionArchive,
|
|
1967
|
+
encodeMeshTile,
|
|
1968
|
+
encodePointCloudSource,
|
|
1969
|
+
encodePointCloudSourceTile,
|
|
1970
|
+
encodePointCloudTile,
|
|
1971
|
+
encodeTileConversionArchiveInBatches
|
|
1972
|
+
};
|