@loaders.gl/tile-converter 5.0.0-alpha.1 → 5.0.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +14 -3
- package/README.md +497 -0
- package/dist/converter.min.cjs +210 -282
- package/dist/i3s-server/bin/i3s-server.min.cjs +106 -341
- package/dist/index.cjs +38586 -0
- package/dist/index.js +38557 -0
- package/dist/v5/adapters.cjs +3548 -0
- package/dist/v5/adapters.d.ts +33 -0
- package/dist/v5/adapters.js +3537 -0
- package/dist/v5/browser-sink.d.ts +37 -0
- package/dist/v5/browser-source.d.ts +38 -0
- package/dist/v5/browser-tileset-source.d.ts +70 -0
- package/dist/v5/browser.cjs +76 -0
- package/dist/v5/browser.d.ts +44 -0
- package/dist/v5/browser.js +75 -0
- package/dist/v5/conversion-api.d.ts +122 -0
- package/dist/v5/conversion-archive.d.ts +32 -0
- package/dist/v5/core.cjs +917 -0
- package/dist/v5/core.d.ts +16 -0
- package/dist/v5/core.js +899 -0
- package/dist/v5/feature-arrow.d.ts +36 -0
- package/dist/v5/i3s-mesh-conversion.d.ts +32 -0
- package/dist/v5/i3s-mesh-sink.d.ts +25 -0
- package/dist/v5/i3s-mesh-source-features.d.ts +13 -0
- package/dist/v5/i3s-mesh-source-material.d.ts +5 -0
- package/dist/v5/i3s-mesh-source.d.ts +30 -0
- package/dist/v5/index.cjs +73 -0
- package/dist/v5/index.d.ts +40 -0
- package/dist/v5/index.js +75 -0
- package/dist/v5/mesh-conversion.d.ts +71 -0
- package/dist/v5/mesh-draco.d.ts +8 -0
- package/dist/v5/mesh-features.d.ts +9 -0
- package/dist/v5/mesh-source-features.d.ts +21 -0
- package/dist/v5/mesh-source-texture.d.ts +6 -0
- package/dist/v5/mesh-source.d.ts +41 -0
- package/dist/v5/mesh-tileset-archive.d.ts +18 -0
- package/dist/v5/mesh-tileset-sink.d.ts +51 -0
- package/dist/v5/mesh.d.ts +86 -0
- package/dist/v5/point-cloud-conversion.d.ts +31 -0
- package/dist/v5/point-cloud-source-encoder.d.ts +46 -0
- package/dist/v5/point-cloud-source.d.ts +25 -0
- package/dist/v5/point-cloud-spatial.d.ts +22 -0
- package/dist/v5/point-cloud-tileset-sink.d.ts +28 -0
- package/dist/v5/point-cloud.d.ts +49 -0
- package/dist/v5/resource-fetcher.d.ts +18 -0
- package/dist/v5/resource-manifest.d.ts +58 -0
- package/dist/v5/spatial-conversion.d.ts +61 -0
- package/dist/v5/tileset-source.d.ts +21 -0
- package/package.json +52 -26
- package/src/common/README.md +5 -0
- package/src/index.ts +1 -2
- package/src/{3d-tiles-converter → v4/3d-tiles-converter}/3d-tiles-converter.ts +13 -2
- package/src/v4/3d-tiles-converter/helpers/validate-i3s-spatial-reference.ts +124 -0
- package/src/{deps-installer → v4/deps-installer}/deps-installer.ts +10 -7
- package/src/{i3s-converter → v4/i3s-converter}/helpers/batch-ids-extensions.ts +1 -1
- package/src/v4/i3s-converter/helpers/encode-string-attribute.ts +23 -0
- package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-converter.ts +30 -55
- package/src/{i3s-converter → v4/i3s-converter}/helpers/gltf-attributes.ts +5 -4
- package/src/{i3s-converter → v4/i3s-converter}/helpers/load-3d-tiles.ts +5 -9
- package/src/{i3s-converter → v4/i3s-converter}/i3s-converter.ts +6 -7
- package/src/{i3s-server → v4/i3s-server}/README.md +2 -2
- package/src/{i3s-server → v4/i3s-server}/routes/slpk-router.ts +1 -0
- package/src/v4/index.ts +2 -0
- package/src/{lib → v4/lib}/utils/compress-util.ts +8 -7
- package/src/v5/adapters.ts +75 -0
- package/src/v5/browser-sink.ts +130 -0
- package/src/v5/browser-source.ts +155 -0
- package/src/v5/browser-tileset-source.ts +328 -0
- package/src/v5/browser.ts +139 -0
- package/src/v5/conversion-api.ts +287 -0
- package/src/v5/conversion-archive.ts +85 -0
- package/src/v5/core.ts +61 -0
- package/src/v5/feature-arrow.ts +429 -0
- package/src/v5/i3s-mesh-conversion.ts +192 -0
- package/src/v5/i3s-mesh-sink.ts +351 -0
- package/src/v5/i3s-mesh-source-features.ts +279 -0
- package/src/v5/i3s-mesh-source-material.ts +157 -0
- package/src/v5/i3s-mesh-source.ts +227 -0
- package/src/v5/index.ts +131 -0
- package/src/v5/mesh-conversion.ts +230 -0
- package/src/v5/mesh-draco.ts +182 -0
- package/src/v5/mesh-features.ts +319 -0
- package/src/v5/mesh-source-features.ts +263 -0
- package/src/v5/mesh-source-texture.ts +169 -0
- package/src/v5/mesh-source.ts +422 -0
- package/src/v5/mesh-tileset-archive.ts +50 -0
- package/src/v5/mesh-tileset-sink.ts +324 -0
- package/src/v5/mesh.ts +526 -0
- package/src/v5/point-cloud-conversion.ts +88 -0
- package/src/v5/point-cloud-source-encoder.ts +116 -0
- package/src/v5/point-cloud-source.ts +84 -0
- package/src/v5/point-cloud-spatial.ts +280 -0
- package/src/v5/point-cloud-tileset-sink.ts +234 -0
- package/src/v5/point-cloud.ts +398 -0
- package/src/v5/resource-fetcher.ts +110 -0
- package/src/v5/resource-manifest.ts +213 -0
- package/src/v5/spatial-conversion.ts +389 -0
- package/src/v5/tileset-source.ts +54 -0
- /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/3d-tiles-content-converter.ts +0 -0
- /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/i3s-obb-to-3d-tiles-obb.ts +0 -0
- /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/load-i3s.ts +0 -0
- /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/texture-atlas.ts +0 -0
- /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/json-templates/tileset.ts +0 -0
- /package/src/{constants.ts → v4/constants.ts} +0 -0
- /package/src/{converter-cli.ts → v4/converter-cli.ts} +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/attribute-metadata-info.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/coordinate-converter.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/create-scene-server-path.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/feature-attributes.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-attributes.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-debug.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-index-document.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-pages.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/preprocess-3d-tiles.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/progress.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/tileset-traversal.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/geometry-definitions.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/layers.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/metadata.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/node.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/scene-server.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/shared-resources.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/store.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/types.ts +0 -0
- /package/src/{i3s-server → v4/i3s-server}/app.ts +0 -0
- /package/src/{i3s-server → v4/i3s-server}/bin/www.ts +0 -0
- /package/src/{i3s-server → v4/i3s-server}/certs/cert.pem +0 -0
- /package/src/{i3s-server → v4/i3s-server}/certs/key.pem +0 -0
- /package/src/{i3s-server → v4/i3s-server}/controllers/slpk-controller.ts +0 -0
- /package/src/{i3s-server → v4/i3s-server}/utils/create-scene-server.ts +0 -0
- /package/src/{i3s-server → v4/i3s-server}/utils/server-utils.ts +0 -0
- /package/src/{lib → v4/lib}/json-schemas/conversion-dump-json-schema.ts +0 -0
- /package/src/{lib → v4/lib}/utils/cli-utils.ts +0 -0
- /package/src/{lib → v4/lib}/utils/conversion-dump.ts +0 -0
- /package/src/{lib → v4/lib}/utils/file-utils.ts +0 -0
- /package/src/{lib → v4/lib}/utils/geometry-utils.ts +0 -0
- /package/src/{lib → v4/lib}/utils/lod-conversion-utils.ts +0 -0
- /package/src/{lib → v4/lib}/utils/queue.ts +0 -0
- /package/src/{lib → v4/lib}/utils/statistic-utils.ts +0 -0
- /package/src/{lib → v4/lib}/utils/write-queue.ts +0 -0
- /package/src/{pgm-loader.ts → v4/pgm-loader.ts} +0 -0
|
@@ -0,0 +1,3537 @@
|
|
|
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
|
+
return encodePointCloudTileWithMetadata(pointBatch, options).pnts;
|
|
329
|
+
}
|
|
330
|
+
function encodePointCloudTileWithMetadata(pointBatch, options = {}) {
|
|
331
|
+
const mesh = "attributes" in pointBatch ? pointBatch : convertTableToMesh(pointBatch);
|
|
332
|
+
validatePointAttributes(mesh.attributes);
|
|
333
|
+
const positionAttribute = mesh.attributes.POSITION;
|
|
334
|
+
validatePointCloudPosition(positionAttribute);
|
|
335
|
+
const placement = encodePointPositions(positionAttribute, options);
|
|
336
|
+
const { positions } = placement;
|
|
337
|
+
const pointCount = positions.length / 3;
|
|
338
|
+
const colors = getColorAttribute(mesh.attributes.COLOR_0 || mesh.attributes.COLOR, pointCount);
|
|
339
|
+
const constantRGBA = colors ? null : getConstantRgba(options.constantRGBA);
|
|
340
|
+
const normals = getNormalAttribute(mesh.attributes.NORMAL, pointCount);
|
|
341
|
+
const batchIds = getBatchIds(mesh.attributes.BATCH_ID, pointCount);
|
|
342
|
+
const batchTableJson = validateBatchTable(options.batchTableJson, batchIds);
|
|
343
|
+
const pnts = Tile3DWriter.encodeSync(
|
|
344
|
+
{
|
|
345
|
+
type: TILE3D_TYPE.POINT_CLOUD,
|
|
346
|
+
featureTableJson: options.rtcCenter || constantRGBA ? {
|
|
347
|
+
...options.rtcCenter ? { RTC_CENTER: [...options.rtcCenter] } : {},
|
|
348
|
+
...constantRGBA ? { CONSTANT_RGBA: constantRGBA } : {}
|
|
349
|
+
} : void 0,
|
|
350
|
+
batchTableJson,
|
|
351
|
+
attributes: { positions, colors, normals, batchIds }
|
|
352
|
+
},
|
|
353
|
+
{}
|
|
354
|
+
);
|
|
355
|
+
return {
|
|
356
|
+
pnts,
|
|
357
|
+
pointCount,
|
|
358
|
+
rtcCenter: placement.rtcCenter,
|
|
359
|
+
localBoundingBox: placement.localBoundingBox,
|
|
360
|
+
maximumPositionError: placement.maximumPositionError
|
|
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 validatePointCloudPosition(attribute) {
|
|
393
|
+
if (!attribute || attribute.size !== 3 || attribute.byteOffset || attribute.byteStride || attribute.normalized || attribute.componentType) {
|
|
394
|
+
throw new TileConversionError2(
|
|
395
|
+
"POINT_CLOUD_POSITION_REQUIRED",
|
|
396
|
+
"Point cloud batches must contain packed, non-normalized three-component POSITION values"
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
if (!attribute.value.length || attribute.value.length % 3) {
|
|
400
|
+
throw new TileConversionError2(
|
|
401
|
+
"POINT_CLOUD_POSITION_COUNT_INVALID",
|
|
402
|
+
"Point cloud POSITION must contain one or more xyz triples"
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const transform = attribute.transform;
|
|
406
|
+
if (transform && (transform.type !== "quantization" || !Number.isInteger(transform.bits) || transform.bits < 1 || transform.bits > 32 || transform.origin.length !== 3 || transform.origin.some((value) => !Number.isFinite(value)) || !Number.isFinite(transform.range) || transform.range < 0)) {
|
|
407
|
+
throw new TileConversionError2(
|
|
408
|
+
"POINT_CLOUD_POSITION_TRANSFORM_INVALID",
|
|
409
|
+
"Position quantization requires finite xyz origin, nonnegative range and 1\u201332 bits"
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function decodePointCloudPositions(attribute) {
|
|
414
|
+
validatePointCloudPosition(attribute);
|
|
415
|
+
const transform = attribute.transform;
|
|
416
|
+
return Float64Array.from(
|
|
417
|
+
attribute.value,
|
|
418
|
+
(value, index) => transform ? transform.origin[index % 3] + value / (2 ** transform.bits - 1) * transform.range : value
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
function encodePointPositions(attribute, options) {
|
|
422
|
+
const { rtcCenter = [0, 0, 0], maxPositionError = Infinity } = options;
|
|
423
|
+
if (rtcCenter.length !== 3 || rtcCenter.some((value) => !Number.isFinite(value))) {
|
|
424
|
+
throw new TileConversionError2(
|
|
425
|
+
"POINT_CLOUD_RTC_CENTER_INVALID",
|
|
426
|
+
"Point cloud RTC_CENTER must contain three finite values"
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
if (options.maxPositionError !== void 0 && (!Number.isFinite(maxPositionError) || maxPositionError < 0)) {
|
|
430
|
+
throw new TileConversionError2(
|
|
431
|
+
"POINT_CLOUD_PRECISION_INVALID",
|
|
432
|
+
"maxPositionError must be finite and nonnegative"
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const values = attribute.value;
|
|
436
|
+
const positions = new Float32Array(values.length);
|
|
437
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
438
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
439
|
+
let maximumPositionError = 0;
|
|
440
|
+
const transform = attribute.transform;
|
|
441
|
+
const errors = [0, 0, 0];
|
|
442
|
+
for (let point = 0; point < values.length; point += 3) {
|
|
443
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
444
|
+
const value = values[point + axis];
|
|
445
|
+
const absoluteValue = transform ? transform.origin[axis] + value / (2 ** transform.bits - 1) * transform.range : value;
|
|
446
|
+
const relativeValue = absoluteValue - rtcCenter[axis];
|
|
447
|
+
positions[point + axis] = relativeValue;
|
|
448
|
+
const encodedValue = positions[point + axis];
|
|
449
|
+
const reconstructed = encodedValue + rtcCenter[axis];
|
|
450
|
+
if (!Number.isFinite(absoluteValue) || !Number.isFinite(relativeValue) || !Number.isFinite(encodedValue) || !Number.isFinite(reconstructed)) {
|
|
451
|
+
throw new TileConversionError2(
|
|
452
|
+
"POINT_CLOUD_POSITION_INVALID",
|
|
453
|
+
"Point positions and their float32 reconstruction must be finite"
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
errors[axis] = reconstructed - absoluteValue;
|
|
457
|
+
minimum[axis] = Math.min(minimum[axis], encodedValue);
|
|
458
|
+
maximum[axis] = Math.max(maximum[axis], encodedValue);
|
|
459
|
+
}
|
|
460
|
+
maximumPositionError = Math.max(maximumPositionError, Math.hypot(...errors));
|
|
461
|
+
}
|
|
462
|
+
if (!Number.isFinite(maximumPositionError) || maximumPositionError > maxPositionError) {
|
|
463
|
+
throw new TileConversionError2(
|
|
464
|
+
"POINT_CLOUD_PRECISION_EXCEEDED",
|
|
465
|
+
"Float32 position reconstruction exceeds maxPositionError; select a closer RTC center"
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
positions,
|
|
470
|
+
rtcCenter: [...rtcCenter],
|
|
471
|
+
localBoundingBox: [minimum, maximum],
|
|
472
|
+
maximumPositionError
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function getColorAttribute(attribute, pointCount) {
|
|
476
|
+
if (!attribute) {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
if (attribute.size !== 3 && attribute.size !== 4) {
|
|
480
|
+
throw new TileConversionError2(
|
|
481
|
+
"POINT_CLOUD_COLOR_SIZE_UNSUPPORTED",
|
|
482
|
+
"Point cloud color attributes must contain three or four components"
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
if (attribute.value.length !== pointCount * attribute.size) {
|
|
486
|
+
throw new TileConversionError2(
|
|
487
|
+
"POINT_CLOUD_COLOR_COUNT_MISMATCH",
|
|
488
|
+
"Point cloud color count does not match the POSITION count"
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
const colors = new Uint8Array(attribute.value.length);
|
|
492
|
+
const maximum = getIntegerColorMaximum(attribute.value);
|
|
493
|
+
for (let index = 0; index < attribute.value.length; index++) {
|
|
494
|
+
const value = attribute.value[index];
|
|
495
|
+
const scaledValue = maximum ? value / maximum * 255 : value <= 1 ? value * 255 : value;
|
|
496
|
+
if (!Number.isFinite(scaledValue) || scaledValue < 0 || scaledValue > 255) {
|
|
497
|
+
throw new TileConversionError2(
|
|
498
|
+
"POINT_CLOUD_COLOR_VALUE_INVALID",
|
|
499
|
+
"Point cloud color components must be finite values from 0 through 255"
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
colors[index] = Math.round(scaledValue);
|
|
503
|
+
}
|
|
504
|
+
return { type: 5121, value: colors, size: attribute.size, normalized: false };
|
|
505
|
+
}
|
|
506
|
+
function getIntegerColorMaximum(values) {
|
|
507
|
+
if (values instanceof Uint8Array || values instanceof Uint8ClampedArray) {
|
|
508
|
+
return 255;
|
|
509
|
+
}
|
|
510
|
+
if (values instanceof Uint16Array) {
|
|
511
|
+
return 65535;
|
|
512
|
+
}
|
|
513
|
+
if (values instanceof Uint32Array) {
|
|
514
|
+
return 4294967295;
|
|
515
|
+
}
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
function getNormalAttribute(attribute, pointCount) {
|
|
519
|
+
if (!attribute) {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
if (attribute.size !== 3 || attribute.value.length !== pointCount * 3) {
|
|
523
|
+
throw new TileConversionError2(
|
|
524
|
+
"POINT_CLOUD_NORMAL_COUNT_MISMATCH",
|
|
525
|
+
"Point cloud normals must contain one xyz triple per position"
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
return { type: 5126, value: Float32Array.from(attribute.value), size: 3 };
|
|
529
|
+
}
|
|
530
|
+
function getBatchIds(attribute, pointCount) {
|
|
531
|
+
if (!attribute) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
if (attribute.size !== 1 || attribute.value.length !== pointCount) {
|
|
535
|
+
throw new TileConversionError2(
|
|
536
|
+
"POINT_CLOUD_BATCH_ID_COUNT_MISMATCH",
|
|
537
|
+
"Point cloud BATCH_ID must contain one value per position"
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
const batchIds = new Uint16Array(pointCount);
|
|
541
|
+
for (let index = 0; index < pointCount; index++) {
|
|
542
|
+
const batchId = attribute.value[index];
|
|
543
|
+
if (!Number.isInteger(batchId) || batchId < 0 || batchId > 65535) {
|
|
544
|
+
throw new TileConversionError2(
|
|
545
|
+
"POINT_CLOUD_BATCH_ID_OUT_OF_RANGE",
|
|
546
|
+
"Point cloud BATCH_ID values must be unsigned 16-bit integers"
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
batchIds[index] = batchId;
|
|
550
|
+
}
|
|
551
|
+
return batchIds;
|
|
552
|
+
}
|
|
553
|
+
function validateBatchTable(batchTableJson, batchIds) {
|
|
554
|
+
if (!batchTableJson) {
|
|
555
|
+
if (batchIds) {
|
|
556
|
+
throw new TileConversionError2(
|
|
557
|
+
"POINT_CLOUD_BATCH_TABLE_REQUIRED",
|
|
558
|
+
"BATCH_ID values require matching batchTableJson properties"
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
return void 0;
|
|
562
|
+
}
|
|
563
|
+
if (!batchIds) {
|
|
564
|
+
throw new TileConversionError2(
|
|
565
|
+
"POINT_CLOUD_BATCH_ID_REQUIRED",
|
|
566
|
+
"batchTableJson requires a BATCH_ID attribute"
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
let batchLength = 0;
|
|
570
|
+
for (const batchId of batchIds) {
|
|
571
|
+
batchLength = Math.max(batchLength, batchId + 1);
|
|
572
|
+
}
|
|
573
|
+
const normalizedBatchTable = {};
|
|
574
|
+
for (const [name, values] of Object.entries(batchTableJson)) {
|
|
575
|
+
if (values.length !== batchLength) {
|
|
576
|
+
throw new TileConversionError2(
|
|
577
|
+
"POINT_CLOUD_BATCH_TABLE_LENGTH_MISMATCH",
|
|
578
|
+
`Batch table property "${name}" must contain ${batchLength} values`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
normalizedBatchTable[name] = [...values];
|
|
582
|
+
}
|
|
583
|
+
return normalizedBatchTable;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/v5/point-cloud-spatial.ts
|
|
587
|
+
import { Matrix4 } from "@math.gl/core";
|
|
588
|
+
import { getSpatialCoordinateFrame, createTilesetSpatialReference } from "@loaders.gl/tiles";
|
|
589
|
+
import { convertTableToMesh as convertTableToMesh2, makeMeshArrowTable } from "@loaders.gl/schema-utils";
|
|
590
|
+
import { TileConversionError as TileConversionError3 } from "@loaders.gl/tile-converter/v5/core";
|
|
591
|
+
async function transformPointCloudSourceTile(sourceTile, options) {
|
|
592
|
+
options.signal?.throwIfAborted();
|
|
593
|
+
const { content } = sourceTile;
|
|
594
|
+
if (!content) return sourceTile;
|
|
595
|
+
const reference = options.spatialContext.spatialReference;
|
|
596
|
+
const contentReference = content.spatialReference;
|
|
597
|
+
const retainTransformedEcef = contentReference?.status === "transformed" && (contentReference.targetCrs || contentReference.sourceCrs) === "EPSG:4978" && reference.status === "native" && reference.sourceCrs === "EPSG:4978" && reference.verticalUnitScale === 1 && (reference.units?.every((unit) => unit === "meter") ?? true);
|
|
598
|
+
const sourceReference = retainTransformedEcef ? createTilesetSpatialReference({
|
|
599
|
+
sourceCrs: "EPSG:4978",
|
|
600
|
+
coordinateFrame: "geocentric",
|
|
601
|
+
axisOrder: "xyz",
|
|
602
|
+
units: reference.units,
|
|
603
|
+
coordinateEpoch: contentReference.coordinateEpoch,
|
|
604
|
+
heightReference: contentReference.targetHeightReference === "native" ? contentReference.heightReference : contentReference.targetHeightReference
|
|
605
|
+
}) : contentReference;
|
|
606
|
+
if (sourceReference && (sourceReference.status === "transformed" || sourceReference.status === "unresolved" || JSON.stringify(sourceReference.sourceCrs) !== JSON.stringify(reference.sourceCrs) || sourceReference.heightReference !== reference.heightReference || sourceReference.verticalUnitScale !== reference.verticalUnitScale || sourceReference.coordinateEpoch !== reference.coordinateEpoch || JSON.stringify(sourceReference.units) !== JSON.stringify(reference.units) || sourceReference.elevationMode !== reference.elevationMode || sourceReference.elevationOffset !== reference.elevationOffset || sourceReference.elevationUnitScale !== reference.elevationUnitScale)) {
|
|
607
|
+
throw new TileConversionError3(
|
|
608
|
+
"POINT_CLOUD_SPATIAL_REFERENCE_MISMATCH",
|
|
609
|
+
"Point content and context must agree on the input CRS, units, height reference and epoch"
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
const sourceFrame = reference.coordinateFrame === "unknown" && reference.sourceCrs ? getSpatialCoordinateFrame(reference.sourceCrs) : reference.coordinateFrame;
|
|
613
|
+
const geographic = sourceFrame === "geographic";
|
|
614
|
+
if (!["xy", "xyz", "unknown"].includes(reference.axisOrder) || (geographic ? !["lnglat", "lnglat-offsets"].includes(content.coordinateSystem) : content.coordinateSystem !== "cartesian" || !["projected", "geocentric", "cartesian", "local"].includes(sourceFrame))) {
|
|
615
|
+
throw new TileConversionError3(
|
|
616
|
+
"POINT_CLOUD_SOURCE_FRAME_UNSUPPORTED",
|
|
617
|
+
"Spatial preparation requires explicit Cartesian or longitude/latitude positions in source xyz order"
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (content.cartographicOrigin.length !== 3 || content.cartographicOrigin.some((value) => !Number.isFinite(value)) || content.coordinateSystem !== "lnglat-offsets" && content.cartographicOrigin.some((value) => value !== 0)) {
|
|
621
|
+
throw new TileConversionError3(
|
|
622
|
+
"POINT_CLOUD_SOURCE_ORIGIN_INVALID",
|
|
623
|
+
"Only longitude/latitude offsets use a nonzero cartographic origin"
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
if (content.modelMatrix && (content.modelMatrix.length !== 16 || Array.from(content.modelMatrix).some((value) => !Number.isFinite(value)))) {
|
|
627
|
+
throw new TileConversionError3(
|
|
628
|
+
"POINT_CLOUD_SOURCE_PLACEMENT_UNSUPPORTED",
|
|
629
|
+
"Point placement must contain sixteen finite matrix values"
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
const matrix = new Matrix4(content.modelMatrix ? Array.from(content.modelMatrix) : void 0);
|
|
633
|
+
const determinant = matrix.determinant();
|
|
634
|
+
if (matrix[3] !== 0 || matrix[7] !== 0 || matrix[11] !== 0 || matrix[15] !== 1 || !Number.isFinite(determinant) || determinant === 0 || geographic && !matrix.equals(new Matrix4())) {
|
|
635
|
+
throw new TileConversionError3(
|
|
636
|
+
"POINT_CLOUD_SOURCE_PLACEMENT_UNSUPPORTED",
|
|
637
|
+
"Cartesian placement must be finite, affine and invertible; geographic placement must be identity"
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
const mesh = convertTableToMesh2(content.data);
|
|
641
|
+
const positionAttribute = mesh.attributes.POSITION;
|
|
642
|
+
const declaredTransform = mesh.schema?.fields.find((field) => field.name === "POSITION")?.metadata?.["loaders.gl.transform"];
|
|
643
|
+
if (declaredTransform && !positionAttribute?.transform) {
|
|
644
|
+
throw new TileConversionError3(
|
|
645
|
+
"POINT_CLOUD_POSITION_TRANSFORM_INVALID",
|
|
646
|
+
"Arrow position transform metadata is unsupported or invalid"
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
const sourcePositions = decodePointCloudPositions(positionAttribute);
|
|
650
|
+
if (sourcePositions.some((value) => !Number.isFinite(value))) {
|
|
651
|
+
throw new TileConversionError3(
|
|
652
|
+
"POINT_CLOUD_POSITION_INVALID",
|
|
653
|
+
"Decoded point positions must be finite"
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
const translationOnly = Array.from(matrix).every(
|
|
657
|
+
(value, index) => index >= 12 && index <= 14 || value === (index % 5 === 0 ? 1 : 0)
|
|
658
|
+
);
|
|
659
|
+
for (let index = 0; index < sourcePositions.length; index += 3) {
|
|
660
|
+
if (geographic || translationOnly) {
|
|
661
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
662
|
+
sourcePositions[index + axis] += geographic ? content.cartographicOrigin[axis] : matrix[12 + axis];
|
|
663
|
+
}
|
|
664
|
+
} else {
|
|
665
|
+
sourcePositions.set(
|
|
666
|
+
matrix.transformAsPoint(Array.from(sourcePositions.subarray(index, index + 3))),
|
|
667
|
+
index
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const normalAttribute = mesh.attributes.NORMAL;
|
|
672
|
+
let normals;
|
|
673
|
+
if (normalAttribute) {
|
|
674
|
+
if (!options.normalReferenceFrame || normalAttribute.size !== 3 || normalAttribute.value.length !== sourcePositions.length || normalAttribute.transform || normalAttribute.normalized || normalAttribute.byteOffset || normalAttribute.byteStride || normalAttribute.componentType || !translationOnly) {
|
|
675
|
+
throw new TileConversionError3(
|
|
676
|
+
"POINT_CLOUD_NORMAL_FRAME_REQUIRED",
|
|
677
|
+
"Spatial point normals require a declared vector frame and packed xyz values"
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
normals = new Float32Array(normalAttribute.value.length);
|
|
681
|
+
for (let index = 0; index < normals.length; index += 3) {
|
|
682
|
+
const normal = Array.from(normalAttribute.value.slice(index, index + 3));
|
|
683
|
+
const length = Math.hypot(...normal);
|
|
684
|
+
if (!Number.isFinite(length) || length === 0) {
|
|
685
|
+
throw new TileConversionError3(
|
|
686
|
+
"POINT_CLOUD_NORMAL_INVALID",
|
|
687
|
+
"Spatial point normals must be finite and nonzero"
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
normals.set(
|
|
691
|
+
normal.map((value) => value / length),
|
|
692
|
+
index
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const transformed = await options.spatialContext.transformGeometryAsync(
|
|
697
|
+
sourcePositions,
|
|
698
|
+
normals,
|
|
699
|
+
options.normalReferenceFrame
|
|
700
|
+
);
|
|
701
|
+
options.signal?.throwIfAborted();
|
|
702
|
+
if (transformed.positions.length !== sourcePositions.length) {
|
|
703
|
+
throw new TileConversionError3(
|
|
704
|
+
"POINT_CLOUD_POSITION_INVALID",
|
|
705
|
+
"Spatial conversion must return one xyz triple per point"
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
709
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
710
|
+
for (let index = 0; index < transformed.positions.length; index++) {
|
|
711
|
+
const value = transformed.positions[index];
|
|
712
|
+
if (!Number.isFinite(value)) {
|
|
713
|
+
throw new TileConversionError3(
|
|
714
|
+
"POINT_CLOUD_POSITION_INVALID",
|
|
715
|
+
"Transformed point positions must be finite"
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
const axis = index % 3;
|
|
719
|
+
minimum[axis] = Math.min(minimum[axis], value);
|
|
720
|
+
maximum[axis] = Math.max(maximum[axis], value);
|
|
721
|
+
}
|
|
722
|
+
const center = minimum.map((value, axis) => value / 2 + maximum[axis] / 2);
|
|
723
|
+
const targetFrame = reference.status === "native" ? sourceFrame : getSpatialCoordinateFrame(reference.targetCrs || reference.sourceCrs);
|
|
724
|
+
const targetIsGeographic = targetFrame === "geographic";
|
|
725
|
+
const attributes = {
|
|
726
|
+
...mesh.attributes,
|
|
727
|
+
POSITION: { value: transformed.positions, size: 3 },
|
|
728
|
+
...transformed.normals ? { NORMAL: { value: transformed.normals, size: 3 } } : {}
|
|
729
|
+
};
|
|
730
|
+
return {
|
|
731
|
+
header: sourceTile.header,
|
|
732
|
+
content: {
|
|
733
|
+
...content,
|
|
734
|
+
data: makeMeshArrowTable(attributes, {
|
|
735
|
+
topology: "point-list",
|
|
736
|
+
boundingBox: [minimum, maximum],
|
|
737
|
+
schema: mesh.schema ? {
|
|
738
|
+
...mesh.schema,
|
|
739
|
+
metadata: {
|
|
740
|
+
...mesh.schema.metadata,
|
|
741
|
+
topology: "point-list",
|
|
742
|
+
boundingBox: JSON.stringify([minimum, maximum])
|
|
743
|
+
},
|
|
744
|
+
fields: mesh.schema.fields.map(
|
|
745
|
+
(field) => field.name === "POSITION" || transformed.normals && field.name === "NORMAL" ? { ...field, metadata: {} } : field
|
|
746
|
+
)
|
|
747
|
+
} : void 0
|
|
748
|
+
}),
|
|
749
|
+
coordinateSystem: targetIsGeographic ? "lnglat" : "cartesian",
|
|
750
|
+
cartographicOrigin: [0, 0, 0],
|
|
751
|
+
modelMatrix: void 0,
|
|
752
|
+
spatialReference: reference,
|
|
753
|
+
spatialBoundingVolume: {
|
|
754
|
+
cartographicBounds: [minimum, maximum],
|
|
755
|
+
coordinateFrame: targetIsGeographic ? "geographic" : "cartesian",
|
|
756
|
+
center,
|
|
757
|
+
radius: Math.hypot(...maximum.map((value, axis) => value - center[axis]))
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// src/v5/point-cloud-source-encoder.ts
|
|
764
|
+
import { traversePointCloudSource, TileConversionError as TileConversionError4 } from "@loaders.gl/tile-converter/v5/core";
|
|
765
|
+
async function* encodePointCloudSource(source, options = {}) {
|
|
766
|
+
for await (const inputTile of traversePointCloudSource(source, options)) {
|
|
767
|
+
const sourceTile = options.spatialContext ? await transformPointCloudSourceTile(inputTile, {
|
|
768
|
+
...options,
|
|
769
|
+
spatialContext: options.spatialContext
|
|
770
|
+
}) : inputTile;
|
|
771
|
+
const encodingOptions = sourceTile.content ? options.getTileEncodingOptions?.(sourceTile) : void 0;
|
|
772
|
+
const encodedTile = encodePointCloudSourceTile(sourceTile, encodingOptions);
|
|
773
|
+
if (encodedTile) {
|
|
774
|
+
yield encodedTile;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function encodePointCloudSourceTile(sourceTile, options = {}) {
|
|
779
|
+
const { header, content } = sourceTile;
|
|
780
|
+
if (!content) {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
const encoded = encodePointCloudTileWithMetadata(content.data, {
|
|
784
|
+
...options,
|
|
785
|
+
constantRGBA: options.constantRGBA ?? content.constantRGBA
|
|
786
|
+
});
|
|
787
|
+
if (content.pointCount !== encoded.pointCount) {
|
|
788
|
+
throw new TileConversionError4(
|
|
789
|
+
"POINT_CLOUD_SOURCE_COUNT_INVALID",
|
|
790
|
+
"Source pointCount must match the encoded Arrow point rows"
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
return {
|
|
794
|
+
id: header.id,
|
|
795
|
+
header,
|
|
796
|
+
...encoded,
|
|
797
|
+
coordinateSystem: content.coordinateSystem,
|
|
798
|
+
cartographicOrigin: [...content.cartographicOrigin],
|
|
799
|
+
modelMatrix: content.modelMatrix ? Array.from(content.modelMatrix) : void 0,
|
|
800
|
+
spatialReference: content.spatialReference,
|
|
801
|
+
spatialBoundingVolume: content.spatialBoundingVolume || header.spatialBoundingVolume
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/v5/point-cloud-conversion.ts
|
|
806
|
+
import { convertTileset } from "@loaders.gl/tile-converter/v5/core";
|
|
807
|
+
import { traversePointCloudSource as traversePointCloudSource2 } from "@loaders.gl/tile-converter/v5/core";
|
|
808
|
+
function convertPointCloudSource(source, options) {
|
|
809
|
+
return convertTileset({
|
|
810
|
+
source: {
|
|
811
|
+
async inspect() {
|
|
812
|
+
if (!source.isReady) {
|
|
813
|
+
await source.initialize();
|
|
814
|
+
}
|
|
815
|
+
},
|
|
816
|
+
read: (_inspection, signal) => traversePointCloudSource2(source, { maxDepth: options.maxDepth, signal })
|
|
817
|
+
},
|
|
818
|
+
codec: {
|
|
819
|
+
async *convert(inputTile, _inspection, signal) {
|
|
820
|
+
const sourceTile = options.spatialContext ? await transformPointCloudSourceTile(inputTile, {
|
|
821
|
+
...options,
|
|
822
|
+
spatialContext: options.spatialContext,
|
|
823
|
+
signal
|
|
824
|
+
}) : inputTile;
|
|
825
|
+
const encodingOptions = sourceTile.content ? options.getTileEncodingOptions?.(sourceTile) : void 0;
|
|
826
|
+
const encodedTile = encodePointCloudSourceTile(sourceTile, encodingOptions);
|
|
827
|
+
if (encodedTile) {
|
|
828
|
+
yield encodedTile;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
},
|
|
832
|
+
sink: options.sink,
|
|
833
|
+
measureInputBytes: options.measureInputBytes,
|
|
834
|
+
measureOutputBytes: (resource) => resource.pnts.byteLength,
|
|
835
|
+
maxInputResourceBytes: options.maxInputResourceBytes,
|
|
836
|
+
maxOutputResourceBytes: options.maxOutputResourceBytes,
|
|
837
|
+
signal: options.signal,
|
|
838
|
+
onProgress: options.onProgress
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/v5/mesh.ts
|
|
843
|
+
import { GLTFScenegraph, GLTFWriter } from "@loaders.gl/gltf";
|
|
844
|
+
import { getBinaryImageMetadata } from "@loaders.gl/images";
|
|
845
|
+
|
|
846
|
+
// src/v5/mesh-features.ts
|
|
847
|
+
import { convertArrowToSchema } from "@loaders.gl/schema-utils";
|
|
848
|
+
import { TileConversionError as TileConversionError5 } from "@loaders.gl/tile-converter/v5/core";
|
|
849
|
+
function prepareMeshFeatureGeometry(mesh, features) {
|
|
850
|
+
if (!features) return mesh;
|
|
851
|
+
const count = features.batches.reduce((total, batch) => total + batch.data.numRows, 0);
|
|
852
|
+
const indices = mesh.indices?.value;
|
|
853
|
+
const vertexCount = indices?.length ?? mesh.attributes.POSITION.value.length / 3;
|
|
854
|
+
const associations = features.triangleFeatureIndices;
|
|
855
|
+
if (!(associations instanceof Uint32Array) || associations.length !== vertexCount / 3 || associations.some((row) => row >= count) || new Set(associations).size !== count) {
|
|
856
|
+
throw new TileConversionError5(
|
|
857
|
+
"MESH_FEATURE_ASSOCIATION_INVALID",
|
|
858
|
+
"Every feature row must own a triangle and every triangle must reference a valid row"
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
if (count > 2 ** 24)
|
|
862
|
+
throw new TileConversionError5(
|
|
863
|
+
"MESH_FEATURE_COUNT_UNSUPPORTED",
|
|
864
|
+
"Feature row indices must be exactly representable in a glTF vertex attribute"
|
|
865
|
+
);
|
|
866
|
+
const attributes = Object.fromEntries(
|
|
867
|
+
Object.entries(mesh.attributes).map(([name, attribute]) => {
|
|
868
|
+
const values = attribute.value;
|
|
869
|
+
const Constructor = values.constructor;
|
|
870
|
+
const output = new Constructor(vertexCount * attribute.size);
|
|
871
|
+
for (let vertex = 0; vertex < vertexCount; vertex++) {
|
|
872
|
+
const sourceVertex = indices?.[vertex] ?? vertex;
|
|
873
|
+
for (let component = 0; component < attribute.size; component++)
|
|
874
|
+
output[vertex * attribute.size + component] = Number(
|
|
875
|
+
values[sourceVertex * attribute.size + component]
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
return [name, { ...attribute, value: output }];
|
|
879
|
+
})
|
|
880
|
+
);
|
|
881
|
+
const featureIndices = count <= 65536 ? new Uint16Array(vertexCount) : new Float32Array(vertexCount);
|
|
882
|
+
for (let vertex = 0; vertex < vertexCount; vertex++)
|
|
883
|
+
featureIndices[vertex] = associations[Math.floor(vertex / 3)];
|
|
884
|
+
attributes._FEATURE_ID_0 = { value: featureIndices, size: 1 };
|
|
885
|
+
return { ...mesh, indices: void 0, attributes };
|
|
886
|
+
}
|
|
887
|
+
function encodeMeshFeatures(scenegraph, meshIndex, features) {
|
|
888
|
+
if (!features) return;
|
|
889
|
+
const { batches, featureIdField } = features;
|
|
890
|
+
const schema = batches[0]?.schema;
|
|
891
|
+
const count = batches.reduce((total, batch) => total + batch.data.numRows, 0);
|
|
892
|
+
const metadataClass = schema?.metadata?.["loaders.gl:feature-class"] || "features";
|
|
893
|
+
if (!schema || !count || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(metadataClass) || !schema.fields.some((field) => field.name === featureIdField && field.nullable !== true) || new Set(schema.fields.map((field) => field.name)).size !== schema.fields.length) {
|
|
894
|
+
throw new TileConversionError5(
|
|
895
|
+
"MESH_FEATURE_SCHEMA_INVALID",
|
|
896
|
+
"Features require a single named class, unique fields, and a non-null stable identifier"
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
for (const batch of batches) {
|
|
900
|
+
const actual = convertArrowToSchema(batch.data.schema).fields;
|
|
901
|
+
if (JSON.stringify(batch.schema) !== JSON.stringify(schema) || batch.length !== batch.data.numRows || JSON.stringify(actual.map((field) => [field.name, field.type])) !== JSON.stringify(schema.fields.map((field) => [field.name, field.type]))) {
|
|
902
|
+
throw new TileConversionError5(
|
|
903
|
+
"MESH_FEATURE_SCHEMA_INVALID",
|
|
904
|
+
"Declared feature schemas and Arrow column types must agree across batches"
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
const properties = /* @__PURE__ */ Object.create(null);
|
|
909
|
+
const columns = /* @__PURE__ */ Object.create(null);
|
|
910
|
+
for (const field of schema.fields) {
|
|
911
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(field.name))
|
|
912
|
+
throw new TileConversionError5(
|
|
913
|
+
"MESH_FEATURE_SCHEMA_INVALID",
|
|
914
|
+
"Property names must be metadata identifiers"
|
|
915
|
+
);
|
|
916
|
+
const values = batches.flatMap((batch) => {
|
|
917
|
+
const column = batch.data.getChild(field.name);
|
|
918
|
+
if (!column)
|
|
919
|
+
throw new TileConversionError5(
|
|
920
|
+
"MESH_FEATURE_SCHEMA_INVALID",
|
|
921
|
+
`Missing Arrow column ${field.name}`
|
|
922
|
+
);
|
|
923
|
+
return Array.from({ length: column.length }, (_, index) => column.get(index));
|
|
924
|
+
});
|
|
925
|
+
if (field.name === featureIdField && (values.some(
|
|
926
|
+
(value) => !(typeof value === "string" || typeof value === "bigint" || Number.isSafeInteger(value))
|
|
927
|
+
) || new Set(values).size !== count))
|
|
928
|
+
throw new TileConversionError5(
|
|
929
|
+
"MESH_FEATURE_IDS_INVALID",
|
|
930
|
+
"Stable identifiers must be unique strings or exact integers"
|
|
931
|
+
);
|
|
932
|
+
const encoded = encodeFeatureColumn(field, values);
|
|
933
|
+
properties[field.name] = encoded.property;
|
|
934
|
+
columns[field.name] = {
|
|
935
|
+
values: addMetadataBuffer(scenegraph, encoded.data),
|
|
936
|
+
...encoded.offsets ? {
|
|
937
|
+
stringOffsets: addMetadataBuffer(scenegraph, encoded.offsets),
|
|
938
|
+
stringOffsetType: "UINT32"
|
|
939
|
+
} : {}
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
scenegraph.json.extensions ||= {};
|
|
943
|
+
scenegraph.json.extensions.EXT_structural_metadata = {
|
|
944
|
+
schema: { id: "loaders_gl_features", classes: { [metadataClass]: { properties } } },
|
|
945
|
+
propertyTables: [{ class: metadataClass, count, properties: columns }]
|
|
946
|
+
};
|
|
947
|
+
const primitive = scenegraph.json.meshes[meshIndex].primitives[0];
|
|
948
|
+
primitive.extensions ||= {};
|
|
949
|
+
primitive.extensions.EXT_mesh_features = {
|
|
950
|
+
featureIds: [{ featureCount: count, attribute: 0, propertyTable: 0 }]
|
|
951
|
+
};
|
|
952
|
+
scenegraph.registerUsedExtension("EXT_structural_metadata");
|
|
953
|
+
scenegraph.registerUsedExtension("EXT_mesh_features");
|
|
954
|
+
}
|
|
955
|
+
function addMetadataBuffer(scenegraph, data) {
|
|
956
|
+
if (scenegraph.byteLength % 8) scenegraph.addBufferView(new Uint8Array(4));
|
|
957
|
+
return scenegraph.addBufferView(data);
|
|
958
|
+
}
|
|
959
|
+
function encodeFeatureColumn(field, values) {
|
|
960
|
+
const type = field.type;
|
|
961
|
+
const hasNull = values.includes(null);
|
|
962
|
+
if (hasNull && field.nullable !== true)
|
|
963
|
+
throw new TileConversionError5(
|
|
964
|
+
"MESH_FEATURE_VALUE_INVALID",
|
|
965
|
+
`Required field ${field.name} contains null`
|
|
966
|
+
);
|
|
967
|
+
const present = values.filter((value) => value !== null);
|
|
968
|
+
let noData;
|
|
969
|
+
if (type === "utf8") {
|
|
970
|
+
if (present.some(
|
|
971
|
+
(value) => typeof value !== "string" || new TextDecoder().decode(new TextEncoder().encode(value)) !== value
|
|
972
|
+
))
|
|
973
|
+
throw new TileConversionError5("MESH_FEATURE_VALUE_INVALID", "Strings require valid Unicode");
|
|
974
|
+
if (hasNull) {
|
|
975
|
+
noData = "";
|
|
976
|
+
const strings = new Set(present);
|
|
977
|
+
while (strings.has(noData)) noData += "_";
|
|
978
|
+
}
|
|
979
|
+
const encoded = values.map(
|
|
980
|
+
(value) => new TextEncoder().encode(value === null ? noData : value)
|
|
981
|
+
);
|
|
982
|
+
const offsets = new Uint32Array(values.length + 1);
|
|
983
|
+
for (let index = 0; index < values.length; index++)
|
|
984
|
+
offsets[index + 1] = offsets[index] + encoded[index].length;
|
|
985
|
+
const data2 = new Uint8Array(offsets[values.length]);
|
|
986
|
+
encoded.forEach((bytes, index) => data2.set(bytes, offsets[index]));
|
|
987
|
+
return {
|
|
988
|
+
property: { type: "STRING", required: field.nullable !== true, ...hasNull ? { noData } : {} },
|
|
989
|
+
data: data2,
|
|
990
|
+
offsets
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
const widths = {
|
|
994
|
+
int8: 1,
|
|
995
|
+
uint8: 1,
|
|
996
|
+
int16: 2,
|
|
997
|
+
uint16: 2,
|
|
998
|
+
int32: 4,
|
|
999
|
+
uint32: 4,
|
|
1000
|
+
int64: 8,
|
|
1001
|
+
uint64: 8,
|
|
1002
|
+
float32: 4,
|
|
1003
|
+
float64: 8
|
|
1004
|
+
};
|
|
1005
|
+
const width = widths[String(type)];
|
|
1006
|
+
if (!width)
|
|
1007
|
+
throw new TileConversionError5(
|
|
1008
|
+
"MESH_FEATURE_TYPE_UNSUPPORTED",
|
|
1009
|
+
`No qualified GLB property mapping for ${String(type)}`
|
|
1010
|
+
);
|
|
1011
|
+
const isInteger64 = type === "int64" || type === "uint64";
|
|
1012
|
+
const isFloat = type === "float32" || type === "float64";
|
|
1013
|
+
const minimum = isFloat ? -Infinity : String(type).startsWith("uint") ? 0n : -(1n << BigInt(width * 8 - 1));
|
|
1014
|
+
const maximum = isFloat ? Infinity : (1n << BigInt(width * 8 - (String(type).startsWith("uint") ? 0 : 1))) - 1n;
|
|
1015
|
+
if (present.some(
|
|
1016
|
+
(value) => isInteger64 ? typeof value !== "bigint" || value < minimum || value > maximum : typeof value !== "number" || !Number.isFinite(value) || !isFloat && (!Number.isSafeInteger(value) || BigInt(value) < minimum || BigInt(value) > maximum)
|
|
1017
|
+
))
|
|
1018
|
+
throw new TileConversionError5(
|
|
1019
|
+
"MESH_FEATURE_VALUE_INVALID",
|
|
1020
|
+
`Values do not fit ${String(type)}`
|
|
1021
|
+
);
|
|
1022
|
+
if (hasNull) {
|
|
1023
|
+
const used = new Set(present.map(String));
|
|
1024
|
+
noData = 0;
|
|
1025
|
+
while (used.has(String(noData))) noData++;
|
|
1026
|
+
if (noData > maximum && minimum < 0) {
|
|
1027
|
+
noData = -1;
|
|
1028
|
+
while (used.has(String(noData))) noData--;
|
|
1029
|
+
}
|
|
1030
|
+
if (noData > maximum || noData < minimum)
|
|
1031
|
+
throw new TileConversionError5(
|
|
1032
|
+
"MESH_FEATURE_NULL_UNREPRESENTABLE",
|
|
1033
|
+
"No unused value remains for null without losing a real value"
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
const data = new Uint8Array(values.length * width);
|
|
1037
|
+
const view = new DataView(data.buffer);
|
|
1038
|
+
values.forEach((value, index) => {
|
|
1039
|
+
const selected = value === null ? isInteger64 ? BigInt(noData) : noData : value;
|
|
1040
|
+
const offset = index * width;
|
|
1041
|
+
switch (type) {
|
|
1042
|
+
case "int8":
|
|
1043
|
+
view.setInt8(offset, selected);
|
|
1044
|
+
break;
|
|
1045
|
+
case "uint8":
|
|
1046
|
+
view.setUint8(offset, selected);
|
|
1047
|
+
break;
|
|
1048
|
+
case "int16":
|
|
1049
|
+
view.setInt16(offset, selected, true);
|
|
1050
|
+
break;
|
|
1051
|
+
case "uint16":
|
|
1052
|
+
view.setUint16(offset, selected, true);
|
|
1053
|
+
break;
|
|
1054
|
+
case "int32":
|
|
1055
|
+
view.setInt32(offset, selected, true);
|
|
1056
|
+
break;
|
|
1057
|
+
case "uint32":
|
|
1058
|
+
view.setUint32(offset, selected, true);
|
|
1059
|
+
break;
|
|
1060
|
+
case "int64":
|
|
1061
|
+
view.setBigInt64(offset, selected, true);
|
|
1062
|
+
break;
|
|
1063
|
+
case "uint64":
|
|
1064
|
+
view.setBigUint64(offset, selected, true);
|
|
1065
|
+
break;
|
|
1066
|
+
case "float32":
|
|
1067
|
+
view.setFloat32(offset, selected, true);
|
|
1068
|
+
break;
|
|
1069
|
+
case "float64":
|
|
1070
|
+
view.setFloat64(offset, selected, true);
|
|
1071
|
+
break;
|
|
1072
|
+
}
|
|
1073
|
+
if (isFloat && !Number.isFinite(
|
|
1074
|
+
type === "float32" ? view.getFloat32(offset, true) : view.getFloat64(offset, true)
|
|
1075
|
+
))
|
|
1076
|
+
throw new TileConversionError5(
|
|
1077
|
+
"MESH_FEATURE_VALUE_INVALID",
|
|
1078
|
+
"Floating point property overflow"
|
|
1079
|
+
);
|
|
1080
|
+
});
|
|
1081
|
+
return {
|
|
1082
|
+
property: {
|
|
1083
|
+
type: "SCALAR",
|
|
1084
|
+
componentType: String(type).toUpperCase(),
|
|
1085
|
+
required: field.nullable !== true,
|
|
1086
|
+
...hasNull ? { noData } : {}
|
|
1087
|
+
},
|
|
1088
|
+
data
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// src/v5/mesh.ts
|
|
1093
|
+
import { TileConversionError as TileConversionError6 } from "@loaders.gl/tile-converter/v5/core";
|
|
1094
|
+
function encodeMeshTile(mesh, options = {}) {
|
|
1095
|
+
const geometry = prepareMeshFeatureGeometry(validateMeshGeometry(mesh), options.features);
|
|
1096
|
+
const { scenegraph, materialIndex } = createMeshTileScenegraph(options, geometry);
|
|
1097
|
+
const meshIndex = scenegraph.addMesh({
|
|
1098
|
+
attributes: geometry.attributes,
|
|
1099
|
+
indices: geometry.indices?.value,
|
|
1100
|
+
material: materialIndex,
|
|
1101
|
+
mode: 4
|
|
1102
|
+
});
|
|
1103
|
+
encodeMeshFeatures(scenegraph, meshIndex, options.features);
|
|
1104
|
+
return finalizeMeshTileScenegraph(scenegraph, meshIndex);
|
|
1105
|
+
}
|
|
1106
|
+
function createMeshTileScenegraph(options, geometry) {
|
|
1107
|
+
const scenegraph = new GLTFScenegraph({ json: { asset: { version: "2.0", generator: "loaders.gl" } } });
|
|
1108
|
+
const material = options.material === void 0 ? void 0 : validateMeshMaterial(options.material, scenegraph, "TEXCOORD_0" in geometry.attributes);
|
|
1109
|
+
return { scenegraph, materialIndex: material ? scenegraph.addMaterial(material) : void 0 };
|
|
1110
|
+
}
|
|
1111
|
+
function finalizeMeshTileScenegraph(scenegraph, meshIndex) {
|
|
1112
|
+
const nodeIndex = scenegraph.addNode({ meshIndex });
|
|
1113
|
+
const sceneIndex = scenegraph.addScene({ nodeIndices: [nodeIndex] });
|
|
1114
|
+
scenegraph.setDefaultScene(sceneIndex);
|
|
1115
|
+
scenegraph.createBinaryChunk();
|
|
1116
|
+
return GLTFWriter.encodeSync(scenegraph.gltf);
|
|
1117
|
+
}
|
|
1118
|
+
function validateMeshGeometry(mesh, allowFloat64Positions = false) {
|
|
1119
|
+
if (mesh.topology !== "triangle-list" || mesh.mode !== 4) {
|
|
1120
|
+
throw new TileConversionError6(
|
|
1121
|
+
"MESH_TOPOLOGY_UNSUPPORTED",
|
|
1122
|
+
"Only triangle-list mode 4 is supported"
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
for (const name of Object.keys(mesh.attributes)) {
|
|
1126
|
+
if (name !== "POSITION" && name !== "NORMAL" && name !== "COLOR_0" && name !== "TEXCOORD_0") {
|
|
1127
|
+
throw new TileConversionError6(
|
|
1128
|
+
"MESH_ATTRIBUTE_UNSUPPORTED",
|
|
1129
|
+
`Mesh attribute ${name} is unsupported`
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
const positions = getFloatAttribute(mesh.attributes.POSITION, "POSITION", allowFloat64Positions);
|
|
1134
|
+
const attributes = {
|
|
1135
|
+
POSITION: { value: positions, size: 3 }
|
|
1136
|
+
};
|
|
1137
|
+
if (mesh.attributes.NORMAL) {
|
|
1138
|
+
const normals = getFloatAttribute(mesh.attributes.NORMAL, "NORMAL");
|
|
1139
|
+
if (normals.length !== positions.length) {
|
|
1140
|
+
throw new TileConversionError6(
|
|
1141
|
+
"MESH_NORMAL_COUNT_MISMATCH",
|
|
1142
|
+
"NORMAL count must match POSITION count"
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
for (let index = 0; index < normals.length; index += 3) {
|
|
1146
|
+
if (Math.abs(Math.hypot(normals[index], normals[index + 1], normals[index + 2]) - 1) > 1e-4) {
|
|
1147
|
+
throw new TileConversionError6(
|
|
1148
|
+
"MESH_NORMAL_INVALID",
|
|
1149
|
+
"NORMAL vectors must have unit length"
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
attributes.NORMAL = { value: normals, size: 3 };
|
|
1154
|
+
}
|
|
1155
|
+
if ("COLOR_0" in mesh.attributes) {
|
|
1156
|
+
const colors = mesh.attributes.COLOR_0;
|
|
1157
|
+
const normalized = isNormalizedUnsignedAttribute(colors);
|
|
1158
|
+
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)) {
|
|
1159
|
+
throw new TileConversionError6(
|
|
1160
|
+
"MESH_COLOR_INVALID",
|
|
1161
|
+
"COLOR_0 requires packed linear RGB/RGBA: Float32 in [0, 1] or normalized Uint8/Uint16, one per vertex"
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
validateAttributeLayout(colors, "COLOR_0", normalized);
|
|
1165
|
+
attributes.COLOR_0 = {
|
|
1166
|
+
value: colors.value,
|
|
1167
|
+
size: colors.size,
|
|
1168
|
+
...normalized ? { normalized: true } : {}
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
if ("TEXCOORD_0" in mesh.attributes) {
|
|
1172
|
+
const textureCoordinates = mesh.attributes.TEXCOORD_0;
|
|
1173
|
+
const normalized = isNormalizedUnsignedAttribute(textureCoordinates);
|
|
1174
|
+
if (!textureCoordinates || !(textureCoordinates.value instanceof Float32Array || normalized) || textureCoordinates.size !== 2 || textureCoordinates.value.length / 2 !== positions.length / 3 || textureCoordinates.value.some((value) => !Number.isFinite(value))) {
|
|
1175
|
+
throw new TileConversionError6(
|
|
1176
|
+
"MESH_TEXCOORD_INVALID",
|
|
1177
|
+
"TEXCOORD_0 requires packed finite Float32 or normalized Uint8/Uint16 UV pairs, one per vertex"
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
validateAttributeLayout(textureCoordinates, "TEXCOORD_0", normalized);
|
|
1181
|
+
attributes.TEXCOORD_0 = {
|
|
1182
|
+
value: textureCoordinates.value,
|
|
1183
|
+
size: 2,
|
|
1184
|
+
...normalized ? { normalized: true } : {}
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
const indices = validateMeshIndices(mesh.indices, positions.length / 3);
|
|
1188
|
+
if (!indices && positions.length % 9 !== 0) {
|
|
1189
|
+
throw new TileConversionError6(
|
|
1190
|
+
"MESH_TRIANGLE_COUNT_INVALID",
|
|
1191
|
+
"Nonindexed meshes require complete triangles"
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
return {
|
|
1195
|
+
topology: "triangle-list",
|
|
1196
|
+
mode: 4,
|
|
1197
|
+
attributes,
|
|
1198
|
+
indices: indices ? { value: indices, size: 1 } : void 0
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
function getFloatAttribute(attribute, name, allowFloat64 = false) {
|
|
1202
|
+
if (!attribute || !(attribute.value instanceof Float32Array || allowFloat64 && attribute.value instanceof Float64Array) || attribute.size !== 3 || attribute.value.length === 0 || attribute.value.length % 3 !== 0) {
|
|
1203
|
+
throw new TileConversionError6(
|
|
1204
|
+
"MESH_ATTRIBUTE_INVALID",
|
|
1205
|
+
`${name} must contain packed ${allowFloat64 ? "Float32 or Float64" : "Float32"} xyz triples`
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
validateAttributeLayout(attribute, name);
|
|
1209
|
+
if (attribute.value.some((value) => !Number.isFinite(value))) {
|
|
1210
|
+
throw new TileConversionError6("MESH_ATTRIBUTE_NONFINITE", `${name} must contain finite values`);
|
|
1211
|
+
}
|
|
1212
|
+
return attribute.value;
|
|
1213
|
+
}
|
|
1214
|
+
function validateMeshIndices(attribute, vertexCount, primitiveMode = 4) {
|
|
1215
|
+
if (!attribute) return void 0;
|
|
1216
|
+
validateAttributeLayout(attribute, "indices");
|
|
1217
|
+
const values = attribute.value;
|
|
1218
|
+
if (attribute.size !== 1 || !(values instanceof Uint8Array || values instanceof Uint16Array || values instanceof Uint32Array)) {
|
|
1219
|
+
throw new TileConversionError6(
|
|
1220
|
+
"MESH_INDICES_INVALID",
|
|
1221
|
+
"Indices must contain packed unsigned integer scalars"
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
if (primitiveMode === 5 ? values.length < 3 : values.length === 0 || values.length % 3 !== 0) {
|
|
1225
|
+
throw new TileConversionError6(
|
|
1226
|
+
"MESH_TRIANGLE_COUNT_INVALID",
|
|
1227
|
+
"Indices must contain complete triangles or at least three strip vertices"
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
const restartIndex = 2 ** (values.BYTES_PER_ELEMENT * 8) - 1;
|
|
1231
|
+
if (values.some((value) => value >= vertexCount || value === restartIndex)) {
|
|
1232
|
+
throw new TileConversionError6(
|
|
1233
|
+
"MESH_INDEX_OUT_OF_RANGE",
|
|
1234
|
+
"Indices must reference existing vertices and exclude primitive-restart values"
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
return values;
|
|
1238
|
+
}
|
|
1239
|
+
function isNormalizedUnsignedAttribute(attribute) {
|
|
1240
|
+
return attribute?.normalized === true && (attribute.value instanceof Uint8Array || attribute.value instanceof Uint16Array);
|
|
1241
|
+
}
|
|
1242
|
+
function validateAttributeLayout(attribute, name, allowNormalized = false) {
|
|
1243
|
+
if ((attribute.byteOffset ?? 0) !== 0 || (attribute.byteStride ?? 0) !== 0 || attribute.normalized && !allowNormalized || attribute.transform || attribute.componentType) {
|
|
1244
|
+
throw new TileConversionError6(
|
|
1245
|
+
"MESH_ATTRIBUTE_LAYOUT_UNSUPPORTED",
|
|
1246
|
+
`${name} must use packed contiguous storage with supported normalization`
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
function validateMeshMaterial(material, scenegraph, hasTextureCoordinates) {
|
|
1251
|
+
if (!material || typeof material !== "object" || Array.isArray(material)) {
|
|
1252
|
+
throw new TileConversionError6("MESH_MATERIAL_INVALID", "Material must be an object");
|
|
1253
|
+
}
|
|
1254
|
+
const allowedProperties = [
|
|
1255
|
+
"baseColorFactor",
|
|
1256
|
+
"baseColorTexture",
|
|
1257
|
+
"metallicFactor",
|
|
1258
|
+
"roughnessFactor",
|
|
1259
|
+
"alphaMode",
|
|
1260
|
+
"alphaCutoff",
|
|
1261
|
+
"doubleSided"
|
|
1262
|
+
];
|
|
1263
|
+
for (const name of Object.keys(material)) {
|
|
1264
|
+
if (!allowedProperties.includes(name)) {
|
|
1265
|
+
throw new TileConversionError6(
|
|
1266
|
+
"MESH_MATERIAL_UNSUPPORTED",
|
|
1267
|
+
`Material property ${name} is unsupported`
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
const { baseColorFactor, metallicFactor, roughnessFactor, alphaMode, alphaCutoff, doubleSided } = material;
|
|
1272
|
+
if (baseColorFactor !== void 0 && (!Array.isArray(baseColorFactor) || baseColorFactor.length !== 4 || Array.from(baseColorFactor).some(
|
|
1273
|
+
(value) => !Number.isFinite(value) || value < 0 || value > 1
|
|
1274
|
+
)) || [metallicFactor, roughnessFactor].some(
|
|
1275
|
+
(value) => value !== void 0 && (!Number.isFinite(value) || value < 0 || value > 1)
|
|
1276
|
+
) || alphaMode !== void 0 && !["OPAQUE", "MASK", "BLEND"].includes(alphaMode) || alphaCutoff !== void 0 && (alphaMode !== "MASK" || !Number.isFinite(alphaCutoff) || alphaCutoff < 0) || doubleSided !== void 0 && typeof doubleSided !== "boolean") {
|
|
1277
|
+
throw new TileConversionError6(
|
|
1278
|
+
"MESH_MATERIAL_INVALID",
|
|
1279
|
+
"Material values must satisfy the selected glTF profile"
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
return {
|
|
1283
|
+
pbrMetallicRoughness: {
|
|
1284
|
+
baseColorFactor: baseColorFactor ? [...baseColorFactor] : void 0,
|
|
1285
|
+
metallicFactor,
|
|
1286
|
+
roughnessFactor,
|
|
1287
|
+
baseColorTexture: "baseColorTexture" in material ? encodeBaseColorTexture(material.baseColorTexture, scenegraph, hasTextureCoordinates) : void 0
|
|
1288
|
+
},
|
|
1289
|
+
alphaMode,
|
|
1290
|
+
alphaCutoff,
|
|
1291
|
+
doubleSided
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
function encodeBaseColorTexture(texture, scenegraph, hasTextureCoordinates) {
|
|
1295
|
+
if (!texture || Object.keys(texture).some(
|
|
1296
|
+
(name) => name !== "data" && name !== "mimeType" && name !== "sampler" && name !== "transform"
|
|
1297
|
+
) || !(texture.data instanceof Uint8Array) || !["image/png", "image/jpeg"].includes(texture.mimeType)) {
|
|
1298
|
+
throw new TileConversionError6(
|
|
1299
|
+
"MESH_TEXTURE_INVALID",
|
|
1300
|
+
"Base-color texture must supply Uint8Array data, image/png or image/jpeg mimeType, and optional sampling/transform controls"
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
if (!hasTextureCoordinates) {
|
|
1304
|
+
throw new TileConversionError6(
|
|
1305
|
+
"MESH_TEXCOORD_REQUIRED",
|
|
1306
|
+
"Base-color texture requires TEXCOORD_0"
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
const image = new DataView(texture.data.buffer, texture.data.byteOffset, texture.data.byteLength);
|
|
1310
|
+
let metadata;
|
|
1311
|
+
try {
|
|
1312
|
+
metadata = getBinaryImageMetadata(image);
|
|
1313
|
+
} catch {
|
|
1314
|
+
}
|
|
1315
|
+
if (!metadata || metadata.mimeType !== texture.mimeType || metadata.width <= 0 || metadata.height <= 0) {
|
|
1316
|
+
throw new TileConversionError6(
|
|
1317
|
+
"MESH_TEXTURE_INVALID",
|
|
1318
|
+
"Image header must match mimeType and declare positive dimensions"
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
const samplerIndex = "sampler" in texture ? scenegraph.addSampler(validateMeshSampler(texture.sampler)) : void 0;
|
|
1322
|
+
const transform = "transform" in texture ? validateMeshTextureTransform(texture.transform) : void 0;
|
|
1323
|
+
if (transform) scenegraph.registerRequiredExtension("KHR_texture_transform");
|
|
1324
|
+
return {
|
|
1325
|
+
...transform ? { extensions: { KHR_texture_transform: transform } } : {},
|
|
1326
|
+
index: scenegraph.addTexture({
|
|
1327
|
+
imageIndex: scenegraph.addImage(image, texture.mimeType),
|
|
1328
|
+
samplerIndex
|
|
1329
|
+
})
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
function validateMeshSampler(sampler) {
|
|
1333
|
+
if (!sampler || typeof sampler !== "object" || Array.isArray(sampler)) {
|
|
1334
|
+
throw new TileConversionError6("MESH_SAMPLER_INVALID", "Sampler must be an object");
|
|
1335
|
+
}
|
|
1336
|
+
const allowedValues = {
|
|
1337
|
+
wrapS: [33071, 33648, 10497],
|
|
1338
|
+
wrapT: [33071, 33648, 10497],
|
|
1339
|
+
magFilter: [9728, 9729],
|
|
1340
|
+
minFilter: [9728, 9729, 9984, 9985, 9986, 9987]
|
|
1341
|
+
};
|
|
1342
|
+
for (const name in sampler) {
|
|
1343
|
+
if (!Object.hasOwn(allowedValues, name)) {
|
|
1344
|
+
throw new TileConversionError6(
|
|
1345
|
+
"MESH_SAMPLER_UNSUPPORTED",
|
|
1346
|
+
`Sampler property ${name} is unsupported`
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
const selectedSampler = {
|
|
1351
|
+
wrapS: sampler.wrapS,
|
|
1352
|
+
wrapT: sampler.wrapT,
|
|
1353
|
+
minFilter: sampler.minFilter,
|
|
1354
|
+
magFilter: sampler.magFilter
|
|
1355
|
+
};
|
|
1356
|
+
for (const name of Object.keys(allowedValues)) {
|
|
1357
|
+
const property = name;
|
|
1358
|
+
if (selectedSampler[property] !== void 0 && !allowedValues[property].includes(selectedSampler[property])) {
|
|
1359
|
+
throw new TileConversionError6(
|
|
1360
|
+
"MESH_SAMPLER_INVALID",
|
|
1361
|
+
`Sampler ${name} must use a glTF wrapping/filtering value`
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return selectedSampler;
|
|
1366
|
+
}
|
|
1367
|
+
function validateMeshTextureTransform(transform) {
|
|
1368
|
+
if (!transform || typeof transform !== "object" || Array.isArray(transform)) {
|
|
1369
|
+
throw new TileConversionError6(
|
|
1370
|
+
"MESH_TEXTURE_TRANSFORM_INVALID",
|
|
1371
|
+
"Texture transform must be an object"
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
for (const name in transform) {
|
|
1375
|
+
if (name !== "offset" && name !== "rotation" && name !== "scale") {
|
|
1376
|
+
throw new TileConversionError6(
|
|
1377
|
+
"MESH_TEXTURE_TRANSFORM_UNSUPPORTED",
|
|
1378
|
+
`Texture transform property ${name} is unsupported`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
const { offset, rotation, scale } = transform;
|
|
1383
|
+
const offsetValues = Array.isArray(offset) && offset.length === 2 ? [offset[0], offset[1]] : offset;
|
|
1384
|
+
const scaleValues = Array.isArray(scale) && scale.length === 2 ? [scale[0], scale[1]] : scale;
|
|
1385
|
+
if ([offsetValues, scaleValues].some(
|
|
1386
|
+
(value) => value !== void 0 && (!Array.isArray(value) || value.length !== 2 || value.some((component) => !Number.isFinite(component)))
|
|
1387
|
+
) || rotation !== void 0 && !Number.isFinite(rotation)) {
|
|
1388
|
+
throw new TileConversionError6(
|
|
1389
|
+
"MESH_TEXTURE_TRANSFORM_INVALID",
|
|
1390
|
+
"Texture transform requires finite UV pairs and a finite rotation"
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
return {
|
|
1394
|
+
offset: offsetValues === void 0 ? void 0 : [offsetValues[0], offsetValues[1]],
|
|
1395
|
+
rotation,
|
|
1396
|
+
scale: scaleValues === void 0 ? void 0 : [scaleValues[0], scaleValues[1]]
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// src/v5/mesh-conversion.ts
|
|
1401
|
+
import { getSpatialCoordinateFrame as getSpatialCoordinateFrame2 } from "@loaders.gl/tiles";
|
|
1402
|
+
import { TileConversionError as TileConversionError8 } from "@loaders.gl/tile-converter/v5/core";
|
|
1403
|
+
|
|
1404
|
+
// src/v5/mesh-draco.ts
|
|
1405
|
+
import { DracoLoader, encodeDraco } from "@loaders.gl/draco";
|
|
1406
|
+
import { TileConversionError as TileConversionError7 } from "@loaders.gl/tile-converter/v5/core";
|
|
1407
|
+
var DRACO_EXTENSION = "KHR_draco_mesh_compression";
|
|
1408
|
+
async function encodeDracoMeshTile(geometry, options, libraryOptions = {}) {
|
|
1409
|
+
geometry = prepareMeshFeatureGeometry(validateMeshGeometry(geometry), options.features);
|
|
1410
|
+
if (!geometry.indices) {
|
|
1411
|
+
geometry.indices = {
|
|
1412
|
+
size: 1,
|
|
1413
|
+
value: Uint32Array.from(
|
|
1414
|
+
{ length: geometry.attributes.POSITION.value.length / 3 },
|
|
1415
|
+
(_, index) => index
|
|
1416
|
+
)
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
const { modules, useLocalLibraries, CDN: contentDeliveryNetwork } = libraryOptions;
|
|
1420
|
+
const runtimeOptions = { modules, core: { useLocalLibraries, CDN: contentDeliveryNetwork } };
|
|
1421
|
+
const { scenegraph, materialIndex } = createMeshTileScenegraph(options, geometry);
|
|
1422
|
+
const encoded = await encodeDraco(geometry, {
|
|
1423
|
+
...runtimeOptions,
|
|
1424
|
+
draco: { method: "MESH_EDGEBREAKER_ENCODING" }
|
|
1425
|
+
});
|
|
1426
|
+
const loaderOptions = { ...runtimeOptions, draco: { shape: "mesh" } };
|
|
1427
|
+
const loader = await DracoLoader.preload("", loaderOptions);
|
|
1428
|
+
const decoded = await loader.parse(encoded.data, loaderOptions);
|
|
1429
|
+
const positions = decoded.attributes.POSITION.value;
|
|
1430
|
+
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)) {
|
|
1431
|
+
throw new TileConversionError7(
|
|
1432
|
+
"MESH_DRACO_ENCODING_INVALID",
|
|
1433
|
+
"Draco must preserve the triangle count and emit finite positions"
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
const inputPositions = geometry.attributes.POSITION.value;
|
|
1437
|
+
const positionKeys = /* @__PURE__ */ new Set();
|
|
1438
|
+
for (let index = 0; index < inputPositions.length; index += 3) {
|
|
1439
|
+
positionKeys.add(getPositionKey(inputPositions, index));
|
|
1440
|
+
}
|
|
1441
|
+
for (let index = 0; index < positions.length; index += 3) {
|
|
1442
|
+
if (!positionKeys.has(getPositionKey(positions, index))) {
|
|
1443
|
+
throw new TileConversionError7(
|
|
1444
|
+
"MESH_DRACO_POSITION_CHANGED",
|
|
1445
|
+
"Lossless Draco must preserve the encoded Float32 positions"
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
if (JSON.stringify(getMeshTriangles(geometry)) !== JSON.stringify(getMeshTriangles(decoded))) {
|
|
1450
|
+
throw new TileConversionError7(
|
|
1451
|
+
"MESH_DRACO_ATTRIBUTE_CHANGED",
|
|
1452
|
+
"Lossless Draco must preserve oriented triangles and every attribute, including feature ownership"
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
const bufferView = scenegraph.addBufferView(encoded.data);
|
|
1456
|
+
const attributes = {};
|
|
1457
|
+
const attributeIdentifiers = {};
|
|
1458
|
+
scenegraph.json.accessors = [];
|
|
1459
|
+
for (const [name, attribute] of Object.entries(geometry.attributes)) {
|
|
1460
|
+
const decodedAttribute = decoded.attributes[name];
|
|
1461
|
+
if (!decodedAttribute || decodedAttribute.value.length !== encoded.report.pointCount * attribute.size) {
|
|
1462
|
+
throw new TileConversionError7("MESH_DRACO_ENCODING_INVALID", `Draco must preserve ${name}`);
|
|
1463
|
+
}
|
|
1464
|
+
const accessor = {
|
|
1465
|
+
componentType: attribute.value instanceof Uint8Array ? 5121 : attribute.value instanceof Uint16Array ? 5123 : attribute.value instanceof Uint32Array ? 5125 : 5126,
|
|
1466
|
+
count: encoded.report.pointCount,
|
|
1467
|
+
type: attribute.size === 1 ? "SCALAR" : attribute.size === 2 ? "VEC2" : attribute.size === 3 ? "VEC3" : "VEC4",
|
|
1468
|
+
...attribute.normalized ? { normalized: true } : {}
|
|
1469
|
+
};
|
|
1470
|
+
if (name === "POSITION") {
|
|
1471
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
1472
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
1473
|
+
for (let index = 0; index < positions.length; index++) {
|
|
1474
|
+
const axis = index % 3;
|
|
1475
|
+
minimum[axis] = Math.min(minimum[axis], positions[index]);
|
|
1476
|
+
maximum[axis] = Math.max(maximum[axis], positions[index]);
|
|
1477
|
+
}
|
|
1478
|
+
accessor.min = minimum;
|
|
1479
|
+
accessor.max = maximum;
|
|
1480
|
+
}
|
|
1481
|
+
attributes[name] = scenegraph.json.accessors.push(accessor) - 1;
|
|
1482
|
+
attributeIdentifiers[name] = encoded.report.attributes[name].id;
|
|
1483
|
+
}
|
|
1484
|
+
const indices = scenegraph.json.accessors.push({
|
|
1485
|
+
componentType: decoded.indices.value instanceof Uint16Array ? 5123 : 5125,
|
|
1486
|
+
count: encoded.report.faceCount * 3,
|
|
1487
|
+
type: "SCALAR"
|
|
1488
|
+
}) - 1;
|
|
1489
|
+
scenegraph.json.meshes = [
|
|
1490
|
+
{
|
|
1491
|
+
primitives: [
|
|
1492
|
+
{
|
|
1493
|
+
attributes,
|
|
1494
|
+
indices,
|
|
1495
|
+
mode: 4,
|
|
1496
|
+
...materialIndex === void 0 ? {} : { material: materialIndex },
|
|
1497
|
+
extensions: { [DRACO_EXTENSION]: { bufferView, attributes: attributeIdentifiers } }
|
|
1498
|
+
}
|
|
1499
|
+
]
|
|
1500
|
+
}
|
|
1501
|
+
];
|
|
1502
|
+
encodeMeshFeatures(scenegraph, 0, options.features);
|
|
1503
|
+
scenegraph.registerRequiredExtension(DRACO_EXTENSION);
|
|
1504
|
+
return { glb: finalizeMeshTileScenegraph(scenegraph, 0), positions };
|
|
1505
|
+
}
|
|
1506
|
+
function getPositionKey(positions, index) {
|
|
1507
|
+
return `${positions[index]},${positions[index + 1]},${positions[index + 2]}`;
|
|
1508
|
+
}
|
|
1509
|
+
function getMeshTriangles(mesh) {
|
|
1510
|
+
const names = Object.keys(mesh.attributes).sort();
|
|
1511
|
+
const indices = mesh.indices.value;
|
|
1512
|
+
const triangles = [];
|
|
1513
|
+
for (let index = 0; index < indices.length; index += 3) {
|
|
1514
|
+
const vertices = [0, 1, 2].map(
|
|
1515
|
+
(corner) => names.map((name) => {
|
|
1516
|
+
const attribute = mesh.attributes[name];
|
|
1517
|
+
return Array.from(
|
|
1518
|
+
{ length: attribute.size },
|
|
1519
|
+
(_, component) => attribute.value[indices[index + corner] * attribute.size + component]
|
|
1520
|
+
).join(",");
|
|
1521
|
+
}).join("|")
|
|
1522
|
+
);
|
|
1523
|
+
triangles.push(
|
|
1524
|
+
[0, 1, 2].map((corner) => [...vertices.slice(corner), ...vertices.slice(0, corner)].join(";")).sort()[0]
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
return triangles.sort();
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// src/v5/mesh-conversion.ts
|
|
1531
|
+
function createMeshConversionCodec(options) {
|
|
1532
|
+
const { spatialContext, maxPositionError, draco = true, dracoLibraryOptions } = options;
|
|
1533
|
+
if (typeof draco !== "boolean") {
|
|
1534
|
+
throw new TileConversionError8("MESH_DRACO_OPTIONS_INVALID", "draco must be a boolean");
|
|
1535
|
+
}
|
|
1536
|
+
if (!Number.isFinite(maxPositionError) || maxPositionError < 0) {
|
|
1537
|
+
throw new TileConversionError8(
|
|
1538
|
+
"MESH_PRECISION_LIMIT_INVALID",
|
|
1539
|
+
"maxPositionError must be finite and nonnegative"
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
const spatialReference = spatialContext.spatialReference;
|
|
1543
|
+
const outputCrs = spatialReference.targetCrs || spatialReference.sourceCrs;
|
|
1544
|
+
const outputFrame = spatialReference.status === "native" ? spatialReference.coordinateFrame : outputCrs ? getSpatialCoordinateFrame2(outputCrs) : "unknown";
|
|
1545
|
+
if (!["native", "transformed"].includes(spatialReference.status) || !["projected", "geocentric", "local", "cartesian"].includes(outputFrame)) {
|
|
1546
|
+
throw new TileConversionError8(
|
|
1547
|
+
"MESH_OUTPUT_FRAME_UNSUPPORTED",
|
|
1548
|
+
"Mesh output requires a resolved Cartesian coordinate frame"
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
return {
|
|
1552
|
+
/** Prepares and encodes one geometry while honoring cancellation. */
|
|
1553
|
+
async *convert(resource, _inspection, signal) {
|
|
1554
|
+
signal?.throwIfAborted();
|
|
1555
|
+
const geometry = validateMeshGeometry(resource.mesh, true);
|
|
1556
|
+
const sourcePositions = geometry.attributes.POSITION.value;
|
|
1557
|
+
const origin = [...resource.origin];
|
|
1558
|
+
if (origin.length !== 3 || origin.some((value) => !Number.isFinite(value))) {
|
|
1559
|
+
throw new TileConversionError8(
|
|
1560
|
+
"MESH_ORIGIN_INVALID",
|
|
1561
|
+
"Target origin must contain three finite coordinates"
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
const transformed = "transformGeometryAsync" in spatialContext ? await spatialContext.transformGeometryAsync(
|
|
1565
|
+
sourcePositions,
|
|
1566
|
+
geometry.attributes.NORMAL?.value,
|
|
1567
|
+
resource.normalReferenceFrame ?? options.normalReferenceFrame
|
|
1568
|
+
) : {
|
|
1569
|
+
positions: spatialContext.transformPositions(sourcePositions),
|
|
1570
|
+
normals: geometry.attributes.NORMAL ? spatialContext.transformNormals(geometry.attributes.NORMAL.value, sourcePositions) : void 0
|
|
1571
|
+
};
|
|
1572
|
+
signal?.throwIfAborted();
|
|
1573
|
+
const targetPositions = transformed.positions;
|
|
1574
|
+
if (targetPositions.length !== sourcePositions.length || targetPositions.some((value) => !Number.isFinite(value))) {
|
|
1575
|
+
throw new TileConversionError8(
|
|
1576
|
+
"MESH_TRANSFORM_INVALID",
|
|
1577
|
+
"Spatial transformation must return one finite xyz position per vertex"
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
if (options.autoOrigin) {
|
|
1581
|
+
const minimum2 = [Infinity, Infinity, Infinity];
|
|
1582
|
+
const maximum2 = [-Infinity, -Infinity, -Infinity];
|
|
1583
|
+
for (let index = 0; index < targetPositions.length; index++) {
|
|
1584
|
+
const axis = index % 3;
|
|
1585
|
+
minimum2[axis] = Math.min(minimum2[axis], targetPositions[index]);
|
|
1586
|
+
maximum2[axis] = Math.max(maximum2[axis], targetPositions[index]);
|
|
1587
|
+
}
|
|
1588
|
+
for (let axis = 0; axis < 3; axis++) origin[axis] = minimum2[axis] / 2 + maximum2[axis] / 2;
|
|
1589
|
+
}
|
|
1590
|
+
const localPositions = new Float32Array(targetPositions.length);
|
|
1591
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
1592
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
1593
|
+
const localMinimum = [Infinity, Infinity, Infinity];
|
|
1594
|
+
const localMaximum = [-Infinity, -Infinity, -Infinity];
|
|
1595
|
+
let maximumPositionError = 0;
|
|
1596
|
+
for (let index = 0; index < targetPositions.length; index += 3) {
|
|
1597
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
1598
|
+
localPositions[index + axis] = targetPositions[index + axis] - origin[axis];
|
|
1599
|
+
}
|
|
1600
|
+
const positionError = Math.hypot(
|
|
1601
|
+
localPositions[index] + origin[0] - targetPositions[index],
|
|
1602
|
+
localPositions[index + 1] + origin[1] - targetPositions[index + 1],
|
|
1603
|
+
localPositions[index + 2] + origin[2] - targetPositions[index + 2]
|
|
1604
|
+
);
|
|
1605
|
+
if (!Number.isFinite(positionError) || positionError > maxPositionError) {
|
|
1606
|
+
throw new TileConversionError8(
|
|
1607
|
+
"MESH_POSITION_PRECISION_EXCEEDED",
|
|
1608
|
+
"Float32 mesh positions exceed the configured reconstruction error budget"
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
maximumPositionError = Math.max(maximumPositionError, positionError);
|
|
1612
|
+
}
|
|
1613
|
+
geometry.attributes.POSITION = { value: localPositions, size: 3 };
|
|
1614
|
+
if (geometry.attributes.NORMAL) {
|
|
1615
|
+
geometry.attributes.NORMAL = {
|
|
1616
|
+
value: transformed.normals,
|
|
1617
|
+
size: 3
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
const appearance = { material: resource.material, features: resource.features };
|
|
1621
|
+
const encoded = draco ? await encodeDracoMeshTile(geometry, appearance, dracoLibraryOptions) : { glb: encodeMeshTile(geometry, appearance), positions: localPositions };
|
|
1622
|
+
signal?.throwIfAborted();
|
|
1623
|
+
for (let index = 0; index < encoded.positions.length; index++) {
|
|
1624
|
+
const axis = index % 3;
|
|
1625
|
+
const position = encoded.positions[index];
|
|
1626
|
+
localMinimum[axis] = Math.min(localMinimum[axis], position);
|
|
1627
|
+
localMaximum[axis] = Math.max(localMaximum[axis], position);
|
|
1628
|
+
minimum[axis] = Math.min(minimum[axis], position + origin[axis]);
|
|
1629
|
+
maximum[axis] = Math.max(maximum[axis], position + origin[axis]);
|
|
1630
|
+
}
|
|
1631
|
+
yield {
|
|
1632
|
+
id: resource.id,
|
|
1633
|
+
glb: encoded.glb,
|
|
1634
|
+
origin,
|
|
1635
|
+
boundingBox: [minimum, maximum],
|
|
1636
|
+
localBoundingBox: [localMinimum, localMaximum],
|
|
1637
|
+
spatialReference,
|
|
1638
|
+
maximumPositionError
|
|
1639
|
+
};
|
|
1640
|
+
},
|
|
1641
|
+
/** Reports position rounding authorized by the configured precision budget. */
|
|
1642
|
+
async validateOutput(resource) {
|
|
1643
|
+
return resource.maximumPositionError > 0 ? [
|
|
1644
|
+
{
|
|
1645
|
+
code: "MESH_POSITION_ROUNDING",
|
|
1646
|
+
message: `Encoded positions have up to ${resource.maximumPositionError} target-unit reconstruction error`,
|
|
1647
|
+
severity: "info",
|
|
1648
|
+
resourceId: resource.id
|
|
1649
|
+
}
|
|
1650
|
+
] : [];
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// src/v5/mesh-tileset-sink.ts
|
|
1656
|
+
import { TileConversionError as TileConversionError9 } from "@loaders.gl/tile-converter/v5/core";
|
|
1657
|
+
import { createBoundedMemoryTileConversionSink } from "@loaders.gl/tile-converter/v5/core";
|
|
1658
|
+
function createSingleMeshTilesetSink(options) {
|
|
1659
|
+
const { geometricError } = options;
|
|
1660
|
+
if (!Number.isFinite(geometricError) || geometricError < 0) {
|
|
1661
|
+
throw new TileConversionError9(
|
|
1662
|
+
"MESH_GEOMETRIC_ERROR_INVALID",
|
|
1663
|
+
"geometricError must be finite and nonnegative, in meters"
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
const memorySink = createBoundedMemoryTileConversionSink(options);
|
|
1667
|
+
let state = "empty";
|
|
1668
|
+
let completed = false;
|
|
1669
|
+
return {
|
|
1670
|
+
getFiles() {
|
|
1671
|
+
return completed ? memorySink.getFiles() : [];
|
|
1672
|
+
},
|
|
1673
|
+
async write(resource, signal) {
|
|
1674
|
+
signal?.throwIfAborted();
|
|
1675
|
+
if (state !== "empty") {
|
|
1676
|
+
throw new TileConversionError9(
|
|
1677
|
+
"SINGLE_MESH_SINK_UNAVAILABLE",
|
|
1678
|
+
"The sink accepts exactly one mesh before finalization or abort"
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
const json = JSON.stringify(createMeshTileset(resource, geometricError));
|
|
1682
|
+
state = "writing";
|
|
1683
|
+
await memorySink.write(
|
|
1684
|
+
{ resourceId: "mesh.glb", parts: [resource.glb], contentType: "model/gltf-binary" },
|
|
1685
|
+
signal
|
|
1686
|
+
);
|
|
1687
|
+
await memorySink.write(
|
|
1688
|
+
{ resourceId: "tileset.json", parts: [json], contentType: "application/json" },
|
|
1689
|
+
signal
|
|
1690
|
+
);
|
|
1691
|
+
state = "written";
|
|
1692
|
+
},
|
|
1693
|
+
async finalize(report) {
|
|
1694
|
+
if (state !== "written") {
|
|
1695
|
+
throw new TileConversionError9(
|
|
1696
|
+
"SINGLE_MESH_SINK_INCOMPLETE",
|
|
1697
|
+
"Finalization requires exactly one successfully written mesh"
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
await memorySink.finalize(report);
|
|
1701
|
+
state = "closed";
|
|
1702
|
+
completed = true;
|
|
1703
|
+
},
|
|
1704
|
+
async abort(error) {
|
|
1705
|
+
state = "closed";
|
|
1706
|
+
completed = false;
|
|
1707
|
+
await memorySink.abort(error);
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
function createMeshTileset(resource, geometricError) {
|
|
1712
|
+
const reference = resource.spatialReference;
|
|
1713
|
+
const outputCrs = reference.targetCrs || reference.sourceCrs;
|
|
1714
|
+
const heightReference = reference.targetHeightReference === "native" ? reference.heightReference : reference.targetHeightReference;
|
|
1715
|
+
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"))) {
|
|
1716
|
+
throw new TileConversionError9(
|
|
1717
|
+
"MESH_TILESET_FRAME_UNSUPPORTED",
|
|
1718
|
+
"Single-mesh tilesets require resolved EPSG:4978 xyz coordinates in meters with ellipsoidal heights"
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
if (!Number.isFinite(resource.maximumPositionError) || resource.maximumPositionError < 0 || resource.maximumPositionError > geometricError) {
|
|
1722
|
+
throw new TileConversionError9(
|
|
1723
|
+
"MESH_GEOMETRIC_ERROR_INVALID",
|
|
1724
|
+
"geometricError must include the measured position reconstruction error"
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
const origin = [...resource.origin];
|
|
1728
|
+
const [minimum, maximum] = resource.localBoundingBox.map((bound) => [...bound]);
|
|
1729
|
+
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])) {
|
|
1730
|
+
throw new TileConversionError9(
|
|
1731
|
+
"MESH_TILESET_BOUNDS_INVALID",
|
|
1732
|
+
"Origin and local bounds must be finite xyz coordinates with ordered bounds"
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
const center = minimum.map((value, axis) => value / 2 + maximum[axis] / 2);
|
|
1736
|
+
const halfSize = minimum.map((value, axis) => maximum[axis] / 2 - value / 2);
|
|
1737
|
+
return {
|
|
1738
|
+
asset: { version: "1.1" },
|
|
1739
|
+
geometricError,
|
|
1740
|
+
root: {
|
|
1741
|
+
// 3D Tiles applies y-up -> z-up to GLB content before the tile transform.
|
|
1742
|
+
transform: [1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, ...origin, 1],
|
|
1743
|
+
boundingVolume: {
|
|
1744
|
+
// Bounds use the same y-up -> z-up rotation before the inverse tile rotation.
|
|
1745
|
+
box: [
|
|
1746
|
+
center[0],
|
|
1747
|
+
-center[2],
|
|
1748
|
+
center[1],
|
|
1749
|
+
halfSize[0],
|
|
1750
|
+
0,
|
|
1751
|
+
0,
|
|
1752
|
+
0,
|
|
1753
|
+
0,
|
|
1754
|
+
halfSize[1],
|
|
1755
|
+
0,
|
|
1756
|
+
-halfSize[2],
|
|
1757
|
+
0
|
|
1758
|
+
]
|
|
1759
|
+
},
|
|
1760
|
+
geometricError,
|
|
1761
|
+
refine: "REPLACE",
|
|
1762
|
+
content: { uri: "mesh.glb" }
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
function createMeshTilesetSink(options) {
|
|
1767
|
+
const { geometricError, maxMeshes } = options;
|
|
1768
|
+
if (!Number.isFinite(geometricError) || geometricError < 0 || !Number.isSafeInteger(maxMeshes) || maxMeshes < 1) {
|
|
1769
|
+
throw new TileConversionError9(
|
|
1770
|
+
"MESH_TILESET_OPTIONS_INVALID",
|
|
1771
|
+
"A leaf collection requires finite nonnegative geometricError and positive maxMeshes"
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
const memory = createBoundedMemoryTileConversionSink(options);
|
|
1775
|
+
const children = [];
|
|
1776
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
1777
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
1778
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
1779
|
+
let state = "open";
|
|
1780
|
+
let completed = false;
|
|
1781
|
+
return {
|
|
1782
|
+
/** Exposes the complete package atomically. */
|
|
1783
|
+
getFiles: () => completed ? memory.getFiles() : [],
|
|
1784
|
+
/** Retains one placement and its corresponding encoded GLB. */
|
|
1785
|
+
async write(resource, signal) {
|
|
1786
|
+
signal?.throwIfAborted();
|
|
1787
|
+
if (state !== "open")
|
|
1788
|
+
throw new TileConversionError9(
|
|
1789
|
+
"MESH_TILESET_SINK_UNAVAILABLE",
|
|
1790
|
+
"The mesh sink is busy or closed"
|
|
1791
|
+
);
|
|
1792
|
+
if (children.length >= maxMeshes || identifiers.has(resource.id))
|
|
1793
|
+
throw new TileConversionError9(
|
|
1794
|
+
"MESH_TILESET_PLACEMENT_INVALID",
|
|
1795
|
+
"Mesh count exceeds maxMeshes or a placement identifier is repeated"
|
|
1796
|
+
);
|
|
1797
|
+
const tile = createMeshTileset(resource, geometricError).root;
|
|
1798
|
+
const lower = resource.localBoundingBox[0].map(
|
|
1799
|
+
(value, axis) => value + resource.origin[axis]
|
|
1800
|
+
);
|
|
1801
|
+
const upper = resource.localBoundingBox[1].map(
|
|
1802
|
+
(value, axis) => value + resource.origin[axis]
|
|
1803
|
+
);
|
|
1804
|
+
if ([...lower, ...upper].some((value) => !Number.isFinite(value)))
|
|
1805
|
+
throw new TileConversionError9(
|
|
1806
|
+
"MESH_TILESET_BOUNDS_INVALID",
|
|
1807
|
+
"Absolute bounds must be finite"
|
|
1808
|
+
);
|
|
1809
|
+
const resourceId = `meshes/${children.length}.glb`;
|
|
1810
|
+
tile.content = { uri: resourceId };
|
|
1811
|
+
state = "writing";
|
|
1812
|
+
await memory.write(
|
|
1813
|
+
{ resourceId, parts: [resource.glb], contentType: "model/gltf-binary" },
|
|
1814
|
+
signal
|
|
1815
|
+
);
|
|
1816
|
+
children.push(tile);
|
|
1817
|
+
identifiers.add(resource.id);
|
|
1818
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
1819
|
+
minimum[axis] = Math.min(minimum[axis], lower[axis]);
|
|
1820
|
+
maximum[axis] = Math.max(maximum[axis], upper[axis]);
|
|
1821
|
+
}
|
|
1822
|
+
state = "open";
|
|
1823
|
+
},
|
|
1824
|
+
/** Writes the enclosing root only when every leaf has completed. */
|
|
1825
|
+
async finalize(report) {
|
|
1826
|
+
if (state !== "open" || !children.length)
|
|
1827
|
+
throw new TileConversionError9(
|
|
1828
|
+
"MESH_TILESET_SINK_INCOMPLETE",
|
|
1829
|
+
"Finalization requires written mesh leaves"
|
|
1830
|
+
);
|
|
1831
|
+
state = "writing";
|
|
1832
|
+
const center = minimum.map((value, axis) => value / 2 + maximum[axis] / 2);
|
|
1833
|
+
const halfSize = minimum.map((value, axis) => maximum[axis] / 2 - value / 2);
|
|
1834
|
+
const rootGeometricError = Math.max(geometricError, 2 * Math.hypot(...halfSize));
|
|
1835
|
+
if (!Number.isFinite(rootGeometricError))
|
|
1836
|
+
throw new TileConversionError9(
|
|
1837
|
+
"MESH_TILESET_BOUNDS_INVALID",
|
|
1838
|
+
"Root bounds must have a finite diagonal"
|
|
1839
|
+
);
|
|
1840
|
+
const json = JSON.stringify({
|
|
1841
|
+
asset: { version: "1.1" },
|
|
1842
|
+
geometricError: rootGeometricError,
|
|
1843
|
+
root: {
|
|
1844
|
+
boundingVolume: {
|
|
1845
|
+
box: [
|
|
1846
|
+
center[0],
|
|
1847
|
+
center[1],
|
|
1848
|
+
center[2],
|
|
1849
|
+
halfSize[0],
|
|
1850
|
+
0,
|
|
1851
|
+
0,
|
|
1852
|
+
0,
|
|
1853
|
+
halfSize[1],
|
|
1854
|
+
0,
|
|
1855
|
+
0,
|
|
1856
|
+
0,
|
|
1857
|
+
halfSize[2]
|
|
1858
|
+
]
|
|
1859
|
+
},
|
|
1860
|
+
geometricError: rootGeometricError,
|
|
1861
|
+
refine: "ADD",
|
|
1862
|
+
children
|
|
1863
|
+
}
|
|
1864
|
+
});
|
|
1865
|
+
await memory.write({
|
|
1866
|
+
resourceId: "tileset.json",
|
|
1867
|
+
parts: [json],
|
|
1868
|
+
contentType: "application/json"
|
|
1869
|
+
});
|
|
1870
|
+
await memory.finalize(report);
|
|
1871
|
+
state = "closed";
|
|
1872
|
+
completed = true;
|
|
1873
|
+
},
|
|
1874
|
+
/** Discards files and hierarchy metadata on failure or cancellation. */
|
|
1875
|
+
async abort(error) {
|
|
1876
|
+
state = "closed";
|
|
1877
|
+
completed = false;
|
|
1878
|
+
children.length = 0;
|
|
1879
|
+
identifiers.clear();
|
|
1880
|
+
await memory.abort(error);
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// src/v5/conversion-archive.ts
|
|
1886
|
+
import {
|
|
1887
|
+
Tiles3DArchiveWriter,
|
|
1888
|
+
encodeTiles3DArchiveInBatches
|
|
1889
|
+
} from "@loaders.gl/3d-tiles/3d-tiles-archive-writer";
|
|
1890
|
+
import { SLPKWriter, encodeSLPKArchiveInBatches } from "@loaders.gl/i3s/i3s-slpk-writer";
|
|
1891
|
+
import { TileConversionError as TileConversionError10 } from "@loaders.gl/tile-converter/v5/core";
|
|
1892
|
+
async function createTileConversionArchive(files, options) {
|
|
1893
|
+
const { resources, format, maxArchiveBytes, signal } = prepareArchiveInput(files, options);
|
|
1894
|
+
const writer = format === "3tz" ? Tiles3DArchiveWriter : SLPKWriter;
|
|
1895
|
+
const archive = await writer.encode(resources, { [format]: { maxArchiveBytes, signal } });
|
|
1896
|
+
return new Blob([archive], { type: writer.mimeTypes[0] });
|
|
1897
|
+
}
|
|
1898
|
+
async function* encodeTileConversionArchiveInBatches(files, options) {
|
|
1899
|
+
const { resources, format, maxArchiveBytes, signal } = prepareArchiveInput(files, options);
|
|
1900
|
+
const encode = format === "3tz" ? encodeTiles3DArchiveInBatches : encodeSLPKArchiveInBatches;
|
|
1901
|
+
yield* encode(resources, { [format]: { maxArchiveBytes, signal } });
|
|
1902
|
+
}
|
|
1903
|
+
function prepareArchiveInput(files, options) {
|
|
1904
|
+
const { format, maxArchiveBytes, signal } = options;
|
|
1905
|
+
if (!Number.isSafeInteger(maxArchiveBytes) || maxArchiveBytes < 0) {
|
|
1906
|
+
throw new TileConversionError10(
|
|
1907
|
+
"INVALID_ARCHIVE_BYTE_LIMIT",
|
|
1908
|
+
"Archive byte limit must be a non-negative safe integer"
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
if (format !== "3tz" && format !== "slpk") {
|
|
1912
|
+
throw new TileConversionError10("INVALID_ARCHIVE_FORMAT", "Expected 3tz or slpk archive format");
|
|
1913
|
+
}
|
|
1914
|
+
const resources = Object.fromEntries(files.map((file) => [file.resourceId, file.blob]));
|
|
1915
|
+
if (Object.keys(resources).length !== files.length) {
|
|
1916
|
+
throw new TileConversionError10(
|
|
1917
|
+
"DUPLICATE_ARCHIVE_RESOURCE",
|
|
1918
|
+
"Archive resource identifiers must be unique"
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
return { resources, format, maxArchiveBytes, signal };
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// src/v5/mesh-tileset-archive.ts
|
|
1925
|
+
import { Tiles3DArchiveWriter as Tiles3DArchiveWriter2 } from "@loaders.gl/3d-tiles/3d-tiles-archive-writer";
|
|
1926
|
+
import { TileConversionError as TileConversionError11 } from "@loaders.gl/tile-converter/v5/core";
|
|
1927
|
+
async function createSingleMeshTilesetArchive(files, options) {
|
|
1928
|
+
const { maxArchiveBytes, signal } = options;
|
|
1929
|
+
if (!Number.isSafeInteger(maxArchiveBytes) || maxArchiveBytes < 0) {
|
|
1930
|
+
throw new TileConversionError11(
|
|
1931
|
+
"INVALID_ARCHIVE_BYTE_LIMIT",
|
|
1932
|
+
"Archive byte limit must be a non-negative safe integer"
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
if (files.length !== 2 || !files.some((file) => file.resourceId === "mesh.glb") || !files.some((file) => file.resourceId === "tileset.json")) {
|
|
1936
|
+
throw new TileConversionError11(
|
|
1937
|
+
"INVALID_SINGLE_MESH_ARCHIVE",
|
|
1938
|
+
"Expected mesh.glb and tileset.json from a finalized single-mesh sink"
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
const resources = Object.fromEntries(files.map((file) => [file.resourceId, file.blob]));
|
|
1942
|
+
const archive = await Tiles3DArchiveWriter2.encode(resources, { "3tz": { maxArchiveBytes, signal } });
|
|
1943
|
+
return new Blob([archive], { type: Tiles3DArchiveWriter2.mimeTypes[0] });
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// src/v5/mesh-source.ts
|
|
1947
|
+
import { Matrix4 as Matrix42 } from "@math.gl/core";
|
|
1948
|
+
import { TileConversionError as TileConversionError14 } from "@loaders.gl/tile-converter/v5/core";
|
|
1949
|
+
import { createTilesetConversionSource } from "@loaders.gl/tile-converter/v5/core";
|
|
1950
|
+
|
|
1951
|
+
// src/v5/mesh-source-texture.ts
|
|
1952
|
+
import { getBinaryImageMetadata as getBinaryImageMetadata2 } from "@loaders.gl/images";
|
|
1953
|
+
import { TileConversionError as TileConversionError12 } from "@loaders.gl/tile-converter/v5/core";
|
|
1954
|
+
function mapMeshSourceTexture(textureInfo) {
|
|
1955
|
+
const texture = textureInfo?.texture;
|
|
1956
|
+
const image = texture?.source;
|
|
1957
|
+
const sampler = texture?.sampler;
|
|
1958
|
+
const extensions = textureInfo?.extensions || {};
|
|
1959
|
+
const hasTransform = Object.hasOwn(extensions, "KHR_texture_transform");
|
|
1960
|
+
const transform = extensions.KHR_texture_transform;
|
|
1961
|
+
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(
|
|
1962
|
+
(name) => !["offset", "rotation", "scale", "texCoord", "extensions", "extras"].includes(name)
|
|
1963
|
+
)) || Object.keys(textureInfo).some(
|
|
1964
|
+
(name) => !["id", "index", "texture", "texCoord", "extensions", "extras"].includes(name)
|
|
1965
|
+
) || !texture || Object.keys(texture).some(
|
|
1966
|
+
(name) => !["id", "source", "sampler", "name", "extensions", "extras"].includes(name)
|
|
1967
|
+
) || 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(
|
|
1968
|
+
(name) => ![
|
|
1969
|
+
"id",
|
|
1970
|
+
"parameters",
|
|
1971
|
+
"wrapS",
|
|
1972
|
+
"wrapT",
|
|
1973
|
+
"magFilter",
|
|
1974
|
+
"minFilter",
|
|
1975
|
+
"name",
|
|
1976
|
+
"extensions",
|
|
1977
|
+
"extras"
|
|
1978
|
+
].includes(name)
|
|
1979
|
+
))) {
|
|
1980
|
+
throw new TileConversionError12(
|
|
1981
|
+
"MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
1982
|
+
"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"
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
const selectedSampler = sampler ? {
|
|
1986
|
+
wrapS: sampler.wrapS,
|
|
1987
|
+
wrapT: sampler.wrapT,
|
|
1988
|
+
magFilter: sampler.magFilter,
|
|
1989
|
+
minFilter: sampler.minFilter
|
|
1990
|
+
} : void 0;
|
|
1991
|
+
return {
|
|
1992
|
+
...mapMeshSourceImage(image),
|
|
1993
|
+
...hasTransform ? {
|
|
1994
|
+
transform: {
|
|
1995
|
+
offset: transform.offset,
|
|
1996
|
+
rotation: transform.rotation,
|
|
1997
|
+
scale: transform.scale
|
|
1998
|
+
}
|
|
1999
|
+
} : {},
|
|
2000
|
+
...selectedSampler && Object.values(selectedSampler).some((value) => value !== void 0) ? { sampler: selectedSampler } : {}
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
function mapMeshSourceImage(image) {
|
|
2004
|
+
if (image.uri === void 0) {
|
|
2005
|
+
return { data: image.bufferView.data, mimeType: image.mimeType };
|
|
2006
|
+
}
|
|
2007
|
+
const match = typeof image.uri === "string" ? /^data:(image\/(?:png|jpeg));base64,([A-Za-z0-9+/]+={0,2})$/.exec(image.uri) : null;
|
|
2008
|
+
if (!match || image.mimeType !== void 0 && image.mimeType !== match[1]) {
|
|
2009
|
+
throw new TileConversionError12(
|
|
2010
|
+
"MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
2011
|
+
"Inline images require a PNG/JPEG base64 data URI and matching optional mimeType"
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
let encodedBytes;
|
|
2015
|
+
try {
|
|
2016
|
+
encodedBytes = atob(match[2]);
|
|
2017
|
+
} catch {
|
|
2018
|
+
throw new TileConversionError12("MESH_SOURCE_TEXTURE_UNSUPPORTED", "Invalid inline image base64");
|
|
2019
|
+
}
|
|
2020
|
+
return {
|
|
2021
|
+
data: Uint8Array.from(encodedBytes, (character) => character.charCodeAt(0)),
|
|
2022
|
+
mimeType: match[1]
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
async function resolveMeshSourceTexture(textureInfo, readExternalResource, contentUri, signal) {
|
|
2026
|
+
const image = textureInfo?.texture?.source;
|
|
2027
|
+
if (!image || image.uri === void 0 || image.uri.startsWith("data:"))
|
|
2028
|
+
return mapMeshSourceTexture(textureInfo);
|
|
2029
|
+
if (!readExternalResource || image.bufferView !== void 0)
|
|
2030
|
+
throw new TileConversionError12(
|
|
2031
|
+
"MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
2032
|
+
"External images require an explicit bounded resource reader"
|
|
2033
|
+
);
|
|
2034
|
+
signal?.throwIfAborted();
|
|
2035
|
+
const data = await readExternalResource(image.uri, contentUri, signal);
|
|
2036
|
+
signal?.throwIfAborted();
|
|
2037
|
+
let metadata;
|
|
2038
|
+
try {
|
|
2039
|
+
metadata = getBinaryImageMetadata2(new DataView(data.buffer, data.byteOffset, data.byteLength));
|
|
2040
|
+
} catch {
|
|
2041
|
+
}
|
|
2042
|
+
if (!metadata || !["image/png", "image/jpeg"].includes(metadata.mimeType) || image.mimeType !== void 0 && image.mimeType !== metadata.mimeType)
|
|
2043
|
+
throw new TileConversionError12(
|
|
2044
|
+
"MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
2045
|
+
"External images require matching PNG/JPEG headers"
|
|
2046
|
+
);
|
|
2047
|
+
const resolved = {
|
|
2048
|
+
...textureInfo,
|
|
2049
|
+
texture: {
|
|
2050
|
+
...textureInfo.texture,
|
|
2051
|
+
source: { ...image, uri: void 0, mimeType: metadata.mimeType, bufferView: { data } }
|
|
2052
|
+
}
|
|
2053
|
+
};
|
|
2054
|
+
return mapMeshSourceTexture(resolved);
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
// src/v5/mesh-source-features.ts
|
|
2058
|
+
import { TileConversionError as TileConversionError13 } from "@loaders.gl/tile-converter/v5/core";
|
|
2059
|
+
function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
|
|
2060
|
+
const declarations = primitive.extensions?.EXT_mesh_features?.featureIds;
|
|
2061
|
+
const metadata = gltf.extensions?.EXT_structural_metadata;
|
|
2062
|
+
const legacy = primitive.attributes._BATCHID;
|
|
2063
|
+
if (!declarations && !legacy && !metadata && !Object.keys(payload.batchTableJson || {}).length && !payload.header?.batchLength) {
|
|
2064
|
+
if (Object.keys(primitive.attributes).some((name) => /^_FEATURE_ID_\d+$/.test(name)))
|
|
2065
|
+
throw new TileConversionError13(
|
|
2066
|
+
"MESH_FEATURE_MAPPING_REQUIRED",
|
|
2067
|
+
"Feature attributes require an EXT_mesh_features declaration"
|
|
2068
|
+
);
|
|
2069
|
+
return void 0;
|
|
2070
|
+
}
|
|
2071
|
+
if (!options)
|
|
2072
|
+
throw new TileConversionError13(
|
|
2073
|
+
"MESH_FEATURE_SCHEMA_REQUIRED",
|
|
2074
|
+
"Feature extraction requires an explicit Arrow schema and class mapping"
|
|
2075
|
+
);
|
|
2076
|
+
if (!options.metadataClass)
|
|
2077
|
+
throw new TileConversionError13(
|
|
2078
|
+
"MESH_FEATURE_CLASS_REQUIRED",
|
|
2079
|
+
"Feature metadata class must be explicit"
|
|
2080
|
+
);
|
|
2081
|
+
let count;
|
|
2082
|
+
let attribute = legacy;
|
|
2083
|
+
let properties;
|
|
2084
|
+
if (declarations) {
|
|
2085
|
+
if (legacy || declarations.length !== 1 || metadata?.propertyTables?.length !== 1 || metadata.propertyTextures?.length || metadata.propertyAttributes?.length)
|
|
2086
|
+
throw new TileConversionError13(
|
|
2087
|
+
"MESH_FEATURE_MAPPING_UNSUPPORTED",
|
|
2088
|
+
"Only one attribute-backed feature set and one property table are supported"
|
|
2089
|
+
);
|
|
2090
|
+
const declaration = declarations[0];
|
|
2091
|
+
if (!Number.isInteger(declaration.attribute) || declaration.attribute < 0 || declaration.propertyTable !== 0 || declaration.texture || declaration.implicit || declaration.nullFeatureId !== void 0)
|
|
2092
|
+
throw new TileConversionError13(
|
|
2093
|
+
"MESH_FEATURE_MAPPING_UNSUPPORTED",
|
|
2094
|
+
"Features require an attribute and property table 0 without null/texture/implicit IDs"
|
|
2095
|
+
);
|
|
2096
|
+
attribute = primitive.attributes[`_FEATURE_ID_${declaration.attribute}`];
|
|
2097
|
+
const table = metadata.propertyTables[0];
|
|
2098
|
+
if (table.class !== options.metadataClass || table.count !== declaration.featureCount)
|
|
2099
|
+
throw new TileConversionError13(
|
|
2100
|
+
"MESH_FEATURE_CLASS_MISMATCH",
|
|
2101
|
+
"Feature class and declared table counts must agree"
|
|
2102
|
+
);
|
|
2103
|
+
count = table.count;
|
|
2104
|
+
properties = {};
|
|
2105
|
+
const classProperties = metadata.schema?.classes?.[table.class]?.properties;
|
|
2106
|
+
if (!classProperties)
|
|
2107
|
+
throw new TileConversionError13(
|
|
2108
|
+
"MESH_FEATURE_SCHEMA_REQUIRED",
|
|
2109
|
+
"A decoded inline structural metadata class is required"
|
|
2110
|
+
);
|
|
2111
|
+
if (Object.keys(table.properties || {}).some((name) => !Object.hasOwn(classProperties, name)))
|
|
2112
|
+
throw new TileConversionError13(
|
|
2113
|
+
"MESH_FEATURE_PROPERTY_UNMAPPED",
|
|
2114
|
+
"Property table columns must be declared in the metadata class"
|
|
2115
|
+
);
|
|
2116
|
+
for (const [name, property] of Object.entries(classProperties)) {
|
|
2117
|
+
const column = table.properties?.[name];
|
|
2118
|
+
if (!["SCALAR", "STRING"].includes(property.type) || property.array || property.noData !== void 0 && (property.normalized || property.offset !== void 0 || property.scale !== void 0 || column?.offset !== void 0 || column?.scale !== 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))
|
|
2119
|
+
throw new TileConversionError13(
|
|
2120
|
+
"MESH_FEATURE_PROPERTY_UNSUPPORTED",
|
|
2121
|
+
`Property ${name} requires a separately qualified semantic mapping`
|
|
2122
|
+
);
|
|
2123
|
+
const data = column?.data;
|
|
2124
|
+
if (data === void 0 && property.default === void 0)
|
|
2125
|
+
throw new TileConversionError13(
|
|
2126
|
+
"MESH_FEATURE_PROPERTY_UNAVAILABLE",
|
|
2127
|
+
`Property ${name} has not been decoded`
|
|
2128
|
+
);
|
|
2129
|
+
const decoded = data ?? Array.from({ length: count }, () => property.default);
|
|
2130
|
+
if (property.noData === void 0 || data === void 0) {
|
|
2131
|
+
properties[name] = decoded;
|
|
2132
|
+
continue;
|
|
2133
|
+
}
|
|
2134
|
+
const sentinel = property.noData;
|
|
2135
|
+
const integer64 = ["INT64", "UINT64"].includes(property.componentType || "");
|
|
2136
|
+
if ((property.type === "STRING" ? typeof sentinel !== "string" : typeof sentinel !== "number" || !Number.isFinite(sentinel) || integer64 && !Number.isSafeInteger(sentinel)) || !decoded || !Array.isArray(decoded) && !ArrayBuffer.isView(decoded) || decoded.length !== count)
|
|
2137
|
+
throw new TileConversionError13(
|
|
2138
|
+
"MESH_FEATURE_PROPERTY_UNSUPPORTED",
|
|
2139
|
+
`Property ${name} requires a valid decoded noData mapping`
|
|
2140
|
+
);
|
|
2141
|
+
const comparable = integer64 ? BigInt(sentinel) : sentinel;
|
|
2142
|
+
properties[name] = Array.from(
|
|
2143
|
+
decoded,
|
|
2144
|
+
(value) => value === comparable ? property.default ?? null : value
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
} else {
|
|
2148
|
+
if (metadata)
|
|
2149
|
+
throw new TileConversionError13(
|
|
2150
|
+
"MESH_FEATURE_MAPPING_UNSUPPORTED",
|
|
2151
|
+
"Structural metadata requires an attribute-backed feature declaration"
|
|
2152
|
+
);
|
|
2153
|
+
count = payload.header?.batchLength ?? Number(payload.featureTableJson?.BATCH_LENGTH);
|
|
2154
|
+
properties = payload.batchTableJson || {};
|
|
2155
|
+
}
|
|
2156
|
+
const vertexCount = mesh.attributes.POSITION.value.length / 3;
|
|
2157
|
+
if (!Number.isSafeInteger(count) || count <= 0 || !attribute || attribute.components !== 1 || attribute.normalized || attribute.value.length !== vertexCount || Array.from(attribute.value).some(
|
|
2158
|
+
(value) => !Number.isSafeInteger(value) || value < 0 || value >= count
|
|
2159
|
+
))
|
|
2160
|
+
throw new TileConversionError13(
|
|
2161
|
+
"MESH_FEATURE_IDS_INVALID",
|
|
2162
|
+
"Feature IDs must be one valid property-table row index per vertex"
|
|
2163
|
+
);
|
|
2164
|
+
const selectedAttribute = declarations ? `_FEATURE_ID_${declarations[0].attribute}` : "_BATCHID";
|
|
2165
|
+
if (Object.keys(primitive.attributes).some(
|
|
2166
|
+
(name) => (name === "_BATCHID" || /^_FEATURE_ID_\d+$/.test(name)) && name !== selectedAttribute
|
|
2167
|
+
))
|
|
2168
|
+
throw new TileConversionError13(
|
|
2169
|
+
"MESH_FEATURE_MAPPING_UNSUPPORTED",
|
|
2170
|
+
"Additional feature ID sets require separate mappings"
|
|
2171
|
+
);
|
|
2172
|
+
for (const [name, values] of Object.entries(properties)) {
|
|
2173
|
+
if (!values || !Array.isArray(values) && !ArrayBuffer.isView(values) || values.length !== count)
|
|
2174
|
+
throw new TileConversionError13(
|
|
2175
|
+
"MESH_FEATURE_PROPERTY_UNAVAILABLE",
|
|
2176
|
+
`Property ${name} must be a decoded column matching the table count`
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
2179
|
+
const sourceIdentifier = options.sourceFeatureIdProperty;
|
|
2180
|
+
if (sourceIdentifier && !properties[sourceIdentifier])
|
|
2181
|
+
throw new TileConversionError13(
|
|
2182
|
+
"MESH_FEATURE_ID_PROPERTY_REQUIRED",
|
|
2183
|
+
"Stable source identifier property is missing"
|
|
2184
|
+
);
|
|
2185
|
+
const batches = convertFeatureAttributesToArrowBatches(
|
|
2186
|
+
Array.from({ length: count }, (_, index) => ({
|
|
2187
|
+
featureId: sourceIdentifier ? properties[sourceIdentifier][index] : index,
|
|
2188
|
+
metadataClass: options.metadataClass,
|
|
2189
|
+
properties: Object.fromEntries(
|
|
2190
|
+
Object.entries(properties).filter(([name]) => name !== sourceIdentifier).map(([name, values]) => [name, values[index]])
|
|
2191
|
+
)
|
|
2192
|
+
})),
|
|
2193
|
+
options
|
|
2194
|
+
);
|
|
2195
|
+
const indices = mesh.indices?.value;
|
|
2196
|
+
const triangleFeatureIndices = new Uint32Array((indices?.length ?? vertexCount) / 3);
|
|
2197
|
+
for (let triangle = 0; triangle < triangleFeatureIndices.length; triangle++) {
|
|
2198
|
+
const corners = [0, 1, 2].map(
|
|
2199
|
+
(corner) => indices?.[triangle * 3 + corner] ?? triangle * 3 + corner
|
|
2200
|
+
);
|
|
2201
|
+
const rows = corners.map((corner) => attribute.value[corner]);
|
|
2202
|
+
if (rows.some((row) => row !== rows[0]) && new Set(corners).size === 3)
|
|
2203
|
+
throw new TileConversionError13(
|
|
2204
|
+
"MESH_FEATURE_TRIANGLE_MIXED",
|
|
2205
|
+
"All vertices of a nondegenerate triangle must reference the same feature row"
|
|
2206
|
+
);
|
|
2207
|
+
triangleFeatureIndices[triangle] = Number(rows[0]);
|
|
2208
|
+
}
|
|
2209
|
+
return {
|
|
2210
|
+
batches,
|
|
2211
|
+
triangleFeatureIndices,
|
|
2212
|
+
featureIdField: options.featureIdField || "feature_id",
|
|
2213
|
+
integer64Encoding: options.integer64Encoding
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
// src/v5/mesh-source.ts
|
|
2218
|
+
function createMeshTilesetConversionSource(tileset, options = {}) {
|
|
2219
|
+
const source = createTilesetConversionSource(tileset, options);
|
|
2220
|
+
return {
|
|
2221
|
+
/** Validates the source frame and supported dataset metadata before traversal. */
|
|
2222
|
+
async inspect(signal) {
|
|
2223
|
+
const metadata = await source.inspect(signal);
|
|
2224
|
+
const reference = metadata.spatialReference;
|
|
2225
|
+
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")) {
|
|
2226
|
+
throw new TileConversionError14(
|
|
2227
|
+
"MESH_SOURCE_FRAME_UNSUPPORTED",
|
|
2228
|
+
"Mesh extraction requires native EPSG:4978 xyz coordinates in meters and ellipsoidal heights"
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2231
|
+
if (metadata.tileset.metadata || metadata.tileset.groups?.length) {
|
|
2232
|
+
throw new TileConversionError14(
|
|
2233
|
+
"MESH_SOURCE_METADATA_UNSUPPORTED",
|
|
2234
|
+
"Tileset/group metadata requires a separate mapping"
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
return metadata;
|
|
2238
|
+
},
|
|
2239
|
+
/** Yields each primitive placement while the shared traversal owns content lifetime. */
|
|
2240
|
+
async *read(inspection, signal) {
|
|
2241
|
+
for await (const item of source.read(inspection, signal)) {
|
|
2242
|
+
const placementReference = item.tile.header._spatialReference || tileset.spatialReference;
|
|
2243
|
+
if (placementReference && (placementReference.status !== "native" || placementReference.sourceCrs !== "EPSG:4978"))
|
|
2244
|
+
throw new TileConversionError14(
|
|
2245
|
+
"MESH_SOURCE_FRAME_UNSUPPORTED",
|
|
2246
|
+
"Nested mesh placements require native ECEF coordinates"
|
|
2247
|
+
);
|
|
2248
|
+
if (item.tile.header.metadata || item.tile.header.implicitMetadata)
|
|
2249
|
+
throw new TileConversionError14(
|
|
2250
|
+
"MESH_SOURCE_METADATA_UNSUPPORTED",
|
|
2251
|
+
"Tile metadata requires a separate mapping"
|
|
2252
|
+
);
|
|
2253
|
+
for (const content of item.contents) {
|
|
2254
|
+
signal?.throwIfAborted();
|
|
2255
|
+
if (content.metadata || content.group !== void 0)
|
|
2256
|
+
throw new TileConversionError14(
|
|
2257
|
+
"MESH_SOURCE_METADATA_UNSUPPORTED",
|
|
2258
|
+
"Content metadata requires a separate mapping"
|
|
2259
|
+
);
|
|
2260
|
+
const payload = content.payload;
|
|
2261
|
+
if (!payload?.gltf || !["glTF", "gltf", "b3dm"].includes(payload.type || content.type || "")) {
|
|
2262
|
+
throw new TileConversionError14(
|
|
2263
|
+
"MESH_SOURCE_CONTENT_UNSUPPORTED",
|
|
2264
|
+
"Mesh extraction requires decoded GLB/glTF or B3DM content"
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2267
|
+
const gltf = payload.gltf;
|
|
2268
|
+
if (gltf.animations?.length || gltf.skins?.length || Object.keys(gltf.extensions || {}).some(
|
|
2269
|
+
(name) => !["EXT_structural_metadata", "CESIUM_RTC"].includes(name)
|
|
2270
|
+
)) {
|
|
2271
|
+
throw new TileConversionError14(
|
|
2272
|
+
"MESH_SOURCE_SCENE_UNSUPPORTED",
|
|
2273
|
+
"Animated/skinned scenes and unmapped scene extensions are unsupported"
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2276
|
+
const axis = payload.gltfUpAxis || "Y";
|
|
2277
|
+
if (!["X", "Y", "Z"].includes(axis))
|
|
2278
|
+
throw new TileConversionError14("MESH_SOURCE_AXIS_UNSUPPORTED", "Unknown glTF up axis");
|
|
2279
|
+
const placement = new Matrix42(item.tile.computedTransform);
|
|
2280
|
+
const rtc = payload.rtcCenter || gltf.extensions?.CESIUM_RTC?.center;
|
|
2281
|
+
if (rtc) {
|
|
2282
|
+
if (rtc.length !== 3 || rtc.some((value) => !Number.isFinite(value)))
|
|
2283
|
+
throw new TileConversionError14(
|
|
2284
|
+
"MESH_SOURCE_TRANSFORM_INVALID",
|
|
2285
|
+
"RTC center must contain finite xyz coordinates"
|
|
2286
|
+
);
|
|
2287
|
+
placement.translate(rtc);
|
|
2288
|
+
}
|
|
2289
|
+
if (axis === "Y")
|
|
2290
|
+
placement.multiplyRight(new Matrix42([1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1]));
|
|
2291
|
+
if (axis === "X")
|
|
2292
|
+
placement.multiplyRight(new Matrix42([0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 1]));
|
|
2293
|
+
const roots = gltf.scene?.nodes || (gltf.scenes?.length === 1 ? gltf.scenes[0].nodes : void 0);
|
|
2294
|
+
if (!roots?.length)
|
|
2295
|
+
throw new TileConversionError14(
|
|
2296
|
+
"MESH_SOURCE_SCENE_UNSUPPORTED",
|
|
2297
|
+
"A default scene or exactly one scene is required"
|
|
2298
|
+
);
|
|
2299
|
+
let primitiveIndex = 0;
|
|
2300
|
+
for (const selected of traverseMeshNodes(roots, placement)) {
|
|
2301
|
+
const resource = await extractPrimitive(
|
|
2302
|
+
selected.primitive,
|
|
2303
|
+
selected.transform,
|
|
2304
|
+
`${item.tile.id}/${content.index}/${primitiveIndex++}`,
|
|
2305
|
+
options.readExternalResource,
|
|
2306
|
+
content.uri,
|
|
2307
|
+
signal
|
|
2308
|
+
);
|
|
2309
|
+
const features = extractMeshFeatures(
|
|
2310
|
+
gltf,
|
|
2311
|
+
selected.primitive,
|
|
2312
|
+
payload,
|
|
2313
|
+
resource.mesh,
|
|
2314
|
+
options.features
|
|
2315
|
+
);
|
|
2316
|
+
yield { ...resource, features };
|
|
2317
|
+
}
|
|
2318
|
+
if (!primitiveIndex)
|
|
2319
|
+
throw new TileConversionError14(
|
|
2320
|
+
"MESH_SOURCE_CONTENT_EMPTY",
|
|
2321
|
+
"Declared content contains no supported mesh primitive"
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
function* traverseMeshNodes(nodes, parent, ancestors = /* @__PURE__ */ new Set()) {
|
|
2329
|
+
for (const node of nodes) {
|
|
2330
|
+
if (ancestors.has(node) || node.skin || node.weights || Object.keys(node.extensions || {}).length)
|
|
2331
|
+
throw new TileConversionError14(
|
|
2332
|
+
"MESH_SOURCE_NODE_UNSUPPORTED",
|
|
2333
|
+
"Cyclic, skinned, morphed, or extended nodes are unsupported"
|
|
2334
|
+
);
|
|
2335
|
+
const transform = new Matrix42(parent).multiplyRight(
|
|
2336
|
+
node.matrix ? new Matrix42(node.matrix) : new Matrix42().translate(node.translation || [0, 0, 0]).multiplyRight(new Matrix42().fromQuaternion(node.rotation || [0, 0, 0, 1])).scale(node.scale || [1, 1, 1])
|
|
2337
|
+
);
|
|
2338
|
+
if (node.mesh) {
|
|
2339
|
+
if (node.mesh.weights || Object.keys(node.mesh.extensions || {}).length)
|
|
2340
|
+
throw new TileConversionError14(
|
|
2341
|
+
"MESH_SOURCE_NODE_UNSUPPORTED",
|
|
2342
|
+
"Morph weights and mesh extensions are unsupported"
|
|
2343
|
+
);
|
|
2344
|
+
for (const primitive of node.mesh.primitives) yield { primitive, transform };
|
|
2345
|
+
}
|
|
2346
|
+
yield* traverseMeshNodes(node.children || [], transform, /* @__PURE__ */ new Set([...ancestors, node]));
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
async function extractPrimitive(primitive, transform, id, readExternalResource, contentUri, signal) {
|
|
2350
|
+
if (primitive.targets?.length || Object.keys(primitive.extensions || {}).some((name) => name !== "EXT_mesh_features"))
|
|
2351
|
+
throw new TileConversionError14(
|
|
2352
|
+
"MESH_SOURCE_PRIMITIVE_UNSUPPORTED",
|
|
2353
|
+
"Morph targets and unmapped primitive extensions are unsupported"
|
|
2354
|
+
);
|
|
2355
|
+
if (Array.from(transform).some((value) => !Number.isFinite(value)) || transform[3] !== 0 || transform[7] !== 0 || transform[11] !== 0 || transform[15] !== 1 || transform.determinant() <= 0)
|
|
2356
|
+
throw new TileConversionError14(
|
|
2357
|
+
"MESH_SOURCE_TRANSFORM_INVALID",
|
|
2358
|
+
"Mesh placement requires a finite invertible affine transform with positive determinant"
|
|
2359
|
+
);
|
|
2360
|
+
const attributes = {};
|
|
2361
|
+
for (const [name, accessor] of Object.entries(primitive.attributes)) {
|
|
2362
|
+
if (name === "_BATCHID" || /^_FEATURE_ID_\d+$/.test(name)) continue;
|
|
2363
|
+
if (!["POSITION", "NORMAL", "COLOR_0", "TEXCOORD_0"].includes(name))
|
|
2364
|
+
throw new TileConversionError14(
|
|
2365
|
+
"MESH_SOURCE_ATTRIBUTE_UNSUPPORTED",
|
|
2366
|
+
`Unsupported mesh attribute ${name}`
|
|
2367
|
+
);
|
|
2368
|
+
attributes[name] = {
|
|
2369
|
+
value: accessor.value,
|
|
2370
|
+
size: accessor.components,
|
|
2371
|
+
...accessor.normalized ? { normalized: true } : {}
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
const mesh = validateMeshGeometry(
|
|
2375
|
+
{
|
|
2376
|
+
topology: "triangle-list",
|
|
2377
|
+
mode: primitive.mode === 5 ? 4 : primitive.mode ?? 4,
|
|
2378
|
+
attributes,
|
|
2379
|
+
indices: createTriangleIndices(primitive, (attributes.POSITION?.value.length ?? 0) / 3)
|
|
2380
|
+
},
|
|
2381
|
+
true
|
|
2382
|
+
);
|
|
2383
|
+
const sourcePositions = mesh.attributes.POSITION.value;
|
|
2384
|
+
const positions = new Float64Array(sourcePositions.length);
|
|
2385
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
2386
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
2387
|
+
for (let index = 0; index < positions.length; index += 3) {
|
|
2388
|
+
const placed = transform.transformAsPoint(
|
|
2389
|
+
Array.from(sourcePositions.subarray(index, index + 3))
|
|
2390
|
+
);
|
|
2391
|
+
positions.set(placed, index);
|
|
2392
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
2393
|
+
minimum[axis] = Math.min(minimum[axis], placed[axis]);
|
|
2394
|
+
maximum[axis] = Math.max(maximum[axis], placed[axis]);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
mesh.attributes.POSITION = { value: positions, size: 3 };
|
|
2398
|
+
if (mesh.attributes.NORMAL) {
|
|
2399
|
+
const values = mesh.attributes.NORMAL.value;
|
|
2400
|
+
const normalMatrix = new Matrix42(transform).invert().transpose();
|
|
2401
|
+
const normals = new Float32Array(values.length);
|
|
2402
|
+
for (let index = 0; index < values.length; index += 3) {
|
|
2403
|
+
const normal = normalMatrix.transformAsVector(Array.from(values.subarray(index, index + 3)));
|
|
2404
|
+
const length = Math.hypot(...normal);
|
|
2405
|
+
normals.set(
|
|
2406
|
+
normal.map((value) => value / length),
|
|
2407
|
+
index
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
mesh.attributes.NORMAL = { value: normals, size: 3 };
|
|
2411
|
+
}
|
|
2412
|
+
return {
|
|
2413
|
+
id,
|
|
2414
|
+
mesh,
|
|
2415
|
+
origin: minimum.map((value, axis) => value / 2 + maximum[axis] / 2),
|
|
2416
|
+
material: await mapMeshSourceMaterial(
|
|
2417
|
+
primitive.material,
|
|
2418
|
+
readExternalResource,
|
|
2419
|
+
contentUri,
|
|
2420
|
+
signal
|
|
2421
|
+
)
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
async function mapMeshSourceMaterial(material, readExternalResource, contentUri, signal) {
|
|
2425
|
+
if (!material) return void 0;
|
|
2426
|
+
const pbr = material.pbrMetallicRoughness;
|
|
2427
|
+
if (Object.entries(material).some(
|
|
2428
|
+
([name, value]) => value !== void 0 && ![
|
|
2429
|
+
"id",
|
|
2430
|
+
"name",
|
|
2431
|
+
"pbrMetallicRoughness",
|
|
2432
|
+
"alphaMode",
|
|
2433
|
+
"alphaCutoff",
|
|
2434
|
+
"doubleSided",
|
|
2435
|
+
"emissiveFactor",
|
|
2436
|
+
"extras"
|
|
2437
|
+
].includes(name)
|
|
2438
|
+
) || Object.entries(pbr || {}).some(
|
|
2439
|
+
([name, value]) => value !== void 0 && !["baseColorFactor", "baseColorTexture", "metallicFactor", "roughnessFactor"].includes(name)
|
|
2440
|
+
) || material.emissiveFactor?.some((value) => value !== 0))
|
|
2441
|
+
throw new TileConversionError14(
|
|
2442
|
+
"MESH_SOURCE_MATERIAL_UNSUPPORTED",
|
|
2443
|
+
"Only metallic-roughness factors and one embedded base-color image are supported"
|
|
2444
|
+
);
|
|
2445
|
+
return {
|
|
2446
|
+
baseColorFactor: pbr?.baseColorFactor,
|
|
2447
|
+
..."baseColorTexture" in (pbr || {}) ? {
|
|
2448
|
+
baseColorTexture: await resolveMeshSourceTexture(
|
|
2449
|
+
pbr.baseColorTexture,
|
|
2450
|
+
readExternalResource,
|
|
2451
|
+
contentUri,
|
|
2452
|
+
signal
|
|
2453
|
+
)
|
|
2454
|
+
} : {},
|
|
2455
|
+
metallicFactor: pbr?.metallicFactor,
|
|
2456
|
+
roughnessFactor: pbr?.roughnessFactor,
|
|
2457
|
+
alphaMode: material.alphaMode,
|
|
2458
|
+
...material.alphaMode === "MASK" ? { alphaCutoff: material.alphaCutoff ?? 0.5 } : {},
|
|
2459
|
+
doubleSided: material.doubleSided
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
function createTriangleIndices(primitive, vertexCount) {
|
|
2463
|
+
const attribute = primitive.indices ? {
|
|
2464
|
+
value: primitive.indices.value,
|
|
2465
|
+
size: primitive.indices.components,
|
|
2466
|
+
normalized: primitive.indices.normalized
|
|
2467
|
+
} : void 0;
|
|
2468
|
+
if (primitive.mode !== 5) return attribute;
|
|
2469
|
+
const sourceIndices = validateMeshIndices(attribute, vertexCount, 5);
|
|
2470
|
+
const indexCount = sourceIndices?.length ?? vertexCount;
|
|
2471
|
+
if (!Number.isSafeInteger(indexCount) || indexCount < 3) {
|
|
2472
|
+
throw new TileConversionError14(
|
|
2473
|
+
"MESH_TRIANGLE_COUNT_INVALID",
|
|
2474
|
+
"Triangle strips require at least three vertices"
|
|
2475
|
+
);
|
|
2476
|
+
}
|
|
2477
|
+
const IndexArray = vertexCount < 65536 ? Uint16Array : Uint32Array;
|
|
2478
|
+
const indices = new IndexArray((indexCount - 2) * 3);
|
|
2479
|
+
for (let triangle = 0; triangle < indexCount - 2; triangle++) {
|
|
2480
|
+
const second = triangle + 1 + triangle % 2;
|
|
2481
|
+
const third = triangle + 2 - triangle % 2;
|
|
2482
|
+
indices[triangle * 3] = sourceIndices?.[triangle] ?? triangle;
|
|
2483
|
+
indices[triangle * 3 + 1] = sourceIndices?.[second] ?? second;
|
|
2484
|
+
indices[triangle * 3 + 2] = sourceIndices?.[third] ?? third;
|
|
2485
|
+
}
|
|
2486
|
+
return { value: indices, size: 1 };
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
// src/v5/i3s-mesh-conversion.ts
|
|
2490
|
+
import { encodeI3SMeshLayer, encodeI3SMeshLayerWithDraco } from "@loaders.gl/i3s";
|
|
2491
|
+
import { TileConversionError as TileConversionError15 } from "@loaders.gl/tile-converter/v5/core";
|
|
2492
|
+
import { createBoundedMemoryTileConversionSink as createBoundedMemoryTileConversionSink2 } from "@loaders.gl/tile-converter/v5/core";
|
|
2493
|
+
function createI3SMeshConversionCodec(options) {
|
|
2494
|
+
const reference = options.spatialContext.spatialReference;
|
|
2495
|
+
const height = reference.targetHeightReference === "native" ? reference.heightReference : reference.targetHeightReference;
|
|
2496
|
+
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")))
|
|
2497
|
+
throw new TileConversionError15(
|
|
2498
|
+
"I3S_MESH_FRAME_UNSUPPORTED",
|
|
2499
|
+
"I3S mesh authoring requires resolved ECEF coordinates and ellipsoidal heights"
|
|
2500
|
+
);
|
|
2501
|
+
let objectIdOffset = 0;
|
|
2502
|
+
return {
|
|
2503
|
+
/** Places a source mesh in ECEF and authors one layer without modifying source arrays. */
|
|
2504
|
+
async *convert(resource, _inspection, signal) {
|
|
2505
|
+
signal?.throwIfAborted();
|
|
2506
|
+
const mesh = validateMeshGeometry(resource.mesh, true);
|
|
2507
|
+
const positions = mesh.attributes.POSITION.value;
|
|
2508
|
+
const context = options.spatialContext;
|
|
2509
|
+
const transformed = "transformGeometryAsync" in context ? await context.transformGeometryAsync(
|
|
2510
|
+
positions,
|
|
2511
|
+
mesh.attributes.NORMAL?.value,
|
|
2512
|
+
resource.normalReferenceFrame ?? options.normalReferenceFrame
|
|
2513
|
+
) : {
|
|
2514
|
+
positions: context.transformPositions(positions),
|
|
2515
|
+
normals: mesh.attributes.NORMAL ? context.transformNormals(mesh.attributes.NORMAL.value, positions) : void 0
|
|
2516
|
+
};
|
|
2517
|
+
signal?.throwIfAborted();
|
|
2518
|
+
mesh.attributes.POSITION = { value: transformed.positions, size: 3 };
|
|
2519
|
+
if (mesh.attributes.NORMAL) mesh.attributes.NORMAL = { value: transformed.normals, size: 3 };
|
|
2520
|
+
let layer;
|
|
2521
|
+
try {
|
|
2522
|
+
const writerOptions = {
|
|
2523
|
+
...options,
|
|
2524
|
+
material: resource.material,
|
|
2525
|
+
features: resource.features,
|
|
2526
|
+
objectIdOffset
|
|
2527
|
+
};
|
|
2528
|
+
layer = options.draco === false ? encodeI3SMeshLayer(mesh, writerOptions) : await encodeI3SMeshLayerWithDraco(mesh, writerOptions, options.dracoLibraryOptions);
|
|
2529
|
+
} catch (error) {
|
|
2530
|
+
throw new TileConversionError15(
|
|
2531
|
+
"I3S_MESH_PROFILE_UNSUPPORTED",
|
|
2532
|
+
error instanceof Error ? error.message : String(error)
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
signal?.throwIfAborted();
|
|
2536
|
+
objectIdOffset += resource.features?.batches.reduce((total, batch) => total + batch.data.numRows, 0) ?? 1;
|
|
2537
|
+
yield { id: resource.id, ...layer };
|
|
2538
|
+
},
|
|
2539
|
+
/** Reports measured precision and explicitly authorized integer representation changes. */
|
|
2540
|
+
async validateOutput(resource) {
|
|
2541
|
+
return [
|
|
2542
|
+
...resource.maximumPositionError > 0 ? [
|
|
2543
|
+
{
|
|
2544
|
+
code: "I3S_MESH_POSITION_ROUNDING",
|
|
2545
|
+
message: `I3S positions have up to ${resource.maximumPositionError} meters reconstruction error`,
|
|
2546
|
+
severity: "info",
|
|
2547
|
+
resourceId: resource.id
|
|
2548
|
+
}
|
|
2549
|
+
] : [],
|
|
2550
|
+
...resource.decimalStringFields.map((field) => ({
|
|
2551
|
+
code: "I3S_INTEGER_DECIMAL_STRING",
|
|
2552
|
+
message: `Explicitly represented 64-bit integer field ${field} as exact decimal text`,
|
|
2553
|
+
severity: "info",
|
|
2554
|
+
resourceId: resource.id
|
|
2555
|
+
}))
|
|
2556
|
+
];
|
|
2557
|
+
}
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
function createSingleMeshI3SSink(options) {
|
|
2561
|
+
const memory = createBoundedMemoryTileConversionSink2(options);
|
|
2562
|
+
let state = "empty";
|
|
2563
|
+
let completed = false;
|
|
2564
|
+
return {
|
|
2565
|
+
/** Exposes resources only after successful finalization. */
|
|
2566
|
+
getFiles: () => completed ? memory.getFiles() : [],
|
|
2567
|
+
/** Writes exactly one layer within the retained-resource budget. */
|
|
2568
|
+
async write(resource, signal) {
|
|
2569
|
+
signal?.throwIfAborted();
|
|
2570
|
+
if (state !== "empty")
|
|
2571
|
+
throw new TileConversionError15(
|
|
2572
|
+
"SINGLE_I3S_MESH_UNAVAILABLE",
|
|
2573
|
+
"The initial I3S sink accepts exactly one mesh layer"
|
|
2574
|
+
);
|
|
2575
|
+
state = "writing";
|
|
2576
|
+
for (const [resourceId, bytes] of Object.entries(resource.files))
|
|
2577
|
+
await memory.write(
|
|
2578
|
+
{
|
|
2579
|
+
resourceId,
|
|
2580
|
+
parts: [bytes],
|
|
2581
|
+
contentType: resourceId.endsWith(".png") ? "image/png" : resourceId.endsWith(".jpg") ? "image/jpeg" : "application/gzip"
|
|
2582
|
+
},
|
|
2583
|
+
signal
|
|
2584
|
+
);
|
|
2585
|
+
state = "written";
|
|
2586
|
+
},
|
|
2587
|
+
/** Commits a complete single-layer result. */
|
|
2588
|
+
async finalize(report) {
|
|
2589
|
+
if (state !== "written")
|
|
2590
|
+
throw new TileConversionError15(
|
|
2591
|
+
"SINGLE_I3S_MESH_INCOMPLETE",
|
|
2592
|
+
"Finalization requires exactly one successfully written layer"
|
|
2593
|
+
);
|
|
2594
|
+
await memory.finalize(report);
|
|
2595
|
+
state = "closed";
|
|
2596
|
+
completed = true;
|
|
2597
|
+
},
|
|
2598
|
+
/** Discards all resources after any conversion failure. */
|
|
2599
|
+
async abort(error) {
|
|
2600
|
+
completed = false;
|
|
2601
|
+
state = "closed";
|
|
2602
|
+
await memory.abort(error);
|
|
2603
|
+
}
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
// src/v5/i3s-mesh-sink.ts
|
|
2608
|
+
import { Ellipsoid } from "@math.gl/geospatial";
|
|
2609
|
+
import { GZipDecompressor, GZipCompressor } from "@loaders.gl/compression";
|
|
2610
|
+
import {
|
|
2611
|
+
TileConversionError as TileConversionError16,
|
|
2612
|
+
createBoundedMemoryTileConversionSink as createBoundedMemoryTileConversionSink3
|
|
2613
|
+
} from "@loaders.gl/tile-converter/v5/core";
|
|
2614
|
+
function createI3SMeshSink(options) {
|
|
2615
|
+
const { maxMeshes, maxResourceBytes } = options;
|
|
2616
|
+
if (!Number.isInteger(maxMeshes) || maxMeshes < 1 || maxMeshes > 64 || !Number.isSafeInteger(maxResourceBytes) || maxResourceBytes < 1)
|
|
2617
|
+
throw new TileConversionError16(
|
|
2618
|
+
"I3S_COLLECTION_OPTIONS_INVALID",
|
|
2619
|
+
"I3S collections require 1\u201364 meshes and a positive resource byte limit"
|
|
2620
|
+
);
|
|
2621
|
+
const memory = createBoundedMemoryTileConversionSink3(options);
|
|
2622
|
+
const compressor = new GZipCompressor({ useNative: false, gzip: { mtime: 0 } });
|
|
2623
|
+
const decompressor = new GZipDecompressor({ useNative: false });
|
|
2624
|
+
const leaves = [];
|
|
2625
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
2626
|
+
const objectIdRanges = [];
|
|
2627
|
+
let layer;
|
|
2628
|
+
let schemaKey;
|
|
2629
|
+
let state = "open";
|
|
2630
|
+
let completed = false;
|
|
2631
|
+
async function readBytes(bytes) {
|
|
2632
|
+
const output = await decompressor.decompress(bytes);
|
|
2633
|
+
if (output.byteLength > maxResourceBytes)
|
|
2634
|
+
throw new TileConversionError16(
|
|
2635
|
+
"I3S_COLLECTION_RESOURCE_TOO_LARGE",
|
|
2636
|
+
"I3S metadata exceeds maxResourceBytes"
|
|
2637
|
+
);
|
|
2638
|
+
return output;
|
|
2639
|
+
}
|
|
2640
|
+
async function readJson(resource, name) {
|
|
2641
|
+
return JSON.parse(new TextDecoder().decode(await readBytes(resource.files[name])));
|
|
2642
|
+
}
|
|
2643
|
+
async function writeJson(resourceId, value) {
|
|
2644
|
+
const bytes = new TextEncoder().encode(JSON.stringify(value));
|
|
2645
|
+
if (bytes.byteLength > maxResourceBytes)
|
|
2646
|
+
throw new TileConversionError16(
|
|
2647
|
+
"I3S_COLLECTION_RESOURCE_TOO_LARGE",
|
|
2648
|
+
"I3S index exceeds maxResourceBytes"
|
|
2649
|
+
);
|
|
2650
|
+
await memory.write({
|
|
2651
|
+
resourceId,
|
|
2652
|
+
parts: [await compressor.compress(bytes.buffer)],
|
|
2653
|
+
contentType: "application/gzip"
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
return {
|
|
2657
|
+
/** Exposes the whole collection atomically. */
|
|
2658
|
+
getFiles: () => completed ? memory.getFiles() : [],
|
|
2659
|
+
/** Retains one leaf's binary resources and validates its shared schema and IDs. */
|
|
2660
|
+
async write(resource, signal) {
|
|
2661
|
+
signal?.throwIfAborted();
|
|
2662
|
+
if (state !== "open" || identifiers.has(resource.id) || leaves.length >= maxMeshes)
|
|
2663
|
+
throw new TileConversionError16(
|
|
2664
|
+
"I3S_COLLECTION_UNAVAILABLE",
|
|
2665
|
+
"I3S collection is busy, closed, full, or repeats a placement"
|
|
2666
|
+
);
|
|
2667
|
+
state = "writing";
|
|
2668
|
+
const authored = await readJson(resource, "3dSceneLayer.json.gz");
|
|
2669
|
+
const key = JSON.stringify([
|
|
2670
|
+
authored.fields,
|
|
2671
|
+
authored.attributeStorageInfo,
|
|
2672
|
+
authored.description,
|
|
2673
|
+
authored.store.defaultGeometrySchema
|
|
2674
|
+
]);
|
|
2675
|
+
if (schemaKey !== void 0 && key !== schemaKey)
|
|
2676
|
+
throw new TileConversionError16(
|
|
2677
|
+
"I3S_COLLECTION_SCHEMA_MISMATCH",
|
|
2678
|
+
"Mesh nodes must share feature fields and the legacy geometry layout"
|
|
2679
|
+
);
|
|
2680
|
+
const objectIds = new DataView(
|
|
2681
|
+
await readBytes(resource.files["nodes/1/attributes/f_0/0.bin.gz"])
|
|
2682
|
+
);
|
|
2683
|
+
const count = objectIds.getUint32(0, true);
|
|
2684
|
+
const first = objectIds.getUint32(4, true);
|
|
2685
|
+
if (objectIds.byteLength !== 4 + count * 4 || !count || objectIdRanges.some(([start, end]) => first < end && first + count > start) || Array.from({ length: count }, (_, index2) => objectIds.getUint32(4 + index2 * 4, true)).some(
|
|
2686
|
+
(value, index2) => value !== first + index2
|
|
2687
|
+
))
|
|
2688
|
+
throw new TileConversionError16(
|
|
2689
|
+
"I3S_COLLECTION_IDS_INVALID",
|
|
2690
|
+
"Generated object IDs must be contiguous and disjoint across nodes"
|
|
2691
|
+
);
|
|
2692
|
+
const index = leaves.length + 1;
|
|
2693
|
+
const node = (await readJson(resource, "nodepages/0.json.gz")).nodes[1];
|
|
2694
|
+
const legacy = await readJson(
|
|
2695
|
+
resource,
|
|
2696
|
+
"nodes/1/3dNodeIndexDocument.json.gz"
|
|
2697
|
+
);
|
|
2698
|
+
if (node.obb.quaternion.join(",") !== "0,0,0,1" || [...node.obb.center, ...node.obb.halfSize].some((value) => !Number.isFinite(value)) || node.obb.halfSize.some((value) => value < 0))
|
|
2699
|
+
throw new TileConversionError16(
|
|
2700
|
+
"I3S_COLLECTION_BOUNDS_INVALID",
|
|
2701
|
+
"Expected finite axis-aligned bounds from the I3S mesh writer"
|
|
2702
|
+
);
|
|
2703
|
+
if (!layer) {
|
|
2704
|
+
layer = structuredClone(authored);
|
|
2705
|
+
layer.materialDefinitions = [];
|
|
2706
|
+
layer.geometryDefinitions = [];
|
|
2707
|
+
layer.textureSetDefinitions = [];
|
|
2708
|
+
schemaKey = key;
|
|
2709
|
+
}
|
|
2710
|
+
const material = structuredClone(authored.materialDefinitions[0]);
|
|
2711
|
+
const texture = material.pbrMetallicRoughness?.baseColorTexture;
|
|
2712
|
+
if (texture) {
|
|
2713
|
+
texture.textureSetDefinitionId = layer.textureSetDefinitions.length;
|
|
2714
|
+
layer.textureSetDefinitions.push(authored.textureSetDefinitions[0]);
|
|
2715
|
+
node.mesh.material.resource = index;
|
|
2716
|
+
}
|
|
2717
|
+
node.index = index;
|
|
2718
|
+
node.parentIndex = 0;
|
|
2719
|
+
node.mesh.material.definition = layer.materialDefinitions.length;
|
|
2720
|
+
node.mesh.geometry.definition = layer.geometryDefinitions.length;
|
|
2721
|
+
node.mesh.geometry.resource = index;
|
|
2722
|
+
node.mesh.attribute.resource = index;
|
|
2723
|
+
layer.materialDefinitions.push(material);
|
|
2724
|
+
layer.geometryDefinitions.push(authored.geometryDefinitions[0]);
|
|
2725
|
+
layer.store.resourcePattern = [
|
|
2726
|
+
.../* @__PURE__ */ new Set([...layer.store.resourcePattern, ...authored.store.resourcePattern])
|
|
2727
|
+
];
|
|
2728
|
+
layer.store.extent = layer.store.extent.map(
|
|
2729
|
+
(value, axis) => axis < 2 ? Math.min(value, authored.store.extent[axis]) : Math.max(value, authored.store.extent[axis])
|
|
2730
|
+
);
|
|
2731
|
+
for (const [name, bytes] of Object.entries(resource.files)) {
|
|
2732
|
+
if (name.startsWith("nodes/1/") && !name.endsWith("3dNodeIndexDocument.json.gz"))
|
|
2733
|
+
await memory.write(
|
|
2734
|
+
{
|
|
2735
|
+
resourceId: name.replace("nodes/1/", `nodes/${index}/`),
|
|
2736
|
+
parts: [bytes],
|
|
2737
|
+
contentType: name.endsWith(".gz") ? "application/gzip" : name.endsWith(".png") ? "image/png" : "image/jpeg"
|
|
2738
|
+
},
|
|
2739
|
+
signal
|
|
2740
|
+
);
|
|
2741
|
+
}
|
|
2742
|
+
leaves.push({ node, legacy: { ...legacy, id: String(index) } });
|
|
2743
|
+
identifiers.add(resource.id);
|
|
2744
|
+
objectIdRanges.push([first, first + count]);
|
|
2745
|
+
state = "open";
|
|
2746
|
+
},
|
|
2747
|
+
/** Rebuilds both indices and layer bounds only after every leaf has completed. */
|
|
2748
|
+
async finalize(report) {
|
|
2749
|
+
if (state !== "open" || !layer || !leaves.length)
|
|
2750
|
+
throw new TileConversionError16(
|
|
2751
|
+
"I3S_COLLECTION_INCOMPLETE",
|
|
2752
|
+
"Finalization requires successfully written mesh nodes"
|
|
2753
|
+
);
|
|
2754
|
+
state = "writing";
|
|
2755
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
2756
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
2757
|
+
for (const { node } of leaves) {
|
|
2758
|
+
const center2 = Ellipsoid.WGS84.cartographicToCartesian(node.obb.center);
|
|
2759
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
2760
|
+
minimum[axis] = Math.min(minimum[axis], center2[axis] - node.obb.halfSize[axis]);
|
|
2761
|
+
maximum[axis] = Math.max(maximum[axis], center2[axis] + node.obb.halfSize[axis]);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
const center = minimum.map((value, axis) => value / 2 + maximum[axis] / 2);
|
|
2765
|
+
const geographicCenter = Array.from(Ellipsoid.WGS84.cartesianToCartographic(center));
|
|
2766
|
+
const reconstructedCenter = Ellipsoid.WGS84.cartographicToCartesian(geographicCenter);
|
|
2767
|
+
const obb = {
|
|
2768
|
+
center: geographicCenter,
|
|
2769
|
+
halfSize: minimum.map(
|
|
2770
|
+
(value, axis) => maximum[axis] / 2 - value / 2 + Math.abs(center[axis] - reconstructedCenter[axis]) + 1e-7
|
|
2771
|
+
),
|
|
2772
|
+
quaternion: [0, 0, 0, 1]
|
|
2773
|
+
};
|
|
2774
|
+
if ([...obb.center, ...obb.halfSize].some((value) => !Number.isFinite(value)))
|
|
2775
|
+
throw new TileConversionError16(
|
|
2776
|
+
"I3S_COLLECTION_BOUNDS_INVALID",
|
|
2777
|
+
"Collection bounds cannot be represented"
|
|
2778
|
+
);
|
|
2779
|
+
const mbs = [...obb.center, Math.hypot(...obb.halfSize)];
|
|
2780
|
+
const children = leaves.map(({ node }) => ({
|
|
2781
|
+
id: String(node.index),
|
|
2782
|
+
href: `../${node.index}`,
|
|
2783
|
+
obb: node.obb,
|
|
2784
|
+
mbs: [...node.obb.center, Math.hypot(...node.obb.halfSize)]
|
|
2785
|
+
}));
|
|
2786
|
+
await writeJson("nodes/root/3dNodeIndexDocument.json.gz", {
|
|
2787
|
+
id: "root",
|
|
2788
|
+
level: 0,
|
|
2789
|
+
version: "1",
|
|
2790
|
+
obb,
|
|
2791
|
+
mbs,
|
|
2792
|
+
lodSelection: [{ metricType: "maxScreenThresholdSQ", maxError: 0 }],
|
|
2793
|
+
children
|
|
2794
|
+
});
|
|
2795
|
+
for (const { node, legacy } of leaves)
|
|
2796
|
+
await writeJson(`nodes/${node.index}/3dNodeIndexDocument.json.gz`, {
|
|
2797
|
+
...legacy,
|
|
2798
|
+
parentNode: { id: "root", href: "../root", obb, mbs }
|
|
2799
|
+
});
|
|
2800
|
+
const nodes = [
|
|
2801
|
+
{ index: 0, obb, lodThreshold: 0, children: leaves.map(({ node }) => node.index) },
|
|
2802
|
+
...leaves.map(({ node }) => node)
|
|
2803
|
+
];
|
|
2804
|
+
for (let page = 0; page * 64 < nodes.length; page++)
|
|
2805
|
+
await writeJson(`nodepages/${page}.json.gz`, {
|
|
2806
|
+
nodes: nodes.slice(page * 64, (page + 1) * 64)
|
|
2807
|
+
});
|
|
2808
|
+
if (!layer.textureSetDefinitions?.length) delete layer.textureSetDefinitions;
|
|
2809
|
+
await writeJson("3dSceneLayer.json.gz", layer);
|
|
2810
|
+
await writeJson("metadata.json.gz", {
|
|
2811
|
+
folderPattern: "BASIC",
|
|
2812
|
+
archiveCompressionType: "STORE",
|
|
2813
|
+
resourceCompressionType: "GZIP",
|
|
2814
|
+
I3SVersion: "1.7",
|
|
2815
|
+
nodeCount: nodes.length
|
|
2816
|
+
});
|
|
2817
|
+
await memory.finalize(report);
|
|
2818
|
+
state = "closed";
|
|
2819
|
+
completed = true;
|
|
2820
|
+
},
|
|
2821
|
+
/** Discards binary resources and index metadata after any failure. */
|
|
2822
|
+
async abort(error) {
|
|
2823
|
+
completed = false;
|
|
2824
|
+
state = "closed";
|
|
2825
|
+
leaves.length = 0;
|
|
2826
|
+
identifiers.clear();
|
|
2827
|
+
objectIdRanges.length = 0;
|
|
2828
|
+
layer = void 0;
|
|
2829
|
+
await memory.abort(error);
|
|
2830
|
+
}
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// src/v5/conversion-api.ts
|
|
2835
|
+
var TileConversionError17 = class extends Error {
|
|
2836
|
+
/** Stable machine-readable failure code. */
|
|
2837
|
+
code;
|
|
2838
|
+
/** Diagnostics associated with this failure. */
|
|
2839
|
+
diagnostics;
|
|
2840
|
+
/** Creates a typed conversion failure. */
|
|
2841
|
+
constructor(code, message, diagnostics = []) {
|
|
2842
|
+
super(message);
|
|
2843
|
+
this.name = "TileConversionError";
|
|
2844
|
+
this.code = code;
|
|
2845
|
+
this.diagnostics = diagnostics;
|
|
2846
|
+
}
|
|
2847
|
+
};
|
|
2848
|
+
|
|
2849
|
+
// src/v5/resource-fetcher.ts
|
|
2850
|
+
function createTileConversionResourceFetcher(options) {
|
|
2851
|
+
let consumed = options.initialInputBytes ?? 0;
|
|
2852
|
+
if (!Number.isSafeInteger(options.maxInputBytes) || options.maxInputBytes < 1 || !Number.isSafeInteger(consumed) || consumed < 0 || consumed > options.maxInputBytes)
|
|
2853
|
+
throw new TileConversionError17(
|
|
2854
|
+
"INVALID_INPUT_MEMORY_LIMIT",
|
|
2855
|
+
"Resource fetch requires a positive aggregate byte budget and valid initial bytes"
|
|
2856
|
+
);
|
|
2857
|
+
const fetcher = options.fetcher ?? fetch;
|
|
2858
|
+
return async (input, requestOptions) => {
|
|
2859
|
+
options.signal?.throwIfAborted();
|
|
2860
|
+
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
2861
|
+
if (!["http:", "https:", "blob:", "data:"].includes(url.protocol))
|
|
2862
|
+
throw new TileConversionError17(
|
|
2863
|
+
"EXTERNAL_RESOURCE_UNSUPPORTED",
|
|
2864
|
+
"Conversion transport requires HTTP(S), Blob or embedded data resources"
|
|
2865
|
+
);
|
|
2866
|
+
const requestSignal = requestOptions?.signal ?? (input instanceof Request ? input.signal : void 0);
|
|
2867
|
+
const signal = options.signal && requestSignal ? AbortSignal.any([options.signal, requestSignal]) : options.signal ?? requestSignal;
|
|
2868
|
+
const response = await fetcher(input, { ...requestOptions, signal });
|
|
2869
|
+
if (!response.ok) {
|
|
2870
|
+
await response.body?.cancel();
|
|
2871
|
+
throw new TileConversionError17(
|
|
2872
|
+
"BROWSER_SOURCE_FETCH_FAILED",
|
|
2873
|
+
`Could not fetch conversion resource (${response.status})`
|
|
2874
|
+
);
|
|
2875
|
+
}
|
|
2876
|
+
const reader = response.body?.getReader();
|
|
2877
|
+
if (!reader)
|
|
2878
|
+
throw new TileConversionError17(
|
|
2879
|
+
"BROWSER_SOURCE_STREAM_UNAVAILABLE",
|
|
2880
|
+
"Conversion resources require readable response streams"
|
|
2881
|
+
);
|
|
2882
|
+
const cancelRead = () => {
|
|
2883
|
+
void reader.cancel(signal?.reason).catch(() => {
|
|
2884
|
+
});
|
|
2885
|
+
};
|
|
2886
|
+
signal?.addEventListener("abort", cancelRead, { once: true });
|
|
2887
|
+
const parts = [];
|
|
2888
|
+
let byteLength = 0;
|
|
2889
|
+
try {
|
|
2890
|
+
while (true) {
|
|
2891
|
+
signal?.throwIfAborted();
|
|
2892
|
+
const { done, value } = await reader.read();
|
|
2893
|
+
signal?.throwIfAborted();
|
|
2894
|
+
if (done) break;
|
|
2895
|
+
if (value.byteLength > options.maxInputBytes - consumed)
|
|
2896
|
+
throw new TileConversionError17(
|
|
2897
|
+
"INPUT_RESOURCE_TOO_LARGE",
|
|
2898
|
+
"Selected contents and external dependencies exceed the aggregate input byte budget"
|
|
2899
|
+
);
|
|
2900
|
+
consumed += value.byteLength;
|
|
2901
|
+
byteLength += value.byteLength;
|
|
2902
|
+
parts.push(value);
|
|
2903
|
+
}
|
|
2904
|
+
} catch (error) {
|
|
2905
|
+
await reader.cancel(error);
|
|
2906
|
+
throw error;
|
|
2907
|
+
} finally {
|
|
2908
|
+
signal?.removeEventListener("abort", cancelRead);
|
|
2909
|
+
reader.releaseLock();
|
|
2910
|
+
}
|
|
2911
|
+
const bytes = new Uint8Array(byteLength);
|
|
2912
|
+
let offset = 0;
|
|
2913
|
+
for (const part of parts) {
|
|
2914
|
+
bytes.set(part, offset);
|
|
2915
|
+
offset += part.byteLength;
|
|
2916
|
+
}
|
|
2917
|
+
return new Response(bytes, {
|
|
2918
|
+
status: response.status,
|
|
2919
|
+
statusText: response.statusText,
|
|
2920
|
+
headers: response.headers
|
|
2921
|
+
});
|
|
2922
|
+
};
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
// src/v5/point-cloud-tileset-sink.ts
|
|
2926
|
+
import {
|
|
2927
|
+
createBoundedMemoryTileConversionSink as createBoundedMemoryTileConversionSink4,
|
|
2928
|
+
TileConversionError as TileConversionError18
|
|
2929
|
+
} from "@loaders.gl/tile-converter/v5/core";
|
|
2930
|
+
function createPointCloudTilesetSink(options) {
|
|
2931
|
+
const { maxTiles, geometricError } = options;
|
|
2932
|
+
if (!Number.isSafeInteger(maxTiles) || maxTiles < 1 || !Number.isFinite(geometricError) || geometricError < 0) {
|
|
2933
|
+
throw new TileConversionError18(
|
|
2934
|
+
"POINT_CLOUD_TILESET_OPTIONS_INVALID",
|
|
2935
|
+
"Positive maxTiles and finite nonnegative geometricError are required"
|
|
2936
|
+
);
|
|
2937
|
+
}
|
|
2938
|
+
const memory = createBoundedMemoryTileConversionSink4(options);
|
|
2939
|
+
const children = [];
|
|
2940
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
2941
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
2942
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
2943
|
+
let state = "open";
|
|
2944
|
+
let completed = false;
|
|
2945
|
+
return {
|
|
2946
|
+
/** Exposes finalized files without exposing a partial package. */
|
|
2947
|
+
getFiles: () => completed ? memory.getFiles() : [],
|
|
2948
|
+
/** Captures one validated placement and encoded PNTS resource. */
|
|
2949
|
+
async write(resource, signal) {
|
|
2950
|
+
signal?.throwIfAborted();
|
|
2951
|
+
if (state !== "open")
|
|
2952
|
+
throw new TileConversionError18(
|
|
2953
|
+
"POINT_CLOUD_TILESET_SINK_UNAVAILABLE",
|
|
2954
|
+
"Point sink is busy or closed"
|
|
2955
|
+
);
|
|
2956
|
+
if (!resource.id || identifiers.has(resource.id) || children.length >= maxTiles) {
|
|
2957
|
+
throw new TileConversionError18(
|
|
2958
|
+
"POINT_CLOUD_TILESET_PLACEMENT_INVALID",
|
|
2959
|
+
"Placements require unique nonempty identifiers within maxTiles"
|
|
2960
|
+
);
|
|
2961
|
+
}
|
|
2962
|
+
const [lower, upper] = getPointTileBounds(resource, geometricError);
|
|
2963
|
+
const resourceId = `points/${children.length}.pnts`;
|
|
2964
|
+
state = "writing";
|
|
2965
|
+
await memory.write(
|
|
2966
|
+
{ resourceId, parts: [resource.pnts], contentType: "application/octet-stream" },
|
|
2967
|
+
signal
|
|
2968
|
+
);
|
|
2969
|
+
if (state !== "writing")
|
|
2970
|
+
throw new TileConversionError18(
|
|
2971
|
+
"POINT_CLOUD_TILESET_SINK_UNAVAILABLE",
|
|
2972
|
+
"Point sink was aborted during writing"
|
|
2973
|
+
);
|
|
2974
|
+
children.push({
|
|
2975
|
+
boundingVolume: { box: createPointBoundsBox(lower, upper) },
|
|
2976
|
+
geometricError,
|
|
2977
|
+
content: { uri: resourceId }
|
|
2978
|
+
});
|
|
2979
|
+
identifiers.add(resource.id);
|
|
2980
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
2981
|
+
minimum[axis] = Math.min(minimum[axis], lower[axis]);
|
|
2982
|
+
maximum[axis] = Math.max(maximum[axis], upper[axis]);
|
|
2983
|
+
}
|
|
2984
|
+
state = "open";
|
|
2985
|
+
},
|
|
2986
|
+
/** Serializes the enclosing root after all placements have completed. */
|
|
2987
|
+
async finalize(report) {
|
|
2988
|
+
if (state !== "open" || !children.length)
|
|
2989
|
+
throw new TileConversionError18(
|
|
2990
|
+
"POINT_CLOUD_TILESET_SINK_INCOMPLETE",
|
|
2991
|
+
"Finalization requires successfully written point tiles"
|
|
2992
|
+
);
|
|
2993
|
+
state = "writing";
|
|
2994
|
+
const rootError = Math.max(
|
|
2995
|
+
geometricError,
|
|
2996
|
+
Math.hypot(...minimum.map((value, axis) => maximum[axis] - value))
|
|
2997
|
+
);
|
|
2998
|
+
if (!Number.isFinite(rootError))
|
|
2999
|
+
throw new TileConversionError18(
|
|
3000
|
+
"POINT_CLOUD_TILESET_BOUNDS_INVALID",
|
|
3001
|
+
"Enclosing bounds must have a finite diagonal"
|
|
3002
|
+
);
|
|
3003
|
+
const json = JSON.stringify({
|
|
3004
|
+
asset: { version: "1.0" },
|
|
3005
|
+
geometricError: rootError,
|
|
3006
|
+
root: {
|
|
3007
|
+
boundingVolume: { box: createPointBoundsBox(minimum, maximum) },
|
|
3008
|
+
geometricError: rootError,
|
|
3009
|
+
refine: "ADD",
|
|
3010
|
+
children
|
|
3011
|
+
}
|
|
3012
|
+
});
|
|
3013
|
+
await memory.write({
|
|
3014
|
+
resourceId: "tileset.json",
|
|
3015
|
+
parts: [json],
|
|
3016
|
+
contentType: "application/json"
|
|
3017
|
+
});
|
|
3018
|
+
if (state !== "writing")
|
|
3019
|
+
throw new TileConversionError18(
|
|
3020
|
+
"POINT_CLOUD_TILESET_SINK_UNAVAILABLE",
|
|
3021
|
+
"Point sink was aborted during finalization"
|
|
3022
|
+
);
|
|
3023
|
+
await memory.finalize(report);
|
|
3024
|
+
if (state !== "writing")
|
|
3025
|
+
throw new TileConversionError18(
|
|
3026
|
+
"POINT_CLOUD_TILESET_SINK_UNAVAILABLE",
|
|
3027
|
+
"Point sink was aborted during finalization"
|
|
3028
|
+
);
|
|
3029
|
+
state = "closed";
|
|
3030
|
+
completed = true;
|
|
3031
|
+
},
|
|
3032
|
+
/** Clears resource and hierarchy storage on failure or cancellation. */
|
|
3033
|
+
async abort(error) {
|
|
3034
|
+
state = "closed";
|
|
3035
|
+
completed = false;
|
|
3036
|
+
children.length = 0;
|
|
3037
|
+
identifiers.clear();
|
|
3038
|
+
await memory.abort(error);
|
|
3039
|
+
}
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
function getPointTileBounds(resource, geometricError) {
|
|
3043
|
+
const reference = resource.spatialReference;
|
|
3044
|
+
const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
3045
|
+
if (!reference || !["native", "transformed"].includes(reference.status) || (reference.targetCrs || reference.sourceCrs) !== "EPSG:4978" || (reference.targetHeightReference === "native" ? reference.heightReference : reference.targetHeightReference) !== "ellipsoidal" || resource.coordinateSystem !== "cartesian" || resource.cartographicOrigin.length !== 3 || resource.cartographicOrigin.some((value) => value !== 0) || resource.modelMatrix && (resource.modelMatrix.length !== 16 || resource.modelMatrix.some((value, index) => value !== identity[index])) || reference.status === "native" && (reference.coordinateFrame !== "geocentric" || reference.axisOrder !== "xyz" || reference.verticalUnitScale !== 1 || reference.units?.some((unit) => unit !== "meter"))) {
|
|
3046
|
+
throw new TileConversionError18(
|
|
3047
|
+
"POINT_CLOUD_TILESET_FRAME_UNSUPPORTED",
|
|
3048
|
+
"Point tiles require declared EPSG:4978 xyz meters, ellipsoidal heights and no additional source placement"
|
|
3049
|
+
);
|
|
3050
|
+
}
|
|
3051
|
+
if (!Number.isFinite(resource.maximumPositionError) || resource.maximumPositionError < 0 || resource.maximumPositionError > geometricError) {
|
|
3052
|
+
throw new TileConversionError18(
|
|
3053
|
+
"POINT_CLOUD_GEOMETRIC_ERROR_INVALID",
|
|
3054
|
+
"geometricError must include measured position reconstruction error"
|
|
3055
|
+
);
|
|
3056
|
+
}
|
|
3057
|
+
const [minimum, maximum] = resource.localBoundingBox;
|
|
3058
|
+
const center = resource.rtcCenter;
|
|
3059
|
+
if (center.length !== 3 || minimum.length !== 3 || maximum.length !== 3 || [...center, ...minimum, ...maximum].some((value) => !Number.isFinite(value)) || minimum.some((value, axis) => value > maximum[axis])) {
|
|
3060
|
+
throw new TileConversionError18(
|
|
3061
|
+
"POINT_CLOUD_TILESET_BOUNDS_INVALID",
|
|
3062
|
+
"RTC center and encoded xyz bounds must be finite and ordered"
|
|
3063
|
+
);
|
|
3064
|
+
}
|
|
3065
|
+
const lower = minimum.map((value, axis) => value + center[axis]);
|
|
3066
|
+
const upper = maximum.map((value, axis) => value + center[axis]);
|
|
3067
|
+
if ([...lower, ...upper].some((value) => !Number.isFinite(value)))
|
|
3068
|
+
throw new TileConversionError18(
|
|
3069
|
+
"POINT_CLOUD_TILESET_BOUNDS_INVALID",
|
|
3070
|
+
"Reconstructed ECEF bounds must be finite"
|
|
3071
|
+
);
|
|
3072
|
+
return [lower, upper];
|
|
3073
|
+
}
|
|
3074
|
+
function createPointBoundsBox(minimum, maximum) {
|
|
3075
|
+
const center = minimum.map((value, axis) => value / 2 + maximum[axis] / 2);
|
|
3076
|
+
const halfSize = minimum.map((value, axis) => maximum[axis] / 2 - value / 2);
|
|
3077
|
+
return [center[0], center[1], center[2], halfSize[0], 0, 0, 0, halfSize[1], 0, 0, 0, halfSize[2]];
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
// src/v5/i3s-mesh-source.ts
|
|
3081
|
+
import {
|
|
3082
|
+
TileConversionError as TileConversionError21,
|
|
3083
|
+
createTilesetConversionSource as createTilesetConversionSource2
|
|
3084
|
+
} from "@loaders.gl/tile-converter/v5/core";
|
|
3085
|
+
|
|
3086
|
+
// src/v5/i3s-mesh-source-material.ts
|
|
3087
|
+
import { getBinaryImageMetadata as getBinaryImageMetadata3 } from "@loaders.gl/images";
|
|
3088
|
+
import { TileConversionError as TileConversionError19 } from "@loaders.gl/tile-converter/v5/core";
|
|
3089
|
+
async function mapI3SMeshSourceMaterial(content, header, readExternalResource, contentUri, signal) {
|
|
3090
|
+
const material = Object.fromEntries(
|
|
3091
|
+
Object.entries(content.material || {}).filter(([, value]) => value !== void 0)
|
|
3092
|
+
);
|
|
3093
|
+
const metallicRoughness = material.pbrMetallicRoughness ? Object.fromEntries(
|
|
3094
|
+
Object.entries(material.pbrMetallicRoughness).filter(([, value]) => value !== void 0)
|
|
3095
|
+
) : {};
|
|
3096
|
+
material.pbrMetallicRoughness = metallicRoughness;
|
|
3097
|
+
if (material.cullFace !== void 0 && material.cullFace !== "back") {
|
|
3098
|
+
throw new TileConversionError19(
|
|
3099
|
+
"I3S_MESH_SOURCE_MATERIAL_UNSUPPORTED",
|
|
3100
|
+
"Only back-face culling is supported"
|
|
3101
|
+
);
|
|
3102
|
+
}
|
|
3103
|
+
delete material.cullFace;
|
|
3104
|
+
let textureInfo = metallicRoughness.baseColorTexture ? { ...metallicRoughness.baseColorTexture } : void 0;
|
|
3105
|
+
const resources = header.textureUrls?.length ? header.textureUrls : header.textureUrl ? [
|
|
3106
|
+
{
|
|
3107
|
+
textureSetDefinitionId: textureInfo?.textureSetDefinitionId ?? 0,
|
|
3108
|
+
textureUrl: header.textureUrl,
|
|
3109
|
+
textureFormat: header.textureFormat || "jpg"
|
|
3110
|
+
}
|
|
3111
|
+
] : [];
|
|
3112
|
+
if (!textureInfo && !header.materialDefinition && resources.length === 1) {
|
|
3113
|
+
textureInfo = { textureSetDefinitionId: resources[0].textureSetDefinitionId };
|
|
3114
|
+
}
|
|
3115
|
+
const encodedTextures = content.textures || {};
|
|
3116
|
+
if (!textureInfo && (resources.length || content.texture || Object.keys(encodedTextures).length)) {
|
|
3117
|
+
throw new TileConversionError19(
|
|
3118
|
+
"I3S_MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
3119
|
+
"Texture resources require a base-color mapping"
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
if (textureInfo) {
|
|
3123
|
+
const definitionId = textureInfo.textureSetDefinitionId;
|
|
3124
|
+
const wrappingModes = { none: 33071, repeat: 10497, mirror: 33648 };
|
|
3125
|
+
if (typeof definitionId !== "number" || !Number.isSafeInteger(definitionId) || definitionId < 0 || textureInfo.texCoord !== void 0 && textureInfo.texCoord !== 0 || Object.keys(textureInfo).some(
|
|
3126
|
+
(name) => !["textureSetDefinitionId", "texCoord", "wrapS", "wrapT", "texture"].includes(name)
|
|
3127
|
+
) || ["wrapS", "wrapT"].some(
|
|
3128
|
+
(name) => textureInfo[name] !== void 0 && (typeof textureInfo[name] !== "string" || !Object.hasOwn(wrappingModes, textureInfo[name]))
|
|
3129
|
+
) || resources.length > 1 || resources.some((resource) => resource.textureSetDefinitionId !== definitionId) || Object.keys(encodedTextures).some((key) => key !== String(definitionId))) {
|
|
3130
|
+
throw new TileConversionError19(
|
|
3131
|
+
"I3S_MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
3132
|
+
"Only one base-color texture set on TEXCOORD_0 with qualified wrapping is supported"
|
|
3133
|
+
);
|
|
3134
|
+
}
|
|
3135
|
+
let encoded = encodedTextures[String(definitionId)] || content.texture || textureInfo.texture?.source?.image;
|
|
3136
|
+
if (!encoded) {
|
|
3137
|
+
if (!resources.length || !readExternalResource) {
|
|
3138
|
+
throw new TileConversionError19(
|
|
3139
|
+
"I3S_MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
3140
|
+
"Encoded texture bytes or an explicit bounded resource reader are required"
|
|
3141
|
+
);
|
|
3142
|
+
}
|
|
3143
|
+
signal?.throwIfAborted();
|
|
3144
|
+
encoded = await readExternalResource(resources[0].textureUrl, contentUri, signal);
|
|
3145
|
+
signal?.throwIfAborted();
|
|
3146
|
+
}
|
|
3147
|
+
const data = encoded instanceof ArrayBuffer ? new Uint8Array(encoded) : encoded;
|
|
3148
|
+
let image;
|
|
3149
|
+
if (data instanceof Uint8Array) {
|
|
3150
|
+
try {
|
|
3151
|
+
image = getBinaryImageMetadata3(new DataView(data.buffer, data.byteOffset, data.byteLength));
|
|
3152
|
+
} catch {
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
const declaredFormat = resources[0]?.textureFormat;
|
|
3156
|
+
if (!image || !["image/png", "image/jpeg"].includes(image.mimeType) || declaredFormat && (declaredFormat === "png" ? image.mimeType !== "image/png" : declaredFormat === "jpg" ? image.mimeType !== "image/jpeg" : true)) {
|
|
3157
|
+
throw new TileConversionError19(
|
|
3158
|
+
"I3S_MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
3159
|
+
"I3S base-color bytes must have matching PNG/JPEG headers"
|
|
3160
|
+
);
|
|
3161
|
+
}
|
|
3162
|
+
const sampler = Object.fromEntries(
|
|
3163
|
+
["wrapS", "wrapT"].filter((name) => textureInfo[name] !== void 0).map((name) => [name, wrappingModes[textureInfo[name]]])
|
|
3164
|
+
);
|
|
3165
|
+
metallicRoughness.baseColorTexture = {
|
|
3166
|
+
texCoord: 0,
|
|
3167
|
+
texture: {
|
|
3168
|
+
source: { mimeType: image.mimeType, bufferView: { data } },
|
|
3169
|
+
...Object.keys(sampler).length ? { sampler } : {}
|
|
3170
|
+
}
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
const mapped = await mapMeshSourceMaterial(
|
|
3174
|
+
material,
|
|
3175
|
+
void 0,
|
|
3176
|
+
contentUri,
|
|
3177
|
+
signal
|
|
3178
|
+
);
|
|
3179
|
+
if (mapped?.baseColorTexture && !content.sourceAttributes?.uv0) {
|
|
3180
|
+
throw new TileConversionError19(
|
|
3181
|
+
"MESH_TEXCOORD_REQUIRED",
|
|
3182
|
+
"Base-color textures require TEXCOORD_0"
|
|
3183
|
+
);
|
|
3184
|
+
}
|
|
3185
|
+
return mapped;
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
// src/v5/i3s-mesh-source-features.ts
|
|
3189
|
+
import { I3SAttributeLoader } from "@loaders.gl/i3s";
|
|
3190
|
+
import { TileConversionError as TileConversionError20 } from "@loaders.gl/tile-converter/v5/core";
|
|
3191
|
+
async function extractI3SMeshFeatures(content, item, metadata, options, readExternalResource, contentUri, signal) {
|
|
3192
|
+
const maximumAttributeBytes = options.maxAttributeBytes;
|
|
3193
|
+
const storage = metadata.tileset.attributeStorageInfo;
|
|
3194
|
+
const attributeUrls = item.tile.header.attributeUrls;
|
|
3195
|
+
if (!options.metadataClass || !options.objectIdProperty || options.rawMetadataField || !Number.isSafeInteger(maximumAttributeBytes) || maximumAttributeBytes <= 0 || !storage?.length || attributeUrls?.length !== storage.length || !readExternalResource || new Set(storage.map((attribute2) => attribute2.name)).size !== storage.length || new Set(storage.map((attribute2) => attribute2.key)).size !== storage.length || !storage.some((attribute2) => attribute2.name === options.objectIdProperty) || (metadata.tileset.fields || []).some(
|
|
3196
|
+
(field) => field.domain || field.type === "esriFieldTypeDate" || !storage.some((attribute2) => attribute2.name === field.name)
|
|
3197
|
+
)) {
|
|
3198
|
+
throw new TileConversionError20(
|
|
3199
|
+
"I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
|
|
3200
|
+
"I3S attributes require an explicit scalar schema, object ID column, byte budget and resource reader; domains/dates/raw metadata need a custom mapper"
|
|
3201
|
+
);
|
|
3202
|
+
}
|
|
3203
|
+
const attributeLoader = await I3SAttributeLoader.preload();
|
|
3204
|
+
const columns = /* @__PURE__ */ Object.create(null);
|
|
3205
|
+
let inputBytes = 0;
|
|
3206
|
+
let count;
|
|
3207
|
+
for (const [index, attribute2] of storage.entries()) {
|
|
3208
|
+
signal?.throwIfAborted();
|
|
3209
|
+
const values = attribute2.objectIds ?? attribute2.attributeValues;
|
|
3210
|
+
const type = values?.valueType;
|
|
3211
|
+
const expectedOrdering = attribute2.objectIds ? ["objectIds"] : type === "String" ? ["attributeByteCounts", "attributeValues"] : ["attributeValues"];
|
|
3212
|
+
const expectedHeader = type === "String" ? [
|
|
3213
|
+
{ property: "count", valueType: "UInt32" },
|
|
3214
|
+
{ property: "attributeValuesByteCount", valueType: "UInt32" }
|
|
3215
|
+
] : [{ property: "count", valueType: "UInt32" }];
|
|
3216
|
+
if (!type || values?.valuesPerElement !== 1 || attribute2.objectIds && attribute2.attributeValues || attribute2.header?.length !== expectedHeader.length || expectedHeader.some(
|
|
3217
|
+
(value, headerIndex) => attribute2.header[headerIndex]?.property !== value.property || attribute2.header[headerIndex]?.valueType !== value.valueType
|
|
3218
|
+
) || attribute2.ordering && JSON.stringify(
|
|
3219
|
+
attribute2.ordering.map((value) => value === "ObjectIds" ? "objectIds" : value)
|
|
3220
|
+
) !== JSON.stringify(expectedOrdering) || type === "String" && (values.encoding !== "UTF-8" || attribute2.attributeByteCounts?.valueType !== "UInt32" || attribute2.attributeByteCounts.valuesPerElement !== 1) || !attributeUrls[index]) {
|
|
3221
|
+
throw new TileConversionError20(
|
|
3222
|
+
"I3S_MESH_SOURCE_ATTRIBUTE_UNSUPPORTED",
|
|
3223
|
+
`Attribute ${attribute2.name} requires a standard scalar/string resource layout`
|
|
3224
|
+
);
|
|
3225
|
+
}
|
|
3226
|
+
const bytes = await readExternalResource(attributeUrls[index], contentUri, signal);
|
|
3227
|
+
signal?.throwIfAborted();
|
|
3228
|
+
inputBytes += bytes instanceof Uint8Array ? bytes.byteLength : Infinity;
|
|
3229
|
+
if (!(bytes instanceof Uint8Array) || inputBytes > maximumAttributeBytes) {
|
|
3230
|
+
throw new TileConversionError20(
|
|
3231
|
+
"INPUT_RESOURCE_TOO_LARGE",
|
|
3232
|
+
"I3S node attributes exceed maxAttributeBytes"
|
|
3233
|
+
);
|
|
3234
|
+
}
|
|
3235
|
+
let parsed;
|
|
3236
|
+
try {
|
|
3237
|
+
parsed = await attributeLoader.parse(Uint8Array.from(bytes).buffer, {
|
|
3238
|
+
attributeName: attribute2.name,
|
|
3239
|
+
attributeType: type,
|
|
3240
|
+
i3s: { attributeValues: "exact" }
|
|
3241
|
+
});
|
|
3242
|
+
} catch (error) {
|
|
3243
|
+
throw new TileConversionError20(
|
|
3244
|
+
"I3S_MESH_SOURCE_ATTRIBUTE_INVALID",
|
|
3245
|
+
`Invalid attribute ${attribute2.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
3246
|
+
);
|
|
3247
|
+
}
|
|
3248
|
+
const column = parsed[attribute2.name];
|
|
3249
|
+
if (!column || count !== void 0 && column.length !== count || !column.length) {
|
|
3250
|
+
throw new TileConversionError20(
|
|
3251
|
+
"I3S_MESH_SOURCE_ATTRIBUTE_COUNT_INVALID",
|
|
3252
|
+
"Every attribute must have the same nonzero row count"
|
|
3253
|
+
);
|
|
3254
|
+
}
|
|
3255
|
+
if (Array.from(column).some(
|
|
3256
|
+
(value) => typeof value === "number" && !Number.isFinite(value)
|
|
3257
|
+
)) {
|
|
3258
|
+
throw new TileConversionError20(
|
|
3259
|
+
"I3S_MESH_SOURCE_ATTRIBUTE_UNSUPPORTED",
|
|
3260
|
+
"Numeric noData values require an explicit custom mapper"
|
|
3261
|
+
);
|
|
3262
|
+
}
|
|
3263
|
+
count = column.length;
|
|
3264
|
+
columns[attribute2.name] = column;
|
|
3265
|
+
}
|
|
3266
|
+
const identifiers = columns[options.objectIdProperty];
|
|
3267
|
+
const attributeRows = /* @__PURE__ */ new Map();
|
|
3268
|
+
for (let index = 0; index < count; index++) {
|
|
3269
|
+
const identifier = identifiers[index];
|
|
3270
|
+
if (!(typeof identifier === "bigint" || typeof identifier === "number" && Number.isSafeInteger(identifier)) || attributeRows.has(BigInt(identifier))) {
|
|
3271
|
+
throw new TileConversionError20(
|
|
3272
|
+
"I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED",
|
|
3273
|
+
"Object IDs must be unique exact integers"
|
|
3274
|
+
);
|
|
3275
|
+
}
|
|
3276
|
+
attributeRows.set(BigInt(identifier), index);
|
|
3277
|
+
}
|
|
3278
|
+
const vertexCount = content.vertexCount;
|
|
3279
|
+
const geometryIdentifiers = content.featureIds;
|
|
3280
|
+
const attribute = content.sourceAttributes?.id;
|
|
3281
|
+
if (geometryIdentifiers.length !== vertexCount || attribute?.size !== 1 || attribute.normalized || attribute.value.length !== vertexCount || Array.from(geometryIdentifiers).some((value, index) => value !== attribute.value[index])) {
|
|
3282
|
+
throw new TileConversionError20(
|
|
3283
|
+
"I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED",
|
|
3284
|
+
"Geometry requires one exact unnormalized object ID per vertex"
|
|
3285
|
+
);
|
|
3286
|
+
}
|
|
3287
|
+
const indices = content.indices;
|
|
3288
|
+
const triangleFeatureIndices = new Uint32Array((indices?.length ?? vertexCount) / 3);
|
|
3289
|
+
if (content.drawRanges?.length) {
|
|
3290
|
+
const covered = new Uint8Array(triangleFeatureIndices.length);
|
|
3291
|
+
for (const range of content.drawRanges) {
|
|
3292
|
+
if (!Number.isSafeInteger(range.firstPrimitive) || range.firstPrimitive < 0 || !Number.isSafeInteger(range.primitiveCount) || range.primitiveCount <= 0 || range.firstPrimitive + range.primitiveCount > covered.length) {
|
|
3293
|
+
throw new TileConversionError20(
|
|
3294
|
+
"I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
|
|
3295
|
+
"Feature draw ranges must cover valid triangles"
|
|
3296
|
+
);
|
|
3297
|
+
}
|
|
3298
|
+
for (let triangle = range.firstPrimitive; triangle < range.firstPrimitive + range.primitiveCount; triangle++) {
|
|
3299
|
+
const vertex = indices?.[triangle * 3] ?? triangle * 3;
|
|
3300
|
+
if (covered[triangle] || geometryIdentifiers[vertex] !== range.featureId) {
|
|
3301
|
+
throw new TileConversionError20(
|
|
3302
|
+
"I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
|
|
3303
|
+
"Feature draw ranges must agree with decoded ownership without overlapping"
|
|
3304
|
+
);
|
|
3305
|
+
}
|
|
3306
|
+
covered[triangle] = 1;
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
if (covered.some((value) => value === 0)) {
|
|
3310
|
+
throw new TileConversionError20(
|
|
3311
|
+
"I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
|
|
3312
|
+
"Feature draw ranges must cover every triangle"
|
|
3313
|
+
);
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
const usedRows = /* @__PURE__ */ new Set();
|
|
3317
|
+
for (let triangle = 0; triangle < triangleFeatureIndices.length; triangle++) {
|
|
3318
|
+
const corners = [0, 1, 2].map(
|
|
3319
|
+
(corner) => indices?.[triangle * 3 + corner] ?? triangle * 3 + corner
|
|
3320
|
+
);
|
|
3321
|
+
const featureIds = corners.map((corner) => geometryIdentifiers[corner]);
|
|
3322
|
+
const row = attributeRows.get(BigInt(featureIds[0]));
|
|
3323
|
+
if (row === void 0 || featureIds.some((identifier) => identifier !== featureIds[0])) {
|
|
3324
|
+
throw new TileConversionError20(
|
|
3325
|
+
"I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
|
|
3326
|
+
"Each triangle must reference one declared attribute row"
|
|
3327
|
+
);
|
|
3328
|
+
}
|
|
3329
|
+
triangleFeatureIndices[triangle] = row;
|
|
3330
|
+
usedRows.add(row);
|
|
3331
|
+
}
|
|
3332
|
+
if (usedRows.size !== count) {
|
|
3333
|
+
throw new TileConversionError20(
|
|
3334
|
+
"I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
|
|
3335
|
+
"Attribute rows without geometry require a separately qualified mapping"
|
|
3336
|
+
);
|
|
3337
|
+
}
|
|
3338
|
+
const stableProperty = options.sourceFeatureIdProperty ?? options.objectIdProperty;
|
|
3339
|
+
if (!Object.hasOwn(columns, stableProperty)) {
|
|
3340
|
+
throw new TileConversionError20(
|
|
3341
|
+
"I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
|
|
3342
|
+
"Stable identifier column is missing"
|
|
3343
|
+
);
|
|
3344
|
+
}
|
|
3345
|
+
const batches = convertFeatureAttributesToArrowBatches(
|
|
3346
|
+
Array.from({ length: count }, (_, index) => ({
|
|
3347
|
+
featureId: columns[stableProperty][index],
|
|
3348
|
+
metadataClass: options.metadataClass,
|
|
3349
|
+
properties: Object.fromEntries(
|
|
3350
|
+
Object.entries(columns).filter(([name]) => name !== options.objectIdProperty && name !== stableProperty).map(([name, values]) => [name, values[index]])
|
|
3351
|
+
)
|
|
3352
|
+
})),
|
|
3353
|
+
options
|
|
3354
|
+
);
|
|
3355
|
+
const stableIds = /* @__PURE__ */ new Set();
|
|
3356
|
+
for (const batch of batches) {
|
|
3357
|
+
const column = batch.data.getChild(options.featureIdField || "feature_id");
|
|
3358
|
+
for (let index = 0; index < column.length; index++) {
|
|
3359
|
+
const identifier = column.get(index);
|
|
3360
|
+
if (stableIds.has(identifier)) {
|
|
3361
|
+
throw new TileConversionError20(
|
|
3362
|
+
"I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED",
|
|
3363
|
+
"Stable IDs must be unique"
|
|
3364
|
+
);
|
|
3365
|
+
}
|
|
3366
|
+
stableIds.add(identifier);
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
return {
|
|
3370
|
+
batches,
|
|
3371
|
+
triangleFeatureIndices,
|
|
3372
|
+
featureIdField: options.featureIdField || "feature_id",
|
|
3373
|
+
integer64Encoding: options.integer64Encoding
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
// src/v5/i3s-mesh-source.ts
|
|
3378
|
+
function createI3SMeshTilesetConversionSource(tileset, options = {}) {
|
|
3379
|
+
const source = createTilesetConversionSource2(tileset, options);
|
|
3380
|
+
return {
|
|
3381
|
+
/** Validates the layer profile and explicit source decode policy before reading. */
|
|
3382
|
+
async inspect(signal) {
|
|
3383
|
+
if (options.features && options.getFeatures) {
|
|
3384
|
+
throw new TileConversionError21(
|
|
3385
|
+
"I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
|
|
3386
|
+
"Choose explicit resource features or a custom getFeatures mapper, not both"
|
|
3387
|
+
);
|
|
3388
|
+
}
|
|
3389
|
+
const metadata = await source.inspect(signal);
|
|
3390
|
+
if (metadata.type !== "I3S" || !["3DObject", "IntegratedMesh"].includes(metadata.tileset.layerType) || tileset.options.i3s?.geometryMode !== "source") {
|
|
3391
|
+
throw new TileConversionError21(
|
|
3392
|
+
"I3S_MESH_SOURCE_PROFILE_UNSUPPORTED",
|
|
3393
|
+
"I3S mesh extraction requires a 3DObject/IntegratedMesh runtime with geometryMode=source"
|
|
3394
|
+
);
|
|
3395
|
+
}
|
|
3396
|
+
if (!metadata.spatialReference?.sourceCrs) {
|
|
3397
|
+
throw new TileConversionError21(
|
|
3398
|
+
"I3S_MESH_SOURCE_REFERENCE_REQUIRED",
|
|
3399
|
+
"I3S conversion requires an explicit source CRS"
|
|
3400
|
+
);
|
|
3401
|
+
}
|
|
3402
|
+
return metadata;
|
|
3403
|
+
},
|
|
3404
|
+
/** Converts each supported content entry before shared traversal releases its buffers. */
|
|
3405
|
+
async *read(metadata, signal) {
|
|
3406
|
+
for await (const item of source.read(metadata, signal)) {
|
|
3407
|
+
for (const entry of item.contents) {
|
|
3408
|
+
signal?.throwIfAborted();
|
|
3409
|
+
const content = entry.payload;
|
|
3410
|
+
const attributes = content?.sourceAttributes;
|
|
3411
|
+
if (content?.geometryMode !== "source" || content.topology !== "triangle-list" || !attributes || !(attributes.position?.value instanceof Float64Array) || content.spatialReference?.status !== "native" || [
|
|
3412
|
+
"sourceCrs",
|
|
3413
|
+
"verticalCrs",
|
|
3414
|
+
"axisOrder",
|
|
3415
|
+
"units",
|
|
3416
|
+
"verticalUnitScale",
|
|
3417
|
+
"heightReference",
|
|
3418
|
+
"elevationMode",
|
|
3419
|
+
"elevationOffset",
|
|
3420
|
+
"elevationUnitScale",
|
|
3421
|
+
"coordinateEpoch"
|
|
3422
|
+
].some(
|
|
3423
|
+
(field) => JSON.stringify(content.spatialReference?.[field]) !== JSON.stringify(metadata.spatialReference?.[field])
|
|
3424
|
+
)) {
|
|
3425
|
+
throw new TileConversionError21(
|
|
3426
|
+
"I3S_MESH_SOURCE_GEOMETRY_UNSUPPORTED",
|
|
3427
|
+
"I3S extraction requires original source-frame Float64 triangle geometry"
|
|
3428
|
+
);
|
|
3429
|
+
}
|
|
3430
|
+
if (!["earth-centered", "vertex-reference-frame"].includes(
|
|
3431
|
+
content.normalReferenceFrame || ""
|
|
3432
|
+
) || content.normalReferenceFrame !== (metadata.tileset.store?.normalReferenceFrame || "earth-centered")) {
|
|
3433
|
+
throw new TileConversionError21(
|
|
3434
|
+
"I3S_MESH_SOURCE_NORMAL_FRAME_UNSUPPORTED",
|
|
3435
|
+
"Unknown I3S vector basis"
|
|
3436
|
+
);
|
|
3437
|
+
}
|
|
3438
|
+
if (content.meshSegmentation || Object.entries(attributes).some(
|
|
3439
|
+
([name, value]) => value && !["position", "normal", "uv0", "id", "faceRange"].includes(name)
|
|
3440
|
+
)) {
|
|
3441
|
+
throw new TileConversionError21(
|
|
3442
|
+
"I3S_MESH_SOURCE_ATTRIBUTE_UNSUPPORTED",
|
|
3443
|
+
"Only positions, normals, TEXCOORD_0 and explicitly mapped feature associations are supported"
|
|
3444
|
+
);
|
|
3445
|
+
}
|
|
3446
|
+
const hasFeatures = content.featureIds.length > 0 || metadata.tileset.fields?.length || metadata.tileset.attributeStorageInfo?.length;
|
|
3447
|
+
if (Array.from(content.featureIds).some((value) => !Number.isSafeInteger(value))) {
|
|
3448
|
+
throw new TileConversionError21(
|
|
3449
|
+
"I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED",
|
|
3450
|
+
"Decoded numeric feature IDs must be exact safe integers"
|
|
3451
|
+
);
|
|
3452
|
+
}
|
|
3453
|
+
if (hasFeatures && !options.getFeatures && !options.features) {
|
|
3454
|
+
throw new TileConversionError21(
|
|
3455
|
+
"I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
|
|
3456
|
+
"I3S feature data requires explicit resource features or a custom Arrow mapper"
|
|
3457
|
+
);
|
|
3458
|
+
}
|
|
3459
|
+
const mesh = validateMeshGeometry(
|
|
3460
|
+
{
|
|
3461
|
+
mode: 4,
|
|
3462
|
+
topology: "triangle-list",
|
|
3463
|
+
attributes: {
|
|
3464
|
+
POSITION: attributes.position,
|
|
3465
|
+
...attributes.normal ? { NORMAL: attributes.normal } : {},
|
|
3466
|
+
...attributes.uv0 ? { TEXCOORD_0: attributes.uv0 } : {}
|
|
3467
|
+
},
|
|
3468
|
+
...content.indices ? { indices: { value: content.indices, size: 1 } } : {}
|
|
3469
|
+
},
|
|
3470
|
+
true
|
|
3471
|
+
);
|
|
3472
|
+
if (mesh.attributes.POSITION.value.length / 3 !== content.vertexCount) {
|
|
3473
|
+
throw new TileConversionError21(
|
|
3474
|
+
"I3S_MESH_SOURCE_COUNT_INVALID",
|
|
3475
|
+
"I3S vertexCount must match the decoded geometry"
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
3478
|
+
const features = options.getFeatures ? await options.getFeatures(content, item, metadata, signal) : options.features ? await extractI3SMeshFeatures(
|
|
3479
|
+
content,
|
|
3480
|
+
item,
|
|
3481
|
+
metadata,
|
|
3482
|
+
options.features,
|
|
3483
|
+
options.readExternalResource,
|
|
3484
|
+
entry.uri,
|
|
3485
|
+
signal
|
|
3486
|
+
) : void 0;
|
|
3487
|
+
signal?.throwIfAborted();
|
|
3488
|
+
if (hasFeatures && !features) {
|
|
3489
|
+
throw new TileConversionError21(
|
|
3490
|
+
"I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
|
|
3491
|
+
"The feature mapper must return all declared feature data"
|
|
3492
|
+
);
|
|
3493
|
+
}
|
|
3494
|
+
const mappedMaterial = await mapI3SMeshSourceMaterial(
|
|
3495
|
+
content,
|
|
3496
|
+
item.tile.header,
|
|
3497
|
+
options.readExternalResource,
|
|
3498
|
+
entry.uri,
|
|
3499
|
+
signal
|
|
3500
|
+
);
|
|
3501
|
+
signal?.throwIfAborted();
|
|
3502
|
+
yield {
|
|
3503
|
+
id: `${item.tile.id}/${entry.index}`,
|
|
3504
|
+
origin: [0, 0, 0],
|
|
3505
|
+
mesh,
|
|
3506
|
+
normalReferenceFrame: content.normalReferenceFrame === "vertex-reference-frame" ? "vertex-reference-frame" : "earth-centered",
|
|
3507
|
+
features,
|
|
3508
|
+
material: mappedMaterial
|
|
3509
|
+
};
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
};
|
|
3514
|
+
}
|
|
3515
|
+
export {
|
|
3516
|
+
convertFeatureAttributesToArrowBatches,
|
|
3517
|
+
convertPointCloudSource,
|
|
3518
|
+
createI3SMeshConversionCodec,
|
|
3519
|
+
createI3SMeshSink,
|
|
3520
|
+
createI3SMeshTilesetConversionSource,
|
|
3521
|
+
createMeshConversionCodec,
|
|
3522
|
+
createMeshTilesetConversionSource,
|
|
3523
|
+
createMeshTilesetSink,
|
|
3524
|
+
createPointCloudTilesetSink,
|
|
3525
|
+
createSingleMeshI3SSink,
|
|
3526
|
+
createSingleMeshTilesetArchive,
|
|
3527
|
+
createSingleMeshTilesetSink,
|
|
3528
|
+
createTileConversionArchive,
|
|
3529
|
+
createTileConversionResourceFetcher,
|
|
3530
|
+
encodeMeshTile,
|
|
3531
|
+
encodePointCloudSource,
|
|
3532
|
+
encodePointCloudSourceTile,
|
|
3533
|
+
encodePointCloudTile,
|
|
3534
|
+
encodePointCloudTileWithMetadata,
|
|
3535
|
+
encodeTileConversionArchiveInBatches,
|
|
3536
|
+
transformPointCloudSourceTile
|
|
3537
|
+
};
|