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