@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
package/src/v5/mesh.ts
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {GLTFScenegraph, GLTFWriter} from '@loaders.gl/gltf';
|
|
6
|
+
import {getBinaryImageMetadata} from '@loaders.gl/images';
|
|
7
|
+
import type {MeshAttribute, MeshGeometry} from '@loaders.gl/schema';
|
|
8
|
+
import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
|
|
9
|
+
|
|
10
|
+
/** Explicit glTF wrapping and filtering for the selected base-color image. */
|
|
11
|
+
export interface MeshTileSampler {
|
|
12
|
+
/** Horizontal wrapping: CLAMP_TO_EDGE, MIRRORED_REPEAT, or REPEAT; omitted values use REPEAT. */
|
|
13
|
+
readonly wrapS?: 33071 | 33648 | 10497;
|
|
14
|
+
/** Vertical wrapping: CLAMP_TO_EDGE, MIRRORED_REPEAT, or REPEAT; omitted values use REPEAT. */
|
|
15
|
+
readonly wrapT?: 33071 | 33648 | 10497;
|
|
16
|
+
/** Magnification: NEAREST or LINEAR; omission leaves filtering to the renderer. */
|
|
17
|
+
readonly magFilter?: 9728 | 9729;
|
|
18
|
+
/** Minification: NEAREST, LINEAR, or a glTF mipmap filter; omission leaves filtering to the renderer. */
|
|
19
|
+
readonly minFilter?: 9728 | 9729 | 9984 | 9985 | 9986 | 9987;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Selected KHR_texture_transform controls for TEXCOORD_0. */
|
|
23
|
+
export interface MeshTileTextureTransform {
|
|
24
|
+
/** Finite UV translation; omitted values use [0, 0]. */
|
|
25
|
+
readonly offset?: readonly [number, number];
|
|
26
|
+
/** Finite counterclockwise rotation in radians around the UV origin; omission uses 0. */
|
|
27
|
+
readonly rotation?: number;
|
|
28
|
+
/** Finite UV scale; zero and negative values are allowed, and omission uses [1, 1]. */
|
|
29
|
+
readonly scale?: readonly [number, number];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** One already encoded base-color image using TEXCOORD_0. */
|
|
33
|
+
export interface MeshTileTexture {
|
|
34
|
+
/** Encoded PNG/JPEG bytes; a typed array subview selects only its own bytes. */
|
|
35
|
+
readonly data: Uint8Array;
|
|
36
|
+
/** Declared image format, checked against the encoded header. */
|
|
37
|
+
readonly mimeType: 'image/png' | 'image/jpeg';
|
|
38
|
+
/** Optional wrapping/filtering; omission retains glTF's implicit sampler. */
|
|
39
|
+
readonly sampler?: MeshTileSampler;
|
|
40
|
+
/** Optional UV transform, preserved as a required KHR_texture_transform extension. */
|
|
41
|
+
readonly transform?: MeshTileTextureTransform;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One glTF metallic-roughness material, shared by every triangle in the mesh. */
|
|
45
|
+
export interface MeshTileMaterial {
|
|
46
|
+
/** Linear RGBA base-color multiplier; components must be finite in [0, 1]. */
|
|
47
|
+
readonly baseColorFactor?: readonly [number, number, number, number];
|
|
48
|
+
/** Optional embedded image, multiplied by baseColorFactor and COLOR_0; requires TEXCOORD_0. */
|
|
49
|
+
readonly baseColorTexture?: MeshTileTexture;
|
|
50
|
+
/** Metallic weight in [0, 1]; omitted values use the glTF default of 1. */
|
|
51
|
+
readonly metallicFactor?: number;
|
|
52
|
+
/** Roughness weight in [0, 1]; omitted values use the glTF default of 1. */
|
|
53
|
+
readonly roughnessFactor?: number;
|
|
54
|
+
/** Alpha interpretation; omitted values use OPAQUE. */
|
|
55
|
+
readonly alphaMode?: 'OPAQUE' | 'MASK' | 'BLEND';
|
|
56
|
+
/** Finite nonnegative threshold, allowed only with MASK; omitted values use 0.5. */
|
|
57
|
+
readonly alphaCutoff?: number;
|
|
58
|
+
/** Whether both sides are rendered; omitted values use false. */
|
|
59
|
+
readonly doubleSided?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Optional appearance for the single-mesh GLB profile. */
|
|
63
|
+
export interface MeshTileOptions {
|
|
64
|
+
/** Explicitly selected material; other texture maps and material extensions are unsupported. */
|
|
65
|
+
readonly material?: MeshTileMaterial;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Encodes one triangle mesh as a self-contained glTF 2.0 GLB resource.
|
|
70
|
+
*
|
|
71
|
+
* POSITION and optional NORMAL must contain packed finite Float32 xyz triples. COLOR_0 accepts
|
|
72
|
+
* packed Float32 linear RGB/RGBA in [0, 1] or normalized Uint8/Uint16 colors per vertex.
|
|
73
|
+
* Normals must have unit length within 0.0001. TEXCOORD_0 accepts packed finite Float32
|
|
74
|
+
* or normalized Uint8/Uint16 UV pairs per vertex. Optional indices use packed unsigned
|
|
75
|
+
* 8-, 16-, or 32-bit values.
|
|
76
|
+
* Unsupported attributes, layouts, and encoded transforms fail rather than being discarded.
|
|
77
|
+
* Coordinates are preserved: callers own CRS conversion, local origins, placement, source material mapping,
|
|
78
|
+
* feature mappings, and tileset packaging. Input arrays are never modified.
|
|
79
|
+
*
|
|
80
|
+
* @param mesh - Triangle-list geometry in the caller's selected local coordinate frame.
|
|
81
|
+
* @param options - Optional single material with an already encoded base-color image.
|
|
82
|
+
* @returns An embedded-buffer GLB containing one mesh, node, and default scene.
|
|
83
|
+
*/
|
|
84
|
+
export function encodeMeshTile(mesh: MeshGeometry, options: MeshTileOptions = {}): ArrayBuffer {
|
|
85
|
+
const geometry = validateMeshGeometry(mesh);
|
|
86
|
+
const {scenegraph, materialIndex} = createMeshTileScenegraph(options, geometry);
|
|
87
|
+
const meshIndex = scenegraph.addMesh({
|
|
88
|
+
attributes: geometry.attributes,
|
|
89
|
+
indices: geometry.indices?.value,
|
|
90
|
+
material: materialIndex,
|
|
91
|
+
mode: 4
|
|
92
|
+
});
|
|
93
|
+
return finalizeMeshTileScenegraph(scenegraph, meshIndex);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Creates the shared material/image scene without allocating uncompressed geometry buffers. */
|
|
97
|
+
export function createMeshTileScenegraph(options: MeshTileOptions, geometry: MeshGeometry) {
|
|
98
|
+
const scenegraph = new GLTFScenegraph({json: {asset: {version: '2.0', generator: 'loaders.gl'}}});
|
|
99
|
+
const material =
|
|
100
|
+
options.material === undefined
|
|
101
|
+
? undefined
|
|
102
|
+
: validateMeshMaterial(options.material, scenegraph, 'TEXCOORD_0' in geometry.attributes);
|
|
103
|
+
return {scenegraph, materialIndex: material ? scenegraph.addMaterial(material) : undefined};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Adds placement-neutral nodes and serializes the single-mesh scene as GLB. */
|
|
107
|
+
export function finalizeMeshTileScenegraph(
|
|
108
|
+
scenegraph: GLTFScenegraph,
|
|
109
|
+
meshIndex: number
|
|
110
|
+
): ArrayBuffer {
|
|
111
|
+
const nodeIndex = scenegraph.addNode({meshIndex});
|
|
112
|
+
const sceneIndex = scenegraph.addScene({nodeIndices: [nodeIndex]});
|
|
113
|
+
scenegraph.setDefaultScene(sceneIndex);
|
|
114
|
+
scenegraph.createBinaryChunk();
|
|
115
|
+
return GLTFWriter.encodeSync!(scenegraph.gltf);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Validates the shared mesh profile; spatial preparation may retain Float64 positions internally. */
|
|
119
|
+
export function validateMeshGeometry(
|
|
120
|
+
mesh: MeshGeometry,
|
|
121
|
+
allowFloat64Positions = false
|
|
122
|
+
): MeshGeometry {
|
|
123
|
+
if (mesh.topology !== 'triangle-list' || mesh.mode !== 4) {
|
|
124
|
+
throw new TileConversionError(
|
|
125
|
+
'MESH_TOPOLOGY_UNSUPPORTED',
|
|
126
|
+
'Only triangle-list mode 4 is supported'
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
for (const name of Object.keys(mesh.attributes)) {
|
|
130
|
+
if (name !== 'POSITION' && name !== 'NORMAL' && name !== 'COLOR_0' && name !== 'TEXCOORD_0') {
|
|
131
|
+
throw new TileConversionError(
|
|
132
|
+
'MESH_ATTRIBUTE_UNSUPPORTED',
|
|
133
|
+
`Mesh attribute ${name} is unsupported`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const positions = getFloatAttribute(mesh.attributes.POSITION, 'POSITION', allowFloat64Positions);
|
|
138
|
+
const attributes: Record<string, MeshAttribute> = {
|
|
139
|
+
POSITION: {value: positions, size: 3}
|
|
140
|
+
};
|
|
141
|
+
if (mesh.attributes.NORMAL) {
|
|
142
|
+
const normals = getFloatAttribute(mesh.attributes.NORMAL, 'NORMAL');
|
|
143
|
+
if (normals.length !== positions.length) {
|
|
144
|
+
throw new TileConversionError(
|
|
145
|
+
'MESH_NORMAL_COUNT_MISMATCH',
|
|
146
|
+
'NORMAL count must match POSITION count'
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
for (let index = 0; index < normals.length; index += 3) {
|
|
150
|
+
if (
|
|
151
|
+
Math.abs(Math.hypot(normals[index], normals[index + 1], normals[index + 2]) - 1) > 0.0001
|
|
152
|
+
) {
|
|
153
|
+
throw new TileConversionError(
|
|
154
|
+
'MESH_NORMAL_INVALID',
|
|
155
|
+
'NORMAL vectors must have unit length'
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
attributes.NORMAL = {value: normals, size: 3};
|
|
160
|
+
}
|
|
161
|
+
if ('COLOR_0' in mesh.attributes) {
|
|
162
|
+
const colors = mesh.attributes.COLOR_0;
|
|
163
|
+
const normalized = isNormalizedUnsignedAttribute(colors);
|
|
164
|
+
if (
|
|
165
|
+
!colors ||
|
|
166
|
+
!(colors.value instanceof Float32Array || normalized) ||
|
|
167
|
+
(colors.size !== 3 && colors.size !== 4) ||
|
|
168
|
+
colors.value.length / colors.size !== positions.length / 3 ||
|
|
169
|
+
(colors.value instanceof Float32Array &&
|
|
170
|
+
colors.value.some(value => !Number.isFinite(value) || value < 0 || value > 1))
|
|
171
|
+
) {
|
|
172
|
+
throw new TileConversionError(
|
|
173
|
+
'MESH_COLOR_INVALID',
|
|
174
|
+
'COLOR_0 requires packed linear RGB/RGBA: Float32 in [0, 1] or normalized Uint8/Uint16, one per vertex'
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
validateAttributeLayout(colors, 'COLOR_0', normalized);
|
|
178
|
+
attributes.COLOR_0 = {
|
|
179
|
+
value: colors.value,
|
|
180
|
+
size: colors.size,
|
|
181
|
+
...(normalized ? {normalized: true} : {})
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if ('TEXCOORD_0' in mesh.attributes) {
|
|
185
|
+
const textureCoordinates = mesh.attributes.TEXCOORD_0;
|
|
186
|
+
const normalized = isNormalizedUnsignedAttribute(textureCoordinates);
|
|
187
|
+
if (
|
|
188
|
+
!textureCoordinates ||
|
|
189
|
+
!(textureCoordinates.value instanceof Float32Array || normalized) ||
|
|
190
|
+
textureCoordinates.size !== 2 ||
|
|
191
|
+
textureCoordinates.value.length / 2 !== positions.length / 3 ||
|
|
192
|
+
textureCoordinates.value.some(value => !Number.isFinite(value))
|
|
193
|
+
) {
|
|
194
|
+
throw new TileConversionError(
|
|
195
|
+
'MESH_TEXCOORD_INVALID',
|
|
196
|
+
'TEXCOORD_0 requires packed finite Float32 or normalized Uint8/Uint16 UV pairs, one per vertex'
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
validateAttributeLayout(textureCoordinates, 'TEXCOORD_0', normalized);
|
|
200
|
+
attributes.TEXCOORD_0 = {
|
|
201
|
+
value: textureCoordinates.value,
|
|
202
|
+
size: 2,
|
|
203
|
+
...(normalized ? {normalized: true} : {})
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const indices = getMeshIndices(mesh.indices, positions.length / 3);
|
|
207
|
+
if (!indices && positions.length % 9 !== 0) {
|
|
208
|
+
throw new TileConversionError(
|
|
209
|
+
'MESH_TRIANGLE_COUNT_INVALID',
|
|
210
|
+
'Nonindexed meshes require complete triangles'
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
topology: 'triangle-list',
|
|
215
|
+
mode: 4,
|
|
216
|
+
attributes,
|
|
217
|
+
indices: indices ? {value: indices, size: 1} : undefined
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Reads one finite packed xyz attribute without changing its storage. */
|
|
222
|
+
function getFloatAttribute(
|
|
223
|
+
attribute: MeshAttribute | undefined,
|
|
224
|
+
name: string,
|
|
225
|
+
allowFloat64 = false
|
|
226
|
+
): Float32Array | Float64Array {
|
|
227
|
+
if (
|
|
228
|
+
!attribute ||
|
|
229
|
+
!(
|
|
230
|
+
attribute.value instanceof Float32Array ||
|
|
231
|
+
(allowFloat64 && attribute.value instanceof Float64Array)
|
|
232
|
+
) ||
|
|
233
|
+
attribute.size !== 3 ||
|
|
234
|
+
attribute.value.length === 0 ||
|
|
235
|
+
attribute.value.length % 3 !== 0
|
|
236
|
+
) {
|
|
237
|
+
throw new TileConversionError(
|
|
238
|
+
'MESH_ATTRIBUTE_INVALID',
|
|
239
|
+
`${name} must contain packed ${allowFloat64 ? 'Float32 or Float64' : 'Float32'} xyz triples`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
validateAttributeLayout(attribute, name);
|
|
243
|
+
if (attribute.value.some(value => !Number.isFinite(value))) {
|
|
244
|
+
throw new TileConversionError('MESH_ATTRIBUTE_NONFINITE', `${name} must contain finite values`);
|
|
245
|
+
}
|
|
246
|
+
return attribute.value;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Reads optional packed triangle indices and checks glTF range and primitive-restart rules. */
|
|
250
|
+
function getMeshIndices(
|
|
251
|
+
attribute: MeshAttribute | undefined,
|
|
252
|
+
vertexCount: number
|
|
253
|
+
): Uint8Array | Uint16Array | Uint32Array | undefined {
|
|
254
|
+
if (!attribute) return undefined;
|
|
255
|
+
validateAttributeLayout(attribute, 'indices');
|
|
256
|
+
const values = attribute.value;
|
|
257
|
+
if (
|
|
258
|
+
attribute.size !== 1 ||
|
|
259
|
+
!(
|
|
260
|
+
values instanceof Uint8Array ||
|
|
261
|
+
values instanceof Uint16Array ||
|
|
262
|
+
values instanceof Uint32Array
|
|
263
|
+
)
|
|
264
|
+
) {
|
|
265
|
+
throw new TileConversionError(
|
|
266
|
+
'MESH_INDICES_INVALID',
|
|
267
|
+
'Indices must contain packed unsigned integer scalars'
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (values.length === 0 || values.length % 3 !== 0) {
|
|
271
|
+
throw new TileConversionError(
|
|
272
|
+
'MESH_TRIANGLE_COUNT_INVALID',
|
|
273
|
+
'Indices must contain complete triangles'
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const restartIndex = 2 ** (values.BYTES_PER_ELEMENT * 8) - 1;
|
|
277
|
+
if (values.some(value => value >= vertexCount || value === restartIndex)) {
|
|
278
|
+
throw new TileConversionError(
|
|
279
|
+
'MESH_INDEX_OUT_OF_RANGE',
|
|
280
|
+
'Indices must reference existing vertices and exclude primitive-restart values'
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
return values;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Identifies explicitly normalized unsigned storage supported by the appearance profile. */
|
|
287
|
+
function isNormalizedUnsignedAttribute(attribute: MeshAttribute | undefined): boolean {
|
|
288
|
+
return (
|
|
289
|
+
attribute?.normalized === true &&
|
|
290
|
+
(attribute.value instanceof Uint8Array || attribute.value instanceof Uint16Array)
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Rejects descriptor layouts and transforms the initial mesh profile cannot represent. */
|
|
295
|
+
function validateAttributeLayout(
|
|
296
|
+
attribute: MeshAttribute,
|
|
297
|
+
name: string,
|
|
298
|
+
allowNormalized = false
|
|
299
|
+
): void {
|
|
300
|
+
if (
|
|
301
|
+
(attribute.byteOffset ?? 0) !== 0 ||
|
|
302
|
+
(attribute.byteStride ?? 0) !== 0 ||
|
|
303
|
+
(attribute.normalized && !allowNormalized) ||
|
|
304
|
+
attribute.transform ||
|
|
305
|
+
attribute.componentType
|
|
306
|
+
) {
|
|
307
|
+
throw new TileConversionError(
|
|
308
|
+
'MESH_ATTRIBUTE_LAYOUT_UNSUPPORTED',
|
|
309
|
+
`${name} must use packed contiguous storage with supported normalization`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Validates and maps the deliberately limited material profile without changing caller objects. */
|
|
315
|
+
function validateMeshMaterial(
|
|
316
|
+
material: MeshTileMaterial,
|
|
317
|
+
scenegraph: GLTFScenegraph,
|
|
318
|
+
hasTextureCoordinates: boolean
|
|
319
|
+
): object {
|
|
320
|
+
if (!material || typeof material !== 'object' || Array.isArray(material)) {
|
|
321
|
+
throw new TileConversionError('MESH_MATERIAL_INVALID', 'Material must be an object');
|
|
322
|
+
}
|
|
323
|
+
const allowedProperties = [
|
|
324
|
+
'baseColorFactor',
|
|
325
|
+
'baseColorTexture',
|
|
326
|
+
'metallicFactor',
|
|
327
|
+
'roughnessFactor',
|
|
328
|
+
'alphaMode',
|
|
329
|
+
'alphaCutoff',
|
|
330
|
+
'doubleSided'
|
|
331
|
+
];
|
|
332
|
+
for (const name of Object.keys(material)) {
|
|
333
|
+
if (!allowedProperties.includes(name)) {
|
|
334
|
+
throw new TileConversionError(
|
|
335
|
+
'MESH_MATERIAL_UNSUPPORTED',
|
|
336
|
+
`Material property ${name} is unsupported`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const {baseColorFactor, metallicFactor, roughnessFactor, alphaMode, alphaCutoff, doubleSided} =
|
|
341
|
+
material;
|
|
342
|
+
if (
|
|
343
|
+
(baseColorFactor !== undefined &&
|
|
344
|
+
(!Array.isArray(baseColorFactor) ||
|
|
345
|
+
baseColorFactor.length !== 4 ||
|
|
346
|
+
Array.from(baseColorFactor).some(
|
|
347
|
+
value => !Number.isFinite(value) || value < 0 || value > 1
|
|
348
|
+
))) ||
|
|
349
|
+
[metallicFactor, roughnessFactor].some(
|
|
350
|
+
value => value !== undefined && (!Number.isFinite(value) || value < 0 || value > 1)
|
|
351
|
+
) ||
|
|
352
|
+
(alphaMode !== undefined && !['OPAQUE', 'MASK', 'BLEND'].includes(alphaMode)) ||
|
|
353
|
+
(alphaCutoff !== undefined &&
|
|
354
|
+
(alphaMode !== 'MASK' || !Number.isFinite(alphaCutoff) || alphaCutoff < 0)) ||
|
|
355
|
+
(doubleSided !== undefined && typeof doubleSided !== 'boolean')
|
|
356
|
+
) {
|
|
357
|
+
throw new TileConversionError(
|
|
358
|
+
'MESH_MATERIAL_INVALID',
|
|
359
|
+
'Material values must satisfy the selected glTF profile'
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
pbrMetallicRoughness: {
|
|
364
|
+
baseColorFactor: baseColorFactor ? [...baseColorFactor] : undefined,
|
|
365
|
+
metallicFactor,
|
|
366
|
+
roughnessFactor,
|
|
367
|
+
baseColorTexture:
|
|
368
|
+
'baseColorTexture' in material
|
|
369
|
+
? encodeBaseColorTexture(material.baseColorTexture, scenegraph, hasTextureCoordinates)
|
|
370
|
+
: undefined
|
|
371
|
+
},
|
|
372
|
+
alphaMode,
|
|
373
|
+
alphaCutoff,
|
|
374
|
+
doubleSided
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Embeds an explicitly selected image without decoding pixels or changing sampler/UV semantics. */
|
|
379
|
+
function encodeBaseColorTexture(
|
|
380
|
+
texture: MeshTileTexture | undefined,
|
|
381
|
+
scenegraph: GLTFScenegraph,
|
|
382
|
+
hasTextureCoordinates: boolean
|
|
383
|
+
): {index: number; extensions?: {KHR_texture_transform: MeshTileTextureTransform}} {
|
|
384
|
+
if (
|
|
385
|
+
!texture ||
|
|
386
|
+
Object.keys(texture).some(
|
|
387
|
+
name => name !== 'data' && name !== 'mimeType' && name !== 'sampler' && name !== 'transform'
|
|
388
|
+
) ||
|
|
389
|
+
!(texture.data instanceof Uint8Array) ||
|
|
390
|
+
!['image/png', 'image/jpeg'].includes(texture.mimeType)
|
|
391
|
+
) {
|
|
392
|
+
throw new TileConversionError(
|
|
393
|
+
'MESH_TEXTURE_INVALID',
|
|
394
|
+
'Base-color texture must supply Uint8Array data, image/png or image/jpeg mimeType, and optional sampling/transform controls'
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
if (!hasTextureCoordinates) {
|
|
398
|
+
throw new TileConversionError(
|
|
399
|
+
'MESH_TEXCOORD_REQUIRED',
|
|
400
|
+
'Base-color texture requires TEXCOORD_0'
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
const image = new DataView(texture.data.buffer, texture.data.byteOffset, texture.data.byteLength);
|
|
404
|
+
let metadata;
|
|
405
|
+
try {
|
|
406
|
+
metadata = getBinaryImageMetadata(image);
|
|
407
|
+
} catch {
|
|
408
|
+
// Malformed encoded headers must produce the same typed diagnostic as unrecognized headers.
|
|
409
|
+
}
|
|
410
|
+
if (
|
|
411
|
+
!metadata ||
|
|
412
|
+
metadata.mimeType !== texture.mimeType ||
|
|
413
|
+
metadata.width <= 0 ||
|
|
414
|
+
metadata.height <= 0
|
|
415
|
+
) {
|
|
416
|
+
throw new TileConversionError(
|
|
417
|
+
'MESH_TEXTURE_INVALID',
|
|
418
|
+
'Image header must match mimeType and declare positive dimensions'
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
const samplerIndex =
|
|
422
|
+
'sampler' in texture ? scenegraph.addSampler(validateMeshSampler(texture.sampler)) : undefined;
|
|
423
|
+
const transform =
|
|
424
|
+
'transform' in texture ? validateMeshTextureTransform(texture.transform) : undefined;
|
|
425
|
+
if (transform) scenegraph.registerRequiredExtension('KHR_texture_transform');
|
|
426
|
+
return {
|
|
427
|
+
...(transform ? {extensions: {KHR_texture_transform: transform}} : {}),
|
|
428
|
+
index: scenegraph.addTexture({
|
|
429
|
+
imageIndex: scenegraph.addImage(image, texture.mimeType),
|
|
430
|
+
samplerIndex
|
|
431
|
+
})
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Validates the four sampling controls without silently discarding unsupported properties. */
|
|
436
|
+
function validateMeshSampler(sampler: MeshTileSampler | undefined): MeshTileSampler {
|
|
437
|
+
if (!sampler || typeof sampler !== 'object' || Array.isArray(sampler)) {
|
|
438
|
+
throw new TileConversionError('MESH_SAMPLER_INVALID', 'Sampler must be an object');
|
|
439
|
+
}
|
|
440
|
+
const allowedValues = {
|
|
441
|
+
wrapS: [33071, 33648, 10497],
|
|
442
|
+
wrapT: [33071, 33648, 10497],
|
|
443
|
+
magFilter: [9728, 9729],
|
|
444
|
+
minFilter: [9728, 9729, 9984, 9985, 9986, 9987]
|
|
445
|
+
};
|
|
446
|
+
for (const name in sampler) {
|
|
447
|
+
if (!Object.hasOwn(allowedValues, name)) {
|
|
448
|
+
throw new TileConversionError(
|
|
449
|
+
'MESH_SAMPLER_UNSUPPORTED',
|
|
450
|
+
`Sampler property ${name} is unsupported`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const selectedSampler = {
|
|
455
|
+
wrapS: sampler.wrapS,
|
|
456
|
+
wrapT: sampler.wrapT,
|
|
457
|
+
minFilter: sampler.minFilter,
|
|
458
|
+
magFilter: sampler.magFilter
|
|
459
|
+
};
|
|
460
|
+
for (const name of Object.keys(allowedValues)) {
|
|
461
|
+
const property = name as keyof MeshTileSampler;
|
|
462
|
+
if (
|
|
463
|
+
selectedSampler[property] !== undefined &&
|
|
464
|
+
!allowedValues[property].includes(selectedSampler[property]!)
|
|
465
|
+
) {
|
|
466
|
+
throw new TileConversionError(
|
|
467
|
+
'MESH_SAMPLER_INVALID',
|
|
468
|
+
`Sampler ${name} must use a glTF wrapping/filtering value`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return selectedSampler;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Copies finite UV controls and rejects unmapped extension semantics. */
|
|
476
|
+
function validateMeshTextureTransform(
|
|
477
|
+
transform: MeshTileTextureTransform | undefined
|
|
478
|
+
): MeshTileTextureTransform {
|
|
479
|
+
if (!transform || typeof transform !== 'object' || Array.isArray(transform)) {
|
|
480
|
+
throw new TileConversionError(
|
|
481
|
+
'MESH_TEXTURE_TRANSFORM_INVALID',
|
|
482
|
+
'Texture transform must be an object'
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
for (const name in transform) {
|
|
486
|
+
if (name !== 'offset' && name !== 'rotation' && name !== 'scale') {
|
|
487
|
+
throw new TileConversionError(
|
|
488
|
+
'MESH_TEXTURE_TRANSFORM_UNSUPPORTED',
|
|
489
|
+
`Texture transform property ${name} is unsupported`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const {offset, rotation, scale} = transform;
|
|
494
|
+
const offsetValues =
|
|
495
|
+
Array.isArray(offset) && offset.length === 2 ? [offset[0], offset[1]] : offset;
|
|
496
|
+
const scaleValues = Array.isArray(scale) && scale.length === 2 ? [scale[0], scale[1]] : scale;
|
|
497
|
+
if (
|
|
498
|
+
[offsetValues, scaleValues].some(
|
|
499
|
+
value =>
|
|
500
|
+
value !== undefined &&
|
|
501
|
+
(!Array.isArray(value) ||
|
|
502
|
+
value.length !== 2 ||
|
|
503
|
+
value.some(component => !Number.isFinite(component)))
|
|
504
|
+
) ||
|
|
505
|
+
(rotation !== undefined && !Number.isFinite(rotation))
|
|
506
|
+
) {
|
|
507
|
+
throw new TileConversionError(
|
|
508
|
+
'MESH_TEXTURE_TRANSFORM_INVALID',
|
|
509
|
+
'Texture transform requires finite UV pairs and a finite rotation'
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
offset: offsetValues === undefined ? undefined : [offsetValues[0], offsetValues[1]],
|
|
514
|
+
rotation,
|
|
515
|
+
scale: scaleValues === undefined ? undefined : [scaleValues[0], scaleValues[1]]
|
|
516
|
+
};
|
|
517
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {PointCloudTilesetSource} from '@loaders.gl/tiles';
|
|
6
|
+
import {convertTileset} from '@loaders.gl/tile-converter/v5/core';
|
|
7
|
+
import type {
|
|
8
|
+
TileConversionProgress,
|
|
9
|
+
TileConversionReport,
|
|
10
|
+
TileConversionSink
|
|
11
|
+
} from '@loaders.gl/tile-converter/v5/core';
|
|
12
|
+
import {traversePointCloudSource} from '@loaders.gl/tile-converter/v5/core';
|
|
13
|
+
import type {PointCloudSourceTile} from '@loaders.gl/tile-converter/v5/core';
|
|
14
|
+
import {encodePointCloudSourceTile} from './point-cloud-source-encoder.js';
|
|
15
|
+
import type {
|
|
16
|
+
EncodedPointCloudSourceTile,
|
|
17
|
+
EncodePointCloudSourceOptions
|
|
18
|
+
} from './point-cloud-source-encoder.js';
|
|
19
|
+
|
|
20
|
+
/** Point-cloud encoding options and an application-owned output destination. */
|
|
21
|
+
export interface ConvertPointCloudSourceOptions extends EncodePointCloudSourceOptions {
|
|
22
|
+
/** Destination receiving PNTS resources and their source placement metadata. */
|
|
23
|
+
readonly sink: TileConversionSink<EncodedPointCloudSourceTile>;
|
|
24
|
+
/** Input byte accounting for each visited source tile, including tiles with null content. */
|
|
25
|
+
readonly measureInputBytes: (sourceTile: PointCloudSourceTile) => number;
|
|
26
|
+
/** Maximum caller-measured bytes in one decoded source tile, checked before PNTS encoding. */
|
|
27
|
+
readonly maxInputResourceBytes?: number;
|
|
28
|
+
/** Maximum bytes in one encoded PNTS resource. Defaults to no limit. */
|
|
29
|
+
readonly maxOutputResourceBytes?: number;
|
|
30
|
+
/** Receive conversion progress, including resource counts and caller-measured input bytes. */
|
|
31
|
+
readonly onProgress?: (progress: TileConversionProgress) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Converts a point-cloud source to PNTS resources through the v5 output-sink lifecycle.
|
|
36
|
+
*
|
|
37
|
+
* Each write is awaited before reading the next source tile. The destination is finalized only
|
|
38
|
+
* after traversal and writes succeed, and aborted on read, encoding, write, or cancellation errors.
|
|
39
|
+
* Input resource counts include empty tiles; output counts and byte sizes describe emitted PNTS
|
|
40
|
+
* resources. Applications supply input byte accounting because source tiles contain decoded Arrow
|
|
41
|
+
* data rather than transport bytes. The caller retains ownership of the input source.
|
|
42
|
+
*
|
|
43
|
+
* @param source - An I3S, COPC, or compatible point-cloud source to initialize and traverse.
|
|
44
|
+
* @param options - Destination, byte accounting, traversal limits, and per-tile encoding options.
|
|
45
|
+
* @returns The shared v5 conversion report after destination finalization.
|
|
46
|
+
*/
|
|
47
|
+
export function convertPointCloudSource(
|
|
48
|
+
source: PointCloudTilesetSource,
|
|
49
|
+
options: ConvertPointCloudSourceOptions
|
|
50
|
+
): Promise<TileConversionReport> {
|
|
51
|
+
return convertTileset({
|
|
52
|
+
source: {
|
|
53
|
+
async inspect() {
|
|
54
|
+
if (!source.isReady) {
|
|
55
|
+
await source.initialize();
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
read: (_inspection, signal) =>
|
|
59
|
+
traversePointCloudSource(source, {maxDepth: options.maxDepth, signal})
|
|
60
|
+
},
|
|
61
|
+
codec: {
|
|
62
|
+
async *convert(sourceTile) {
|
|
63
|
+
const encodingOptions = sourceTile.content
|
|
64
|
+
? options.getTileEncodingOptions?.(sourceTile)
|
|
65
|
+
: undefined;
|
|
66
|
+
const encodedTile = encodePointCloudSourceTile(sourceTile, encodingOptions);
|
|
67
|
+
if (encodedTile) {
|
|
68
|
+
yield encodedTile;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
sink: options.sink,
|
|
73
|
+
measureInputBytes: options.measureInputBytes,
|
|
74
|
+
measureOutputBytes: resource => resource.pnts.byteLength,
|
|
75
|
+
maxInputResourceBytes: options.maxInputResourceBytes,
|
|
76
|
+
maxOutputResourceBytes: options.maxOutputResourceBytes,
|
|
77
|
+
signal: options.signal,
|
|
78
|
+
onProgress: options.onProgress
|
|
79
|
+
});
|
|
80
|
+
}
|