@loaders.gl/tile-converter 5.0.0-alpha.1 → 5.0.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +14 -3
- package/README.md +497 -0
- package/dist/converter.min.cjs +210 -282
- package/dist/i3s-server/bin/i3s-server.min.cjs +106 -341
- package/dist/index.cjs +38586 -0
- package/dist/index.js +38557 -0
- package/dist/v5/adapters.cjs +3548 -0
- package/dist/v5/adapters.d.ts +33 -0
- package/dist/v5/adapters.js +3537 -0
- package/dist/v5/browser-sink.d.ts +37 -0
- package/dist/v5/browser-source.d.ts +38 -0
- package/dist/v5/browser-tileset-source.d.ts +70 -0
- package/dist/v5/browser.cjs +76 -0
- package/dist/v5/browser.d.ts +44 -0
- package/dist/v5/browser.js +75 -0
- package/dist/v5/conversion-api.d.ts +122 -0
- package/dist/v5/conversion-archive.d.ts +32 -0
- package/dist/v5/core.cjs +917 -0
- package/dist/v5/core.d.ts +16 -0
- package/dist/v5/core.js +899 -0
- package/dist/v5/feature-arrow.d.ts +36 -0
- package/dist/v5/i3s-mesh-conversion.d.ts +32 -0
- package/dist/v5/i3s-mesh-sink.d.ts +25 -0
- package/dist/v5/i3s-mesh-source-features.d.ts +13 -0
- package/dist/v5/i3s-mesh-source-material.d.ts +5 -0
- package/dist/v5/i3s-mesh-source.d.ts +30 -0
- package/dist/v5/index.cjs +73 -0
- package/dist/v5/index.d.ts +40 -0
- package/dist/v5/index.js +75 -0
- package/dist/v5/mesh-conversion.d.ts +71 -0
- package/dist/v5/mesh-draco.d.ts +8 -0
- package/dist/v5/mesh-features.d.ts +9 -0
- package/dist/v5/mesh-source-features.d.ts +21 -0
- package/dist/v5/mesh-source-texture.d.ts +6 -0
- package/dist/v5/mesh-source.d.ts +41 -0
- package/dist/v5/mesh-tileset-archive.d.ts +18 -0
- package/dist/v5/mesh-tileset-sink.d.ts +51 -0
- package/dist/v5/mesh.d.ts +86 -0
- package/dist/v5/point-cloud-conversion.d.ts +31 -0
- package/dist/v5/point-cloud-source-encoder.d.ts +46 -0
- package/dist/v5/point-cloud-source.d.ts +25 -0
- package/dist/v5/point-cloud-spatial.d.ts +22 -0
- package/dist/v5/point-cloud-tileset-sink.d.ts +28 -0
- package/dist/v5/point-cloud.d.ts +49 -0
- package/dist/v5/resource-fetcher.d.ts +18 -0
- package/dist/v5/resource-manifest.d.ts +58 -0
- package/dist/v5/spatial-conversion.d.ts +61 -0
- package/dist/v5/tileset-source.d.ts +21 -0
- package/package.json +52 -26
- package/src/common/README.md +5 -0
- package/src/index.ts +1 -2
- package/src/{3d-tiles-converter → v4/3d-tiles-converter}/3d-tiles-converter.ts +13 -2
- package/src/v4/3d-tiles-converter/helpers/validate-i3s-spatial-reference.ts +124 -0
- package/src/{deps-installer → v4/deps-installer}/deps-installer.ts +10 -7
- package/src/{i3s-converter → v4/i3s-converter}/helpers/batch-ids-extensions.ts +1 -1
- package/src/v4/i3s-converter/helpers/encode-string-attribute.ts +23 -0
- package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-converter.ts +30 -55
- package/src/{i3s-converter → v4/i3s-converter}/helpers/gltf-attributes.ts +5 -4
- package/src/{i3s-converter → v4/i3s-converter}/helpers/load-3d-tiles.ts +5 -9
- package/src/{i3s-converter → v4/i3s-converter}/i3s-converter.ts +6 -7
- package/src/{i3s-server → v4/i3s-server}/README.md +2 -2
- package/src/{i3s-server → v4/i3s-server}/routes/slpk-router.ts +1 -0
- package/src/v4/index.ts +2 -0
- package/src/{lib → v4/lib}/utils/compress-util.ts +8 -7
- package/src/v5/adapters.ts +75 -0
- package/src/v5/browser-sink.ts +130 -0
- package/src/v5/browser-source.ts +155 -0
- package/src/v5/browser-tileset-source.ts +328 -0
- package/src/v5/browser.ts +139 -0
- package/src/v5/conversion-api.ts +287 -0
- package/src/v5/conversion-archive.ts +85 -0
- package/src/v5/core.ts +61 -0
- package/src/v5/feature-arrow.ts +429 -0
- package/src/v5/i3s-mesh-conversion.ts +192 -0
- package/src/v5/i3s-mesh-sink.ts +351 -0
- package/src/v5/i3s-mesh-source-features.ts +279 -0
- package/src/v5/i3s-mesh-source-material.ts +157 -0
- package/src/v5/i3s-mesh-source.ts +227 -0
- package/src/v5/index.ts +131 -0
- package/src/v5/mesh-conversion.ts +230 -0
- package/src/v5/mesh-draco.ts +182 -0
- package/src/v5/mesh-features.ts +319 -0
- package/src/v5/mesh-source-features.ts +263 -0
- package/src/v5/mesh-source-texture.ts +169 -0
- package/src/v5/mesh-source.ts +422 -0
- package/src/v5/mesh-tileset-archive.ts +50 -0
- package/src/v5/mesh-tileset-sink.ts +324 -0
- package/src/v5/mesh.ts +526 -0
- package/src/v5/point-cloud-conversion.ts +88 -0
- package/src/v5/point-cloud-source-encoder.ts +116 -0
- package/src/v5/point-cloud-source.ts +84 -0
- package/src/v5/point-cloud-spatial.ts +280 -0
- package/src/v5/point-cloud-tileset-sink.ts +234 -0
- package/src/v5/point-cloud.ts +398 -0
- package/src/v5/resource-fetcher.ts +110 -0
- package/src/v5/resource-manifest.ts +213 -0
- package/src/v5/spatial-conversion.ts +389 -0
- package/src/v5/tileset-source.ts +54 -0
- /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/3d-tiles-content-converter.ts +0 -0
- /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/i3s-obb-to-3d-tiles-obb.ts +0 -0
- /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/load-i3s.ts +0 -0
- /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/texture-atlas.ts +0 -0
- /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/json-templates/tileset.ts +0 -0
- /package/src/{constants.ts → v4/constants.ts} +0 -0
- /package/src/{converter-cli.ts → v4/converter-cli.ts} +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/attribute-metadata-info.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/coordinate-converter.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/create-scene-server-path.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/feature-attributes.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-attributes.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-debug.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-index-document.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-pages.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/preprocess-3d-tiles.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/progress.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/tileset-traversal.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/geometry-definitions.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/layers.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/metadata.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/node.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/scene-server.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/shared-resources.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/store.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/types.ts +0 -0
- /package/src/{i3s-server → v4/i3s-server}/app.ts +0 -0
- /package/src/{i3s-server → v4/i3s-server}/bin/www.ts +0 -0
- /package/src/{i3s-server → v4/i3s-server}/certs/cert.pem +0 -0
- /package/src/{i3s-server → v4/i3s-server}/certs/key.pem +0 -0
- /package/src/{i3s-server → v4/i3s-server}/controllers/slpk-controller.ts +0 -0
- /package/src/{i3s-server → v4/i3s-server}/utils/create-scene-server.ts +0 -0
- /package/src/{i3s-server → v4/i3s-server}/utils/server-utils.ts +0 -0
- /package/src/{lib → v4/lib}/json-schemas/conversion-dump-json-schema.ts +0 -0
- /package/src/{lib → v4/lib}/utils/cli-utils.ts +0 -0
- /package/src/{lib → v4/lib}/utils/conversion-dump.ts +0 -0
- /package/src/{lib → v4/lib}/utils/file-utils.ts +0 -0
- /package/src/{lib → v4/lib}/utils/geometry-utils.ts +0 -0
- /package/src/{lib → v4/lib}/utils/lod-conversion-utils.ts +0 -0
- /package/src/{lib → v4/lib}/utils/queue.ts +0 -0
- /package/src/{lib → v4/lib}/utils/statistic-utils.ts +0 -0
- /package/src/{lib → v4/lib}/utils/write-queue.ts +0 -0
- /package/src/{pgm-loader.ts → v4/pgm-loader.ts} +0 -0
|
@@ -0,0 +1,230 @@
|
|
|
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 {
|
|
12
|
+
Tiles3DConversionSpatialContext,
|
|
13
|
+
I3SConversionSpatialContext
|
|
14
|
+
} from '@loaders.gl/tile-converter/v5/core';
|
|
15
|
+
import {encodeMeshTile, validateMeshGeometry} from './mesh.js';
|
|
16
|
+
import type {MeshTileFeatures} from './mesh-features.js';
|
|
17
|
+
import type {MeshTileMaterial} from './mesh.js';
|
|
18
|
+
import {encodeDracoMeshTile} from './mesh-draco.js';
|
|
19
|
+
|
|
20
|
+
/** Explicitly selected geometry, with positions already in the spatial context's source frame. */
|
|
21
|
+
export interface MeshConversionInput {
|
|
22
|
+
/** Caller-owned resource identifier, forwarded to the output sink. */
|
|
23
|
+
readonly id: string;
|
|
24
|
+
/** Triangle geometry with absolute packed Float32 or Float64 source positions. */
|
|
25
|
+
readonly mesh: MeshGeometry;
|
|
26
|
+
/** Original I3S vector basis; overrides the codec default when supplied by a source. */
|
|
27
|
+
readonly normalReferenceFrame?: 'earth-centered' | 'vertex-reference-frame';
|
|
28
|
+
/** Optional single material with an embedded base-color image, mapped explicitly by the source adapter. */
|
|
29
|
+
readonly material?: MeshTileMaterial;
|
|
30
|
+
/** Explicit feature table and triangle associations, written as structural metadata. */
|
|
31
|
+
readonly features?: MeshTileFeatures;
|
|
32
|
+
/** Finite xyz origin in the target frame, subtracted before float32 encoding. */
|
|
33
|
+
readonly origin: readonly [number, number, number];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A completed GLB resource and the metadata needed to place its local geometry. */
|
|
37
|
+
export interface EncodedMeshConversionResource {
|
|
38
|
+
/** Caller-owned resource identifier. */
|
|
39
|
+
readonly id: string;
|
|
40
|
+
/** Self-contained GLB with local positions and transformed normals. */
|
|
41
|
+
readonly glb: ArrayBuffer;
|
|
42
|
+
/** Target-frame translation to add to the encoded positions. */
|
|
43
|
+
readonly origin: readonly [number, number, number];
|
|
44
|
+
/** Target-frame bounds of the reconstructed encoded vertices, including float32 rounding. */
|
|
45
|
+
readonly boundingBox: readonly [readonly number[], readonly number[]];
|
|
46
|
+
/** Bounds of encoded local vertices before adding the origin, without absolute-coordinate cancellation. */
|
|
47
|
+
readonly localBoundingBox: readonly [readonly number[], readonly number[]];
|
|
48
|
+
/** Metadata describing the output coordinate operation and height reference. */
|
|
49
|
+
readonly spatialReference: TilesetSpatialReference;
|
|
50
|
+
/** Largest measured Euclidean reconstruction error in target coordinate units. */
|
|
51
|
+
readonly maximumPositionError: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Spatial policy used by the concrete mesh conversion codec. */
|
|
55
|
+
export interface MeshConversionCodecOptions {
|
|
56
|
+
/** Shared context that accepts absolute source positions and returns absolute target positions. */
|
|
57
|
+
readonly spatialContext: Tiles3DConversionSpatialContext | I3SConversionSpatialContext;
|
|
58
|
+
/** I3S vector frame when using an I3S context; defaults to earth-centered. */
|
|
59
|
+
readonly normalReferenceFrame?: 'earth-centered' | 'vertex-reference-frame';
|
|
60
|
+
/** Rebase around the transformed bounds center instead of the resource's explicit origin. */
|
|
61
|
+
readonly autoOrigin?: boolean;
|
|
62
|
+
/** Maximum Euclidean reconstruction error per vertex, in target coordinate units. */
|
|
63
|
+
readonly maxPositionError: number;
|
|
64
|
+
/** Lossless Draco Edge Breaker is enabled by default; false emits uncompressed GLB geometry. */
|
|
65
|
+
readonly draco?: boolean;
|
|
66
|
+
/** Optional local/CDN library locations or injected Draco runtimes for compression and verification. */
|
|
67
|
+
readonly dracoLibraryOptions?: LoadLibraryOptions;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Creates a concrete mesh codec for `convertTileset`.
|
|
72
|
+
*
|
|
73
|
+
* Source adapters must select geometry and apply source placement before supplying absolute
|
|
74
|
+
* positions and matching normals. This codec applies the supplied spatial context once, rebases
|
|
75
|
+
* around each input's target origin, and rejects rounding above the explicit error budget.
|
|
76
|
+
* Lossless Draco Edge Breaker is enabled by default; decoded positions determine output bounds.
|
|
77
|
+
* Set draco to false for uncompressed geometry. Quantization is not enabled by this profile.
|
|
78
|
+
* Geographic and unknown output frames are rejected. The returned origin/bounds/reference are
|
|
79
|
+
* external placement metadata; applications own axis conventions, hierarchy, and packaging.
|
|
80
|
+
* Input arrays are not mutated. Core input/output byte gates, cancellation, awaited writes, and
|
|
81
|
+
* destination finalize/abort handling apply when used through `convertTileset`.
|
|
82
|
+
*
|
|
83
|
+
* @param options - Resolved spatial context and finite nonnegative target-unit precision budget.
|
|
84
|
+
* @returns One GLB and matching metadata per input geometry.
|
|
85
|
+
*/
|
|
86
|
+
export function createMeshConversionCodec<TInspection = unknown>(
|
|
87
|
+
options: MeshConversionCodecOptions
|
|
88
|
+
): TileConversionCodec<TInspection, MeshConversionInput, EncodedMeshConversionResource> {
|
|
89
|
+
const {spatialContext, maxPositionError, draco = true, dracoLibraryOptions} = options;
|
|
90
|
+
if (typeof draco !== 'boolean') {
|
|
91
|
+
throw new TileConversionError('MESH_DRACO_OPTIONS_INVALID', 'draco must be a boolean');
|
|
92
|
+
}
|
|
93
|
+
if (!Number.isFinite(maxPositionError) || maxPositionError < 0) {
|
|
94
|
+
throw new TileConversionError(
|
|
95
|
+
'MESH_PRECISION_LIMIT_INVALID',
|
|
96
|
+
'maxPositionError must be finite and nonnegative'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const spatialReference = spatialContext.spatialReference;
|
|
100
|
+
const outputCrs = spatialReference.targetCrs || spatialReference.sourceCrs;
|
|
101
|
+
const outputFrame =
|
|
102
|
+
spatialReference.status === 'native'
|
|
103
|
+
? spatialReference.coordinateFrame
|
|
104
|
+
: outputCrs
|
|
105
|
+
? getSpatialCoordinateFrame(outputCrs)
|
|
106
|
+
: 'unknown';
|
|
107
|
+
if (
|
|
108
|
+
!['native', 'transformed'].includes(spatialReference.status) ||
|
|
109
|
+
!['projected', 'geocentric', 'local', 'cartesian'].includes(outputFrame)
|
|
110
|
+
) {
|
|
111
|
+
throw new TileConversionError(
|
|
112
|
+
'MESH_OUTPUT_FRAME_UNSUPPORTED',
|
|
113
|
+
'Mesh output requires a resolved Cartesian coordinate frame'
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
/** Prepares and encodes one geometry while honoring cancellation. */
|
|
118
|
+
async *convert(resource, _inspection, signal) {
|
|
119
|
+
signal?.throwIfAborted();
|
|
120
|
+
const geometry = validateMeshGeometry(resource.mesh, true);
|
|
121
|
+
const sourcePositions = geometry.attributes.POSITION.value;
|
|
122
|
+
const origin: [number, number, number] = [...resource.origin];
|
|
123
|
+
if (origin.length !== 3 || origin.some(value => !Number.isFinite(value))) {
|
|
124
|
+
throw new TileConversionError(
|
|
125
|
+
'MESH_ORIGIN_INVALID',
|
|
126
|
+
'Target origin must contain three finite coordinates'
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const transformed =
|
|
130
|
+
'transformGeometryAsync' in spatialContext
|
|
131
|
+
? await spatialContext.transformGeometryAsync(
|
|
132
|
+
sourcePositions,
|
|
133
|
+
geometry.attributes.NORMAL?.value,
|
|
134
|
+
resource.normalReferenceFrame ?? options.normalReferenceFrame
|
|
135
|
+
)
|
|
136
|
+
: {
|
|
137
|
+
positions: spatialContext.transformPositions(sourcePositions),
|
|
138
|
+
normals: geometry.attributes.NORMAL
|
|
139
|
+
? spatialContext.transformNormals(geometry.attributes.NORMAL.value, sourcePositions)
|
|
140
|
+
: undefined
|
|
141
|
+
};
|
|
142
|
+
signal?.throwIfAborted();
|
|
143
|
+
const targetPositions = transformed.positions;
|
|
144
|
+
if (
|
|
145
|
+
targetPositions.length !== sourcePositions.length ||
|
|
146
|
+
targetPositions.some(value => !Number.isFinite(value))
|
|
147
|
+
) {
|
|
148
|
+
throw new TileConversionError(
|
|
149
|
+
'MESH_TRANSFORM_INVALID',
|
|
150
|
+
'Spatial transformation must return one finite xyz position per vertex'
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (options.autoOrigin) {
|
|
154
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
155
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
156
|
+
for (let index = 0; index < targetPositions.length; index++) {
|
|
157
|
+
const axis = index % 3;
|
|
158
|
+
minimum[axis] = Math.min(minimum[axis], targetPositions[index]);
|
|
159
|
+
maximum[axis] = Math.max(maximum[axis], targetPositions[index]);
|
|
160
|
+
}
|
|
161
|
+
for (let axis = 0; axis < 3; axis++) origin[axis] = minimum[axis] / 2 + maximum[axis] / 2;
|
|
162
|
+
}
|
|
163
|
+
const localPositions = new Float32Array(targetPositions.length);
|
|
164
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
165
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
166
|
+
const localMinimum = [Infinity, Infinity, Infinity];
|
|
167
|
+
const localMaximum = [-Infinity, -Infinity, -Infinity];
|
|
168
|
+
let maximumPositionError = 0;
|
|
169
|
+
for (let index = 0; index < targetPositions.length; index += 3) {
|
|
170
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
171
|
+
localPositions[index + axis] = targetPositions[index + axis] - origin[axis];
|
|
172
|
+
}
|
|
173
|
+
const positionError = Math.hypot(
|
|
174
|
+
localPositions[index] + origin[0] - targetPositions[index],
|
|
175
|
+
localPositions[index + 1] + origin[1] - targetPositions[index + 1],
|
|
176
|
+
localPositions[index + 2] + origin[2] - targetPositions[index + 2]
|
|
177
|
+
);
|
|
178
|
+
if (!Number.isFinite(positionError) || positionError > maxPositionError) {
|
|
179
|
+
throw new TileConversionError(
|
|
180
|
+
'MESH_POSITION_PRECISION_EXCEEDED',
|
|
181
|
+
'Float32 mesh positions exceed the configured reconstruction error budget'
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
maximumPositionError = Math.max(maximumPositionError, positionError);
|
|
185
|
+
}
|
|
186
|
+
geometry.attributes.POSITION = {value: localPositions, size: 3};
|
|
187
|
+
if (geometry.attributes.NORMAL) {
|
|
188
|
+
geometry.attributes.NORMAL = {
|
|
189
|
+
value: transformed.normals!,
|
|
190
|
+
size: 3
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const appearance = {material: resource.material, features: resource.features};
|
|
194
|
+
const encoded = draco
|
|
195
|
+
? await encodeDracoMeshTile(geometry, appearance, dracoLibraryOptions)
|
|
196
|
+
: {glb: encodeMeshTile(geometry, appearance), positions: localPositions};
|
|
197
|
+
signal?.throwIfAborted();
|
|
198
|
+
for (let index = 0; index < encoded.positions.length; index++) {
|
|
199
|
+
const axis = index % 3;
|
|
200
|
+
const position = encoded.positions[index];
|
|
201
|
+
localMinimum[axis] = Math.min(localMinimum[axis], position);
|
|
202
|
+
localMaximum[axis] = Math.max(localMaximum[axis], position);
|
|
203
|
+
minimum[axis] = Math.min(minimum[axis], position + origin[axis]);
|
|
204
|
+
maximum[axis] = Math.max(maximum[axis], position + origin[axis]);
|
|
205
|
+
}
|
|
206
|
+
yield {
|
|
207
|
+
id: resource.id,
|
|
208
|
+
glb: encoded.glb,
|
|
209
|
+
origin,
|
|
210
|
+
boundingBox: [minimum, maximum],
|
|
211
|
+
localBoundingBox: [localMinimum, localMaximum],
|
|
212
|
+
spatialReference,
|
|
213
|
+
maximumPositionError
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
/** Reports position rounding authorized by the configured precision budget. */
|
|
217
|
+
async validateOutput(resource) {
|
|
218
|
+
return resource.maximumPositionError > 0
|
|
219
|
+
? [
|
|
220
|
+
{
|
|
221
|
+
code: 'MESH_POSITION_ROUNDING',
|
|
222
|
+
message: `Encoded positions have up to ${resource.maximumPositionError} target-unit reconstruction error`,
|
|
223
|
+
severity: 'info' as const,
|
|
224
|
+
resourceId: resource.id
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
: [];
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
import {prepareMeshFeatureGeometry, encodeMeshFeatures} from './mesh-features.js';
|
|
18
|
+
|
|
19
|
+
const DRACO_EXTENSION = 'KHR_draco_mesh_compression';
|
|
20
|
+
|
|
21
|
+
/** Encodes the validated single-mesh profile with lossless Edge Breaker and no raw fallback. */
|
|
22
|
+
export async function encodeDracoMeshTile(
|
|
23
|
+
geometry: MeshGeometry,
|
|
24
|
+
options: MeshTileOptions,
|
|
25
|
+
libraryOptions: LoadLibraryOptions = {}
|
|
26
|
+
): Promise<{glb: ArrayBuffer; positions: Float32Array}> {
|
|
27
|
+
geometry = prepareMeshFeatureGeometry(validateMeshGeometry(geometry), options.features);
|
|
28
|
+
// Draco requires explicit faces even when the input triangle list is non-indexed.
|
|
29
|
+
if (!geometry.indices) {
|
|
30
|
+
geometry.indices = {
|
|
31
|
+
size: 1,
|
|
32
|
+
value: Uint32Array.from(
|
|
33
|
+
{length: geometry.attributes.POSITION.value.length / 3},
|
|
34
|
+
(_, index) => index
|
|
35
|
+
)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const {modules, useLocalLibraries, CDN: contentDeliveryNetwork} = libraryOptions;
|
|
39
|
+
const runtimeOptions = {modules, core: {useLocalLibraries, CDN: contentDeliveryNetwork}};
|
|
40
|
+
const {scenegraph, materialIndex} = createMeshTileScenegraph(options, geometry);
|
|
41
|
+
const encoded = await encodeDraco(geometry, {
|
|
42
|
+
...runtimeOptions,
|
|
43
|
+
draco: {method: 'MESH_EDGEBREAKER_ENCODING'}
|
|
44
|
+
});
|
|
45
|
+
const loaderOptions = {...runtimeOptions, draco: {shape: 'mesh' as const}};
|
|
46
|
+
const loader = await DracoLoader.preload('', loaderOptions);
|
|
47
|
+
const decoded = (await loader.parse!(encoded.data, loaderOptions)) as DracoMesh;
|
|
48
|
+
const positions = decoded.attributes.POSITION.value;
|
|
49
|
+
if (
|
|
50
|
+
!(positions instanceof Float32Array) ||
|
|
51
|
+
positions.length !== encoded.report.pointCount * 3 ||
|
|
52
|
+
encoded.report.pointCount === 0 ||
|
|
53
|
+
!decoded.indices ||
|
|
54
|
+
decoded.indices.value.length !== encoded.report.faceCount * 3 ||
|
|
55
|
+
encoded.report.faceCount * 3 !==
|
|
56
|
+
(geometry.indices?.value.length ?? geometry.attributes.POSITION.value.length / 3)
|
|
57
|
+
) {
|
|
58
|
+
throw new TileConversionError(
|
|
59
|
+
'MESH_DRACO_ENCODING_INVALID',
|
|
60
|
+
'Draco must preserve the triangle count and emit finite positions'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
// Lossy quantization is deliberately unavailable in this initial profile.
|
|
64
|
+
const inputPositions = geometry.attributes.POSITION.value;
|
|
65
|
+
const positionKeys = new Set<string>();
|
|
66
|
+
for (let index = 0; index < inputPositions.length; index += 3) {
|
|
67
|
+
positionKeys.add(getPositionKey(inputPositions, index));
|
|
68
|
+
}
|
|
69
|
+
for (let index = 0; index < positions.length; index += 3) {
|
|
70
|
+
if (!positionKeys.has(getPositionKey(positions, index))) {
|
|
71
|
+
throw new TileConversionError(
|
|
72
|
+
'MESH_DRACO_POSITION_CHANGED',
|
|
73
|
+
'Lossless Draco must preserve the encoded Float32 positions'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (JSON.stringify(getMeshTriangles(geometry)) !== JSON.stringify(getMeshTriangles(decoded))) {
|
|
78
|
+
throw new TileConversionError(
|
|
79
|
+
'MESH_DRACO_ATTRIBUTE_CHANGED',
|
|
80
|
+
'Lossless Draco must preserve oriented triangles and every attribute, including feature ownership'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
const bufferView = scenegraph.addBufferView(encoded.data);
|
|
84
|
+
const attributes: Record<string, number> = {};
|
|
85
|
+
const attributeIdentifiers: Record<string, number> = {};
|
|
86
|
+
scenegraph.json.accessors = [];
|
|
87
|
+
for (const [name, attribute] of Object.entries(geometry.attributes)) {
|
|
88
|
+
const decodedAttribute = decoded.attributes[name];
|
|
89
|
+
if (
|
|
90
|
+
!decodedAttribute ||
|
|
91
|
+
decodedAttribute.value.length !== encoded.report.pointCount * attribute.size
|
|
92
|
+
) {
|
|
93
|
+
throw new TileConversionError('MESH_DRACO_ENCODING_INVALID', `Draco must preserve ${name}`);
|
|
94
|
+
}
|
|
95
|
+
const accessor: GLTFAccessor = {
|
|
96
|
+
componentType:
|
|
97
|
+
attribute.value instanceof Uint8Array
|
|
98
|
+
? 5121
|
|
99
|
+
: attribute.value instanceof Uint16Array
|
|
100
|
+
? 5123
|
|
101
|
+
: attribute.value instanceof Uint32Array
|
|
102
|
+
? 5125
|
|
103
|
+
: 5126,
|
|
104
|
+
count: encoded.report.pointCount,
|
|
105
|
+
type:
|
|
106
|
+
attribute.size === 1
|
|
107
|
+
? 'SCALAR'
|
|
108
|
+
: attribute.size === 2
|
|
109
|
+
? 'VEC2'
|
|
110
|
+
: attribute.size === 3
|
|
111
|
+
? 'VEC3'
|
|
112
|
+
: 'VEC4',
|
|
113
|
+
...(attribute.normalized ? {normalized: true} : {})
|
|
114
|
+
};
|
|
115
|
+
if (name === 'POSITION') {
|
|
116
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
117
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
118
|
+
for (let index = 0; index < positions.length; index++) {
|
|
119
|
+
const axis = index % 3;
|
|
120
|
+
minimum[axis] = Math.min(minimum[axis], positions[index]);
|
|
121
|
+
maximum[axis] = Math.max(maximum[axis], positions[index]);
|
|
122
|
+
}
|
|
123
|
+
accessor.min = minimum;
|
|
124
|
+
accessor.max = maximum;
|
|
125
|
+
}
|
|
126
|
+
attributes[name] = scenegraph.json.accessors.push(accessor) - 1;
|
|
127
|
+
attributeIdentifiers[name] = encoded.report.attributes[name].id;
|
|
128
|
+
}
|
|
129
|
+
const indices =
|
|
130
|
+
scenegraph.json.accessors.push({
|
|
131
|
+
componentType: decoded.indices.value instanceof Uint16Array ? 5123 : 5125,
|
|
132
|
+
count: encoded.report.faceCount * 3,
|
|
133
|
+
type: 'SCALAR'
|
|
134
|
+
}) - 1;
|
|
135
|
+
scenegraph.json.meshes = [
|
|
136
|
+
{
|
|
137
|
+
primitives: [
|
|
138
|
+
{
|
|
139
|
+
attributes,
|
|
140
|
+
indices,
|
|
141
|
+
mode: 4,
|
|
142
|
+
...(materialIndex === undefined ? {} : {material: materialIndex}),
|
|
143
|
+
extensions: {[DRACO_EXTENSION]: {bufferView, attributes: attributeIdentifiers}}
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
];
|
|
148
|
+
encodeMeshFeatures(scenegraph, 0, options.features);
|
|
149
|
+
scenegraph.registerRequiredExtension(DRACO_EXTENSION);
|
|
150
|
+
return {glb: finalizeMeshTileScenegraph(scenegraph, 0), positions};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Identifies one exact finite Float32 position independently of Draco vertex ordering. */
|
|
154
|
+
function getPositionKey(positions: ArrayLike<number>, index: number): string {
|
|
155
|
+
return `${positions[index]},${positions[index + 1]},${positions[index + 2]}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Compares oriented triangles independently of vertex/face order, retaining every attribute. */
|
|
159
|
+
function getMeshTriangles(mesh: Pick<MeshGeometry, 'attributes' | 'indices'>): string[] {
|
|
160
|
+
const names = Object.keys(mesh.attributes).sort();
|
|
161
|
+
const indices = mesh.indices!.value;
|
|
162
|
+
const triangles: string[] = [];
|
|
163
|
+
for (let index = 0; index < indices.length; index += 3) {
|
|
164
|
+
const vertices = [0, 1, 2].map(corner =>
|
|
165
|
+
names
|
|
166
|
+
.map(name => {
|
|
167
|
+
const attribute = mesh.attributes[name];
|
|
168
|
+
return Array.from(
|
|
169
|
+
{length: attribute.size},
|
|
170
|
+
(_, component) => attribute.value[indices[index + corner] * attribute.size + component]
|
|
171
|
+
).join(',');
|
|
172
|
+
})
|
|
173
|
+
.join('|')
|
|
174
|
+
);
|
|
175
|
+
triangles.push(
|
|
176
|
+
[0, 1, 2]
|
|
177
|
+
.map(corner => [...vertices.slice(corner), ...vertices.slice(0, corner)].join(';'))
|
|
178
|
+
.sort()[0]
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return triangles.sort();
|
|
182
|
+
}
|