@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,200 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {getSpatialCoordinateFrame} from '@loaders.gl/tiles';
|
|
6
|
+
import type {TilesetSpatialReference} from '@loaders.gl/tiles';
|
|
7
|
+
import type {MeshGeometry} from '@loaders.gl/schema';
|
|
8
|
+
import type {LoadLibraryOptions} from '@loaders.gl/worker-utils';
|
|
9
|
+
import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
|
|
10
|
+
import type {TileConversionCodec} from '@loaders.gl/tile-converter/v5/core';
|
|
11
|
+
import type {Tiles3DConversionSpatialContext} from '@loaders.gl/tile-converter/v5/core';
|
|
12
|
+
import {encodeMeshTile, validateMeshGeometry} from './mesh.js';
|
|
13
|
+
import type {MeshTileMaterial} from './mesh.js';
|
|
14
|
+
import {encodeDracoMeshTile} from './mesh-draco.js';
|
|
15
|
+
|
|
16
|
+
/** Explicitly selected geometry, with positions already in the spatial context's source frame. */
|
|
17
|
+
export interface MeshConversionInput {
|
|
18
|
+
/** Caller-owned resource identifier, forwarded to the output sink. */
|
|
19
|
+
readonly id: string;
|
|
20
|
+
/** Triangle geometry with absolute packed Float32 or Float64 source positions. */
|
|
21
|
+
readonly mesh: MeshGeometry;
|
|
22
|
+
/** Optional single material with an embedded base-color image, mapped explicitly by the source adapter. */
|
|
23
|
+
readonly material?: MeshTileMaterial;
|
|
24
|
+
/** Finite xyz origin in the target frame, subtracted before float32 encoding. */
|
|
25
|
+
readonly origin: readonly [number, number, number];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A completed GLB resource and the metadata needed to place its local geometry. */
|
|
29
|
+
export interface EncodedMeshConversionResource {
|
|
30
|
+
/** Caller-owned resource identifier. */
|
|
31
|
+
readonly id: string;
|
|
32
|
+
/** Self-contained GLB with local positions and transformed normals. */
|
|
33
|
+
readonly glb: ArrayBuffer;
|
|
34
|
+
/** Target-frame translation to add to the encoded positions. */
|
|
35
|
+
readonly origin: readonly [number, number, number];
|
|
36
|
+
/** Target-frame bounds of the reconstructed encoded vertices, including float32 rounding. */
|
|
37
|
+
readonly boundingBox: readonly [readonly number[], readonly number[]];
|
|
38
|
+
/** Bounds of encoded local vertices before adding the origin, without absolute-coordinate cancellation. */
|
|
39
|
+
readonly localBoundingBox: readonly [readonly number[], readonly number[]];
|
|
40
|
+
/** Metadata describing the output coordinate operation and height reference. */
|
|
41
|
+
readonly spatialReference: TilesetSpatialReference;
|
|
42
|
+
/** Largest measured Euclidean reconstruction error in target coordinate units. */
|
|
43
|
+
readonly maximumPositionError: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Spatial policy used by the concrete mesh conversion codec. */
|
|
47
|
+
export interface MeshConversionCodecOptions {
|
|
48
|
+
/** Shared context that accepts absolute source positions and returns absolute target positions. */
|
|
49
|
+
readonly spatialContext: Tiles3DConversionSpatialContext;
|
|
50
|
+
/** Maximum Euclidean reconstruction error per vertex, in target coordinate units. */
|
|
51
|
+
readonly maxPositionError: number;
|
|
52
|
+
/** Lossless Draco Edge Breaker is enabled by default; false emits uncompressed GLB geometry. */
|
|
53
|
+
readonly draco?: boolean;
|
|
54
|
+
/** Optional local/CDN library locations or injected Draco runtimes for compression and verification. */
|
|
55
|
+
readonly dracoLibraryOptions?: LoadLibraryOptions;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Creates a concrete mesh codec for `convertTileset`.
|
|
60
|
+
*
|
|
61
|
+
* Source adapters must select geometry and apply source placement before supplying absolute
|
|
62
|
+
* positions and matching normals. This codec applies the supplied spatial context once, rebases
|
|
63
|
+
* around each input's target origin, and rejects rounding above the explicit error budget.
|
|
64
|
+
* Lossless Draco Edge Breaker is enabled by default; decoded positions determine output bounds.
|
|
65
|
+
* Set draco to false for uncompressed geometry. Quantization is not enabled by this profile.
|
|
66
|
+
* Geographic and unknown output frames are rejected. The returned origin/bounds/reference are
|
|
67
|
+
* external placement metadata; applications own axis conventions, hierarchy, and packaging.
|
|
68
|
+
* Input arrays are not mutated. Core input/output byte gates, cancellation, awaited writes, and
|
|
69
|
+
* destination finalize/abort handling apply when used through `convertTileset`.
|
|
70
|
+
*
|
|
71
|
+
* @param options - Resolved spatial context and finite nonnegative target-unit precision budget.
|
|
72
|
+
* @returns One GLB and matching metadata per input geometry.
|
|
73
|
+
*/
|
|
74
|
+
export function createMeshConversionCodec<TInspection = unknown>(
|
|
75
|
+
options: MeshConversionCodecOptions
|
|
76
|
+
): TileConversionCodec<TInspection, MeshConversionInput, EncodedMeshConversionResource> {
|
|
77
|
+
const {spatialContext, maxPositionError, draco = true, dracoLibraryOptions} = options;
|
|
78
|
+
if (typeof draco !== 'boolean') {
|
|
79
|
+
throw new TileConversionError('MESH_DRACO_OPTIONS_INVALID', 'draco must be a boolean');
|
|
80
|
+
}
|
|
81
|
+
if (!Number.isFinite(maxPositionError) || maxPositionError < 0) {
|
|
82
|
+
throw new TileConversionError(
|
|
83
|
+
'MESH_PRECISION_LIMIT_INVALID',
|
|
84
|
+
'maxPositionError must be finite and nonnegative'
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const spatialReference = spatialContext.spatialReference;
|
|
88
|
+
const outputCrs = spatialReference.targetCrs || spatialReference.sourceCrs;
|
|
89
|
+
const outputFrame =
|
|
90
|
+
spatialReference.status === 'native'
|
|
91
|
+
? spatialReference.coordinateFrame
|
|
92
|
+
: outputCrs
|
|
93
|
+
? getSpatialCoordinateFrame(outputCrs)
|
|
94
|
+
: 'unknown';
|
|
95
|
+
if (
|
|
96
|
+
!['native', 'transformed'].includes(spatialReference.status) ||
|
|
97
|
+
!['projected', 'geocentric', 'local', 'cartesian'].includes(outputFrame)
|
|
98
|
+
) {
|
|
99
|
+
throw new TileConversionError(
|
|
100
|
+
'MESH_OUTPUT_FRAME_UNSUPPORTED',
|
|
101
|
+
'Mesh output requires a resolved Cartesian coordinate frame'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
/** Prepares and encodes one geometry while honoring cancellation. */
|
|
106
|
+
async *convert(resource, _inspection, signal) {
|
|
107
|
+
signal?.throwIfAborted();
|
|
108
|
+
if ('features' in resource && resource.features) {
|
|
109
|
+
throw new TileConversionError(
|
|
110
|
+
'MESH_FEATURE_OUTPUT_UNSUPPORTED',
|
|
111
|
+
'The GLB codec requires an explicit feature metadata writer; use the I3S codec for supported feature-bearing meshes'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const geometry = validateMeshGeometry(resource.mesh, true);
|
|
115
|
+
const sourcePositions = geometry.attributes.POSITION.value;
|
|
116
|
+
const origin: [number, number, number] = [...resource.origin];
|
|
117
|
+
if (origin.length !== 3 || origin.some(value => !Number.isFinite(value))) {
|
|
118
|
+
throw new TileConversionError(
|
|
119
|
+
'MESH_ORIGIN_INVALID',
|
|
120
|
+
'Target origin must contain three finite coordinates'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const targetPositions = spatialContext.transformPositions(sourcePositions);
|
|
124
|
+
if (
|
|
125
|
+
targetPositions.length !== sourcePositions.length ||
|
|
126
|
+
targetPositions.some(value => !Number.isFinite(value))
|
|
127
|
+
) {
|
|
128
|
+
throw new TileConversionError(
|
|
129
|
+
'MESH_TRANSFORM_INVALID',
|
|
130
|
+
'Spatial transformation must return one finite xyz position per vertex'
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const localPositions = new Float32Array(targetPositions.length);
|
|
134
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
135
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
136
|
+
const localMinimum = [Infinity, Infinity, Infinity];
|
|
137
|
+
const localMaximum = [-Infinity, -Infinity, -Infinity];
|
|
138
|
+
let maximumPositionError = 0;
|
|
139
|
+
for (let index = 0; index < targetPositions.length; index += 3) {
|
|
140
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
141
|
+
localPositions[index + axis] = targetPositions[index + axis] - origin[axis];
|
|
142
|
+
}
|
|
143
|
+
const positionError = Math.hypot(
|
|
144
|
+
localPositions[index] + origin[0] - targetPositions[index],
|
|
145
|
+
localPositions[index + 1] + origin[1] - targetPositions[index + 1],
|
|
146
|
+
localPositions[index + 2] + origin[2] - targetPositions[index + 2]
|
|
147
|
+
);
|
|
148
|
+
if (!Number.isFinite(positionError) || positionError > maxPositionError) {
|
|
149
|
+
throw new TileConversionError(
|
|
150
|
+
'MESH_POSITION_PRECISION_EXCEEDED',
|
|
151
|
+
'Float32 mesh positions exceed the configured reconstruction error budget'
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
maximumPositionError = Math.max(maximumPositionError, positionError);
|
|
155
|
+
}
|
|
156
|
+
geometry.attributes.POSITION = {value: localPositions, size: 3};
|
|
157
|
+
if (geometry.attributes.NORMAL) {
|
|
158
|
+
geometry.attributes.NORMAL = {
|
|
159
|
+
value: spatialContext.transformNormals(geometry.attributes.NORMAL.value, sourcePositions),
|
|
160
|
+
size: 3
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const appearance = {material: resource.material};
|
|
164
|
+
const encoded = draco
|
|
165
|
+
? await encodeDracoMeshTile(geometry, appearance, dracoLibraryOptions)
|
|
166
|
+
: {glb: encodeMeshTile(geometry, appearance), positions: localPositions};
|
|
167
|
+
signal?.throwIfAborted();
|
|
168
|
+
for (let index = 0; index < encoded.positions.length; index++) {
|
|
169
|
+
const axis = index % 3;
|
|
170
|
+
const position = encoded.positions[index];
|
|
171
|
+
localMinimum[axis] = Math.min(localMinimum[axis], position);
|
|
172
|
+
localMaximum[axis] = Math.max(localMaximum[axis], position);
|
|
173
|
+
minimum[axis] = Math.min(minimum[axis], position + origin[axis]);
|
|
174
|
+
maximum[axis] = Math.max(maximum[axis], position + origin[axis]);
|
|
175
|
+
}
|
|
176
|
+
yield {
|
|
177
|
+
id: resource.id,
|
|
178
|
+
glb: encoded.glb,
|
|
179
|
+
origin,
|
|
180
|
+
boundingBox: [minimum, maximum],
|
|
181
|
+
localBoundingBox: [localMinimum, localMaximum],
|
|
182
|
+
spatialReference,
|
|
183
|
+
maximumPositionError
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
/** Reports position rounding authorized by the configured precision budget. */
|
|
187
|
+
async validateOutput(resource) {
|
|
188
|
+
return resource.maximumPositionError > 0
|
|
189
|
+
? [
|
|
190
|
+
{
|
|
191
|
+
code: 'MESH_POSITION_ROUNDING',
|
|
192
|
+
message: `Encoded positions have up to ${resource.maximumPositionError} target-unit reconstruction error`,
|
|
193
|
+
severity: 'info' as const,
|
|
194
|
+
resourceId: resource.id
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
: [];
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {DracoLoader, encodeDraco, type DracoMesh} from '@loaders.gl/draco';
|
|
6
|
+
import type {GLTFAccessor} from '@loaders.gl/gltf';
|
|
7
|
+
import type {MeshGeometry} from '@loaders.gl/schema';
|
|
8
|
+
import type {LoadLibraryOptions} from '@loaders.gl/worker-utils';
|
|
9
|
+
import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
|
|
10
|
+
import {
|
|
11
|
+
createMeshTileScenegraph,
|
|
12
|
+
finalizeMeshTileScenegraph,
|
|
13
|
+
type MeshTileOptions,
|
|
14
|
+
validateMeshGeometry
|
|
15
|
+
} from './mesh.js';
|
|
16
|
+
|
|
17
|
+
const DRACO_EXTENSION = 'KHR_draco_mesh_compression';
|
|
18
|
+
|
|
19
|
+
/** Encodes the validated single-mesh profile with lossless Edge Breaker and no raw fallback. */
|
|
20
|
+
export async function encodeDracoMeshTile(
|
|
21
|
+
geometry: MeshGeometry,
|
|
22
|
+
options: MeshTileOptions,
|
|
23
|
+
libraryOptions: LoadLibraryOptions = {}
|
|
24
|
+
): Promise<{glb: ArrayBuffer; positions: Float32Array}> {
|
|
25
|
+
geometry = validateMeshGeometry(geometry);
|
|
26
|
+
// Draco requires explicit faces even when the input triangle list is non-indexed.
|
|
27
|
+
if (!geometry.indices) {
|
|
28
|
+
geometry.indices = {
|
|
29
|
+
size: 1,
|
|
30
|
+
value: Uint32Array.from(
|
|
31
|
+
{length: geometry.attributes.POSITION.value.length / 3},
|
|
32
|
+
(_, index) => index
|
|
33
|
+
)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const {modules, useLocalLibraries, CDN: contentDeliveryNetwork} = libraryOptions;
|
|
37
|
+
const runtimeOptions = {modules, core: {useLocalLibraries, CDN: contentDeliveryNetwork}};
|
|
38
|
+
const {scenegraph, materialIndex} = createMeshTileScenegraph(options, geometry);
|
|
39
|
+
const encoded = await encodeDraco(geometry, {
|
|
40
|
+
...runtimeOptions,
|
|
41
|
+
draco: {method: 'MESH_EDGEBREAKER_ENCODING'}
|
|
42
|
+
});
|
|
43
|
+
const loaderOptions = {...runtimeOptions, draco: {shape: 'mesh' as const}};
|
|
44
|
+
const loader = await DracoLoader.preload('', loaderOptions);
|
|
45
|
+
const decoded = (await loader.parse!(encoded.data, loaderOptions)) as DracoMesh;
|
|
46
|
+
const positions = decoded.attributes.POSITION.value;
|
|
47
|
+
if (
|
|
48
|
+
!(positions instanceof Float32Array) ||
|
|
49
|
+
positions.length !== encoded.report.pointCount * 3 ||
|
|
50
|
+
encoded.report.pointCount === 0 ||
|
|
51
|
+
!decoded.indices ||
|
|
52
|
+
decoded.indices.value.length !== encoded.report.faceCount * 3 ||
|
|
53
|
+
encoded.report.faceCount * 3 !==
|
|
54
|
+
(geometry.indices?.value.length ?? geometry.attributes.POSITION.value.length / 3)
|
|
55
|
+
) {
|
|
56
|
+
throw new TileConversionError(
|
|
57
|
+
'MESH_DRACO_ENCODING_INVALID',
|
|
58
|
+
'Draco must preserve the triangle count and emit finite positions'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
// Lossy quantization is deliberately unavailable in this initial profile.
|
|
62
|
+
const inputPositions = geometry.attributes.POSITION.value;
|
|
63
|
+
const positionKeys = new Set<string>();
|
|
64
|
+
for (let index = 0; index < inputPositions.length; index += 3) {
|
|
65
|
+
positionKeys.add(getPositionKey(inputPositions, index));
|
|
66
|
+
}
|
|
67
|
+
for (let index = 0; index < positions.length; index += 3) {
|
|
68
|
+
if (!positionKeys.has(getPositionKey(positions, index))) {
|
|
69
|
+
throw new TileConversionError(
|
|
70
|
+
'MESH_DRACO_POSITION_CHANGED',
|
|
71
|
+
'Lossless Draco must preserve the encoded Float32 positions'
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const bufferView = scenegraph.addBufferView(encoded.data);
|
|
76
|
+
const attributes: Record<string, number> = {};
|
|
77
|
+
const attributeIdentifiers: Record<string, number> = {};
|
|
78
|
+
scenegraph.json.accessors = [];
|
|
79
|
+
for (const [name, attribute] of Object.entries(geometry.attributes)) {
|
|
80
|
+
const decodedAttribute = decoded.attributes[name];
|
|
81
|
+
if (
|
|
82
|
+
!decodedAttribute ||
|
|
83
|
+
decodedAttribute.value.length !== encoded.report.pointCount * attribute.size
|
|
84
|
+
) {
|
|
85
|
+
throw new TileConversionError('MESH_DRACO_ENCODING_INVALID', `Draco must preserve ${name}`);
|
|
86
|
+
}
|
|
87
|
+
const accessor: GLTFAccessor = {
|
|
88
|
+
componentType:
|
|
89
|
+
attribute.value instanceof Uint8Array
|
|
90
|
+
? 5121
|
|
91
|
+
: attribute.value instanceof Uint16Array
|
|
92
|
+
? 5123
|
|
93
|
+
: 5126,
|
|
94
|
+
count: encoded.report.pointCount,
|
|
95
|
+
type: attribute.size === 2 ? 'VEC2' : attribute.size === 3 ? 'VEC3' : 'VEC4',
|
|
96
|
+
...(attribute.normalized ? {normalized: true} : {})
|
|
97
|
+
};
|
|
98
|
+
if (name === 'POSITION') {
|
|
99
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
100
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
101
|
+
for (let index = 0; index < positions.length; index++) {
|
|
102
|
+
const axis = index % 3;
|
|
103
|
+
minimum[axis] = Math.min(minimum[axis], positions[index]);
|
|
104
|
+
maximum[axis] = Math.max(maximum[axis], positions[index]);
|
|
105
|
+
}
|
|
106
|
+
accessor.min = minimum;
|
|
107
|
+
accessor.max = maximum;
|
|
108
|
+
}
|
|
109
|
+
attributes[name] = scenegraph.json.accessors.push(accessor) - 1;
|
|
110
|
+
attributeIdentifiers[name] = encoded.report.attributes[name].id;
|
|
111
|
+
}
|
|
112
|
+
const indices =
|
|
113
|
+
scenegraph.json.accessors.push({
|
|
114
|
+
componentType: decoded.indices.value instanceof Uint16Array ? 5123 : 5125,
|
|
115
|
+
count: encoded.report.faceCount * 3,
|
|
116
|
+
type: 'SCALAR'
|
|
117
|
+
}) - 1;
|
|
118
|
+
scenegraph.json.meshes = [
|
|
119
|
+
{
|
|
120
|
+
primitives: [
|
|
121
|
+
{
|
|
122
|
+
attributes,
|
|
123
|
+
indices,
|
|
124
|
+
mode: 4,
|
|
125
|
+
...(materialIndex === undefined ? {} : {material: materialIndex}),
|
|
126
|
+
extensions: {[DRACO_EXTENSION]: {bufferView, attributes: attributeIdentifiers}}
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
];
|
|
131
|
+
scenegraph.registerRequiredExtension(DRACO_EXTENSION);
|
|
132
|
+
return {glb: finalizeMeshTileScenegraph(scenegraph, 0), positions};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Identifies one exact finite Float32 position independently of Draco vertex ordering. */
|
|
136
|
+
function getPositionKey(positions: ArrayLike<number>, index: number): string {
|
|
137
|
+
return `${positions[index]},${positions[index + 1]},${positions[index + 2]}`;
|
|
138
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {MeshGeometry} from '@loaders.gl/schema';
|
|
6
|
+
import type {GLTFPostprocessed, GLTFMeshPrimitivePostprocessed} from '@loaders.gl/gltf';
|
|
7
|
+
import type {I3SMeshFeatures} from '@loaders.gl/i3s';
|
|
8
|
+
import {convertFeatureAttributesToArrowBatches} from './feature-arrow.js';
|
|
9
|
+
import type {FeatureArrowBatchOptions} from './feature-arrow.js';
|
|
10
|
+
import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
|
|
11
|
+
|
|
12
|
+
/** Explicit source feature mapping; target type changes must be authorized separately. */
|
|
13
|
+
export interface MeshSourceFeatureOptions extends FeatureArrowBatchOptions {
|
|
14
|
+
/** Batch-table class label, or the exact structural-metadata class to select. */
|
|
15
|
+
readonly metadataClass: string;
|
|
16
|
+
/** Optional source property providing stable identifiers; otherwise row indices are stable within the content. */
|
|
17
|
+
readonly sourceFeatureIdProperty?: string;
|
|
18
|
+
/** Explicit I3S representation for 64-bit integers; omission rejects those target fields. */
|
|
19
|
+
readonly integer64Encoding?: 'decimal-string';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Extracts legacy batch tables or one decoded modern property table and validates geometry ownership. */
|
|
23
|
+
export function extractMeshFeatures(
|
|
24
|
+
gltf: GLTFPostprocessed,
|
|
25
|
+
primitive: GLTFMeshPrimitivePostprocessed,
|
|
26
|
+
payload: {
|
|
27
|
+
batchTableJson?: Record<string, unknown>;
|
|
28
|
+
header?: {batchLength?: number};
|
|
29
|
+
featureTableJson?: Record<string, unknown>;
|
|
30
|
+
},
|
|
31
|
+
mesh: MeshGeometry,
|
|
32
|
+
options?: MeshSourceFeatureOptions
|
|
33
|
+
): I3SMeshFeatures | undefined {
|
|
34
|
+
const declarations = primitive.extensions?.EXT_mesh_features?.featureIds;
|
|
35
|
+
const metadata = gltf.extensions?.EXT_structural_metadata;
|
|
36
|
+
const legacy = primitive.attributes._BATCHID;
|
|
37
|
+
if (
|
|
38
|
+
!declarations &&
|
|
39
|
+
!legacy &&
|
|
40
|
+
!metadata &&
|
|
41
|
+
!Object.keys(payload.batchTableJson || {}).length &&
|
|
42
|
+
!payload.header?.batchLength
|
|
43
|
+
) {
|
|
44
|
+
if (Object.keys(primitive.attributes).some(name => /^_FEATURE_ID_\d+$/.test(name)))
|
|
45
|
+
throw new TileConversionError(
|
|
46
|
+
'MESH_FEATURE_MAPPING_REQUIRED',
|
|
47
|
+
'Feature attributes require an EXT_mesh_features declaration'
|
|
48
|
+
);
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
if (!options)
|
|
52
|
+
throw new TileConversionError(
|
|
53
|
+
'MESH_FEATURE_SCHEMA_REQUIRED',
|
|
54
|
+
'Feature extraction requires an explicit Arrow schema and class mapping'
|
|
55
|
+
);
|
|
56
|
+
if (!options.metadataClass)
|
|
57
|
+
throw new TileConversionError(
|
|
58
|
+
'MESH_FEATURE_CLASS_REQUIRED',
|
|
59
|
+
'Feature metadata class must be explicit'
|
|
60
|
+
);
|
|
61
|
+
let count: number;
|
|
62
|
+
let attribute = legacy;
|
|
63
|
+
let properties: Record<string, ArrayLike<unknown>>;
|
|
64
|
+
if (declarations) {
|
|
65
|
+
if (
|
|
66
|
+
legacy ||
|
|
67
|
+
declarations.length !== 1 ||
|
|
68
|
+
metadata?.propertyTables?.length !== 1 ||
|
|
69
|
+
metadata.propertyTextures?.length ||
|
|
70
|
+
metadata.propertyAttributes?.length
|
|
71
|
+
)
|
|
72
|
+
throw new TileConversionError(
|
|
73
|
+
'MESH_FEATURE_MAPPING_UNSUPPORTED',
|
|
74
|
+
'Only one attribute-backed feature set and one property table are supported'
|
|
75
|
+
);
|
|
76
|
+
const declaration = declarations[0];
|
|
77
|
+
if (
|
|
78
|
+
!Number.isInteger(declaration.attribute) ||
|
|
79
|
+
declaration.attribute < 0 ||
|
|
80
|
+
declaration.propertyTable !== 0 ||
|
|
81
|
+
declaration.texture ||
|
|
82
|
+
declaration.implicit ||
|
|
83
|
+
declaration.nullFeatureId !== undefined
|
|
84
|
+
)
|
|
85
|
+
throw new TileConversionError(
|
|
86
|
+
'MESH_FEATURE_MAPPING_UNSUPPORTED',
|
|
87
|
+
'Features require an attribute and property table 0 without null/texture/implicit IDs'
|
|
88
|
+
);
|
|
89
|
+
attribute = primitive.attributes[`_FEATURE_ID_${declaration.attribute}`];
|
|
90
|
+
const table = metadata.propertyTables[0];
|
|
91
|
+
if (table.class !== options.metadataClass || table.count !== declaration.featureCount)
|
|
92
|
+
throw new TileConversionError(
|
|
93
|
+
'MESH_FEATURE_CLASS_MISMATCH',
|
|
94
|
+
'Feature class and declared table counts must agree'
|
|
95
|
+
);
|
|
96
|
+
count = table.count;
|
|
97
|
+
properties = {};
|
|
98
|
+
const classProperties = metadata.schema?.classes?.[table.class]?.properties;
|
|
99
|
+
if (!classProperties)
|
|
100
|
+
throw new TileConversionError(
|
|
101
|
+
'MESH_FEATURE_SCHEMA_REQUIRED',
|
|
102
|
+
'A decoded inline structural metadata class is required'
|
|
103
|
+
);
|
|
104
|
+
if (Object.keys(table.properties || {}).some(name => !Object.hasOwn(classProperties, name)))
|
|
105
|
+
throw new TileConversionError(
|
|
106
|
+
'MESH_FEATURE_PROPERTY_UNMAPPED',
|
|
107
|
+
'Property table columns must be declared in the metadata class'
|
|
108
|
+
);
|
|
109
|
+
for (const [name, property] of Object.entries(classProperties) as [
|
|
110
|
+
string,
|
|
111
|
+
{
|
|
112
|
+
type: string;
|
|
113
|
+
componentType?: string;
|
|
114
|
+
array?: boolean;
|
|
115
|
+
noData?: unknown;
|
|
116
|
+
default?: unknown;
|
|
117
|
+
normalized?: boolean;
|
|
118
|
+
offset?: unknown;
|
|
119
|
+
scale?: unknown;
|
|
120
|
+
}
|
|
121
|
+
][]) {
|
|
122
|
+
const column = table.properties?.[name];
|
|
123
|
+
if (
|
|
124
|
+
!['SCALAR', 'STRING'].includes(property.type) ||
|
|
125
|
+
property.array ||
|
|
126
|
+
property.noData !== undefined ||
|
|
127
|
+
(['INT64', 'UINT64'].includes(property.componentType || '') &&
|
|
128
|
+
(property.normalized ||
|
|
129
|
+
property.offset !== undefined ||
|
|
130
|
+
property.scale !== undefined ||
|
|
131
|
+
column?.offset !== undefined ||
|
|
132
|
+
column?.scale !== undefined))
|
|
133
|
+
)
|
|
134
|
+
throw new TileConversionError(
|
|
135
|
+
'MESH_FEATURE_PROPERTY_UNSUPPORTED',
|
|
136
|
+
`Property ${name} requires a separately qualified semantic mapping`
|
|
137
|
+
);
|
|
138
|
+
const data = column?.data;
|
|
139
|
+
if (data === undefined && property.default === undefined)
|
|
140
|
+
throw new TileConversionError(
|
|
141
|
+
'MESH_FEATURE_PROPERTY_UNAVAILABLE',
|
|
142
|
+
`Property ${name} has not been decoded`
|
|
143
|
+
);
|
|
144
|
+
properties[name] = data ?? Array.from({length: count}, () => property.default);
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
if (metadata)
|
|
148
|
+
throw new TileConversionError(
|
|
149
|
+
'MESH_FEATURE_MAPPING_UNSUPPORTED',
|
|
150
|
+
'Structural metadata requires an attribute-backed feature declaration'
|
|
151
|
+
);
|
|
152
|
+
count = payload.header?.batchLength ?? Number(payload.featureTableJson?.BATCH_LENGTH);
|
|
153
|
+
properties = (payload.batchTableJson as Record<string, ArrayLike<unknown>>) || {};
|
|
154
|
+
}
|
|
155
|
+
const vertexCount = mesh.attributes.POSITION.value.length / 3;
|
|
156
|
+
if (
|
|
157
|
+
!Number.isSafeInteger(count) ||
|
|
158
|
+
count <= 0 ||
|
|
159
|
+
!attribute ||
|
|
160
|
+
attribute.components !== 1 ||
|
|
161
|
+
attribute.normalized ||
|
|
162
|
+
attribute.value.length !== vertexCount ||
|
|
163
|
+
Array.from(attribute.value as ArrayLike<number>).some(
|
|
164
|
+
value => !Number.isSafeInteger(value) || value < 0 || value >= count
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
throw new TileConversionError(
|
|
168
|
+
'MESH_FEATURE_IDS_INVALID',
|
|
169
|
+
'Feature IDs must be one valid property-table row index per vertex'
|
|
170
|
+
);
|
|
171
|
+
const selectedAttribute = declarations ? `_FEATURE_ID_${declarations[0].attribute}` : '_BATCHID';
|
|
172
|
+
if (
|
|
173
|
+
Object.keys(primitive.attributes).some(
|
|
174
|
+
name => (name === '_BATCHID' || /^_FEATURE_ID_\d+$/.test(name)) && name !== selectedAttribute
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
throw new TileConversionError(
|
|
178
|
+
'MESH_FEATURE_MAPPING_UNSUPPORTED',
|
|
179
|
+
'Additional feature ID sets require separate mappings'
|
|
180
|
+
);
|
|
181
|
+
for (const [name, values] of Object.entries(properties)) {
|
|
182
|
+
if (
|
|
183
|
+
!values ||
|
|
184
|
+
(!Array.isArray(values) && !ArrayBuffer.isView(values)) ||
|
|
185
|
+
values.length !== count
|
|
186
|
+
)
|
|
187
|
+
throw new TileConversionError(
|
|
188
|
+
'MESH_FEATURE_PROPERTY_UNAVAILABLE',
|
|
189
|
+
`Property ${name} must be a decoded column matching the table count`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const sourceIdentifier = options.sourceFeatureIdProperty;
|
|
193
|
+
if (sourceIdentifier && !properties[sourceIdentifier])
|
|
194
|
+
throw new TileConversionError(
|
|
195
|
+
'MESH_FEATURE_ID_PROPERTY_REQUIRED',
|
|
196
|
+
'Stable source identifier property is missing'
|
|
197
|
+
);
|
|
198
|
+
const batches = convertFeatureAttributesToArrowBatches(
|
|
199
|
+
Array.from({length: count}, (_, index) => ({
|
|
200
|
+
featureId: (sourceIdentifier ? properties[sourceIdentifier][index] : index) as
|
|
201
|
+
| string
|
|
202
|
+
| number
|
|
203
|
+
| bigint,
|
|
204
|
+
metadataClass: options.metadataClass,
|
|
205
|
+
properties: Object.fromEntries(
|
|
206
|
+
Object.entries(properties)
|
|
207
|
+
.filter(([name]) => name !== sourceIdentifier)
|
|
208
|
+
.map(([name, values]) => [name, values[index]])
|
|
209
|
+
)
|
|
210
|
+
})),
|
|
211
|
+
options
|
|
212
|
+
);
|
|
213
|
+
const indices = mesh.indices?.value;
|
|
214
|
+
const triangleFeatureIndices = new Uint32Array((indices?.length ?? vertexCount) / 3);
|
|
215
|
+
for (let triangle = 0; triangle < triangleFeatureIndices.length; triangle++) {
|
|
216
|
+
const rows = [0, 1, 2].map(
|
|
217
|
+
corner => attribute.value[indices?.[triangle * 3 + corner] ?? triangle * 3 + corner]
|
|
218
|
+
);
|
|
219
|
+
if (rows.some(row => row !== rows[0]))
|
|
220
|
+
throw new TileConversionError(
|
|
221
|
+
'MESH_FEATURE_TRIANGLE_MIXED',
|
|
222
|
+
'All vertices of a triangle must reference the same feature row'
|
|
223
|
+
);
|
|
224
|
+
triangleFeatureIndices[triangle] = Number(rows[0]);
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
batches,
|
|
228
|
+
triangleFeatureIndices,
|
|
229
|
+
featureIdField: options.featureIdField || 'feature_id',
|
|
230
|
+
integer64Encoding: options.integer64Encoding
|
|
231
|
+
};
|
|
232
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {GLTFMaterialPostprocessed, GLTFImagePostprocessed} from '@loaders.gl/gltf';
|
|
6
|
+
import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
|
|
7
|
+
import type {MeshTileSampler, MeshTileTexture, MeshTileTextureTransform} from './mesh.js';
|
|
8
|
+
|
|
9
|
+
/** Maps a buffer-view or base64 PNG/JPEG image, sampling and UV transform without decoding pixels. */
|
|
10
|
+
export function mapMeshSourceTexture(
|
|
11
|
+
textureInfo: NonNullable<GLTFMaterialPostprocessed['pbrMetallicRoughness']>['baseColorTexture']
|
|
12
|
+
): MeshTileTexture {
|
|
13
|
+
const texture = textureInfo?.texture;
|
|
14
|
+
const image = texture?.source;
|
|
15
|
+
const sampler = texture?.sampler;
|
|
16
|
+
const extensions = textureInfo?.extensions || {};
|
|
17
|
+
const hasTransform = Object.hasOwn(extensions, 'KHR_texture_transform');
|
|
18
|
+
const transform = extensions.KHR_texture_transform;
|
|
19
|
+
if (
|
|
20
|
+
!textureInfo ||
|
|
21
|
+
(textureInfo.texCoord !== undefined && textureInfo.texCoord !== 0) ||
|
|
22
|
+
Object.keys(extensions).some(name => name !== 'KHR_texture_transform') ||
|
|
23
|
+
(hasTransform &&
|
|
24
|
+
(!transform ||
|
|
25
|
+
typeof transform !== 'object' ||
|
|
26
|
+
Array.isArray(transform) ||
|
|
27
|
+
(transform.texCoord !== undefined && transform.texCoord !== 0) ||
|
|
28
|
+
Object.keys(transform.extensions || {}).length ||
|
|
29
|
+
Object.keys(transform).some(
|
|
30
|
+
name =>
|
|
31
|
+
!['offset', 'rotation', 'scale', 'texCoord', 'extensions', 'extras'].includes(name)
|
|
32
|
+
))) ||
|
|
33
|
+
Object.keys(textureInfo).some(
|
|
34
|
+
name => !['id', 'index', 'texture', 'texCoord', 'extensions', 'extras'].includes(name)
|
|
35
|
+
) ||
|
|
36
|
+
!texture ||
|
|
37
|
+
Object.keys(texture).some(
|
|
38
|
+
name => !['id', 'source', 'sampler', 'name', 'extensions', 'extras'].includes(name)
|
|
39
|
+
) ||
|
|
40
|
+
Object.keys(texture.extensions || {}).length ||
|
|
41
|
+
!image ||
|
|
42
|
+
(image.uri !== undefined && image.bufferView !== undefined) ||
|
|
43
|
+
Object.keys(image.extensions || {}).length ||
|
|
44
|
+
(image.uri === undefined &&
|
|
45
|
+
(!(image.bufferView?.data instanceof Uint8Array) ||
|
|
46
|
+
Object.keys(image.bufferView.extensions || {}).length ||
|
|
47
|
+
!['image/png', 'image/jpeg'].includes(image.mimeType || ''))) ||
|
|
48
|
+
(sampler &&
|
|
49
|
+
(Object.keys(sampler.extensions || {}).length ||
|
|
50
|
+
Object.keys(sampler).some(
|
|
51
|
+
name =>
|
|
52
|
+
![
|
|
53
|
+
'id',
|
|
54
|
+
'parameters',
|
|
55
|
+
'wrapS',
|
|
56
|
+
'wrapT',
|
|
57
|
+
'magFilter',
|
|
58
|
+
'minFilter',
|
|
59
|
+
'name',
|
|
60
|
+
'extensions',
|
|
61
|
+
'extras'
|
|
62
|
+
].includes(name)
|
|
63
|
+
)))
|
|
64
|
+
) {
|
|
65
|
+
throw new TileConversionError(
|
|
66
|
+
'MESH_SOURCE_TEXTURE_UNSUPPORTED',
|
|
67
|
+
'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'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const selectedSampler = sampler
|
|
71
|
+
? ({
|
|
72
|
+
wrapS: sampler.wrapS,
|
|
73
|
+
wrapT: sampler.wrapT,
|
|
74
|
+
magFilter: sampler.magFilter,
|
|
75
|
+
minFilter: sampler.minFilter
|
|
76
|
+
} as MeshTileSampler)
|
|
77
|
+
: undefined;
|
|
78
|
+
return {
|
|
79
|
+
...mapMeshSourceImage(image),
|
|
80
|
+
...(hasTransform
|
|
81
|
+
? {
|
|
82
|
+
transform: {
|
|
83
|
+
offset: transform.offset,
|
|
84
|
+
rotation: transform.rotation,
|
|
85
|
+
scale: transform.scale
|
|
86
|
+
} as MeshTileTextureTransform
|
|
87
|
+
}
|
|
88
|
+
: {}),
|
|
89
|
+
...(selectedSampler && Object.values(selectedSampler).some(value => value !== undefined)
|
|
90
|
+
? {sampler: selectedSampler}
|
|
91
|
+
: {})
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Reads self-contained encoded bytes without fetch, pixel decoding, or Node APIs. */
|
|
96
|
+
function mapMeshSourceImage(
|
|
97
|
+
image: GLTFImagePostprocessed
|
|
98
|
+
): Pick<MeshTileTexture, 'data' | 'mimeType'> {
|
|
99
|
+
if (image.uri === undefined) {
|
|
100
|
+
return {data: image.bufferView!.data, mimeType: image.mimeType as MeshTileTexture['mimeType']};
|
|
101
|
+
}
|
|
102
|
+
const match =
|
|
103
|
+
typeof image.uri === 'string'
|
|
104
|
+
? /^data:(image\/(?:png|jpeg));base64,([A-Za-z0-9+/]+={0,2})$/.exec(image.uri)
|
|
105
|
+
: null;
|
|
106
|
+
if (!match || (image.mimeType !== undefined && image.mimeType !== match[1])) {
|
|
107
|
+
throw new TileConversionError(
|
|
108
|
+
'MESH_SOURCE_TEXTURE_UNSUPPORTED',
|
|
109
|
+
'Inline images require a PNG/JPEG base64 data URI and matching optional mimeType'
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
let encodedBytes: string;
|
|
113
|
+
try {
|
|
114
|
+
encodedBytes = atob(match[2]);
|
|
115
|
+
} catch {
|
|
116
|
+
throw new TileConversionError('MESH_SOURCE_TEXTURE_UNSUPPORTED', 'Invalid inline image base64');
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
data: Uint8Array.from(encodedBytes, character => character.charCodeAt(0)),
|
|
120
|
+
mimeType: match[1] as MeshTileTexture['mimeType']
|
|
121
|
+
};
|
|
122
|
+
}
|