@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.
Files changed (141) hide show
  1. package/LICENSE +14 -3
  2. package/README.md +497 -0
  3. package/dist/converter.min.cjs +210 -282
  4. package/dist/i3s-server/bin/i3s-server.min.cjs +106 -341
  5. package/dist/index.cjs +38586 -0
  6. package/dist/index.js +38557 -0
  7. package/dist/v5/adapters.cjs +3548 -0
  8. package/dist/v5/adapters.d.ts +33 -0
  9. package/dist/v5/adapters.js +3537 -0
  10. package/dist/v5/browser-sink.d.ts +37 -0
  11. package/dist/v5/browser-source.d.ts +38 -0
  12. package/dist/v5/browser-tileset-source.d.ts +70 -0
  13. package/dist/v5/browser.cjs +76 -0
  14. package/dist/v5/browser.d.ts +44 -0
  15. package/dist/v5/browser.js +75 -0
  16. package/dist/v5/conversion-api.d.ts +122 -0
  17. package/dist/v5/conversion-archive.d.ts +32 -0
  18. package/dist/v5/core.cjs +917 -0
  19. package/dist/v5/core.d.ts +16 -0
  20. package/dist/v5/core.js +899 -0
  21. package/dist/v5/feature-arrow.d.ts +36 -0
  22. package/dist/v5/i3s-mesh-conversion.d.ts +32 -0
  23. package/dist/v5/i3s-mesh-sink.d.ts +25 -0
  24. package/dist/v5/i3s-mesh-source-features.d.ts +13 -0
  25. package/dist/v5/i3s-mesh-source-material.d.ts +5 -0
  26. package/dist/v5/i3s-mesh-source.d.ts +30 -0
  27. package/dist/v5/index.cjs +73 -0
  28. package/dist/v5/index.d.ts +40 -0
  29. package/dist/v5/index.js +75 -0
  30. package/dist/v5/mesh-conversion.d.ts +71 -0
  31. package/dist/v5/mesh-draco.d.ts +8 -0
  32. package/dist/v5/mesh-features.d.ts +9 -0
  33. package/dist/v5/mesh-source-features.d.ts +21 -0
  34. package/dist/v5/mesh-source-texture.d.ts +6 -0
  35. package/dist/v5/mesh-source.d.ts +41 -0
  36. package/dist/v5/mesh-tileset-archive.d.ts +18 -0
  37. package/dist/v5/mesh-tileset-sink.d.ts +51 -0
  38. package/dist/v5/mesh.d.ts +86 -0
  39. package/dist/v5/point-cloud-conversion.d.ts +31 -0
  40. package/dist/v5/point-cloud-source-encoder.d.ts +46 -0
  41. package/dist/v5/point-cloud-source.d.ts +25 -0
  42. package/dist/v5/point-cloud-spatial.d.ts +22 -0
  43. package/dist/v5/point-cloud-tileset-sink.d.ts +28 -0
  44. package/dist/v5/point-cloud.d.ts +49 -0
  45. package/dist/v5/resource-fetcher.d.ts +18 -0
  46. package/dist/v5/resource-manifest.d.ts +58 -0
  47. package/dist/v5/spatial-conversion.d.ts +61 -0
  48. package/dist/v5/tileset-source.d.ts +21 -0
  49. package/package.json +52 -26
  50. package/src/common/README.md +5 -0
  51. package/src/index.ts +1 -2
  52. package/src/{3d-tiles-converter → v4/3d-tiles-converter}/3d-tiles-converter.ts +13 -2
  53. package/src/v4/3d-tiles-converter/helpers/validate-i3s-spatial-reference.ts +124 -0
  54. package/src/{deps-installer → v4/deps-installer}/deps-installer.ts +10 -7
  55. package/src/{i3s-converter → v4/i3s-converter}/helpers/batch-ids-extensions.ts +1 -1
  56. package/src/v4/i3s-converter/helpers/encode-string-attribute.ts +23 -0
  57. package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-converter.ts +30 -55
  58. package/src/{i3s-converter → v4/i3s-converter}/helpers/gltf-attributes.ts +5 -4
  59. package/src/{i3s-converter → v4/i3s-converter}/helpers/load-3d-tiles.ts +5 -9
  60. package/src/{i3s-converter → v4/i3s-converter}/i3s-converter.ts +6 -7
  61. package/src/{i3s-server → v4/i3s-server}/README.md +2 -2
  62. package/src/{i3s-server → v4/i3s-server}/routes/slpk-router.ts +1 -0
  63. package/src/v4/index.ts +2 -0
  64. package/src/{lib → v4/lib}/utils/compress-util.ts +8 -7
  65. package/src/v5/adapters.ts +75 -0
  66. package/src/v5/browser-sink.ts +130 -0
  67. package/src/v5/browser-source.ts +155 -0
  68. package/src/v5/browser-tileset-source.ts +328 -0
  69. package/src/v5/browser.ts +139 -0
  70. package/src/v5/conversion-api.ts +287 -0
  71. package/src/v5/conversion-archive.ts +85 -0
  72. package/src/v5/core.ts +61 -0
  73. package/src/v5/feature-arrow.ts +429 -0
  74. package/src/v5/i3s-mesh-conversion.ts +192 -0
  75. package/src/v5/i3s-mesh-sink.ts +351 -0
  76. package/src/v5/i3s-mesh-source-features.ts +279 -0
  77. package/src/v5/i3s-mesh-source-material.ts +157 -0
  78. package/src/v5/i3s-mesh-source.ts +227 -0
  79. package/src/v5/index.ts +131 -0
  80. package/src/v5/mesh-conversion.ts +230 -0
  81. package/src/v5/mesh-draco.ts +182 -0
  82. package/src/v5/mesh-features.ts +319 -0
  83. package/src/v5/mesh-source-features.ts +263 -0
  84. package/src/v5/mesh-source-texture.ts +169 -0
  85. package/src/v5/mesh-source.ts +422 -0
  86. package/src/v5/mesh-tileset-archive.ts +50 -0
  87. package/src/v5/mesh-tileset-sink.ts +324 -0
  88. package/src/v5/mesh.ts +526 -0
  89. package/src/v5/point-cloud-conversion.ts +88 -0
  90. package/src/v5/point-cloud-source-encoder.ts +116 -0
  91. package/src/v5/point-cloud-source.ts +84 -0
  92. package/src/v5/point-cloud-spatial.ts +280 -0
  93. package/src/v5/point-cloud-tileset-sink.ts +234 -0
  94. package/src/v5/point-cloud.ts +398 -0
  95. package/src/v5/resource-fetcher.ts +110 -0
  96. package/src/v5/resource-manifest.ts +213 -0
  97. package/src/v5/spatial-conversion.ts +389 -0
  98. package/src/v5/tileset-source.ts +54 -0
  99. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/3d-tiles-content-converter.ts +0 -0
  100. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/i3s-obb-to-3d-tiles-obb.ts +0 -0
  101. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/load-i3s.ts +0 -0
  102. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/texture-atlas.ts +0 -0
  103. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/json-templates/tileset.ts +0 -0
  104. /package/src/{constants.ts → v4/constants.ts} +0 -0
  105. /package/src/{converter-cli.ts → v4/converter-cli.ts} +0 -0
  106. /package/src/{i3s-converter → v4/i3s-converter}/helpers/attribute-metadata-info.ts +0 -0
  107. /package/src/{i3s-converter → v4/i3s-converter}/helpers/coordinate-converter.ts +0 -0
  108. /package/src/{i3s-converter → v4/i3s-converter}/helpers/create-scene-server-path.ts +0 -0
  109. /package/src/{i3s-converter → v4/i3s-converter}/helpers/feature-attributes.ts +0 -0
  110. /package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-attributes.ts +0 -0
  111. /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-debug.ts +0 -0
  112. /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-index-document.ts +0 -0
  113. /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-pages.ts +0 -0
  114. /package/src/{i3s-converter → v4/i3s-converter}/helpers/preprocess-3d-tiles.ts +0 -0
  115. /package/src/{i3s-converter → v4/i3s-converter}/helpers/progress.ts +0 -0
  116. /package/src/{i3s-converter → v4/i3s-converter}/helpers/tileset-traversal.ts +0 -0
  117. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/geometry-definitions.ts +0 -0
  118. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/layers.ts +0 -0
  119. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/metadata.ts +0 -0
  120. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/node.ts +0 -0
  121. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/scene-server.ts +0 -0
  122. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/shared-resources.ts +0 -0
  123. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/store.ts +0 -0
  124. /package/src/{i3s-converter → v4/i3s-converter}/types.ts +0 -0
  125. /package/src/{i3s-server → v4/i3s-server}/app.ts +0 -0
  126. /package/src/{i3s-server → v4/i3s-server}/bin/www.ts +0 -0
  127. /package/src/{i3s-server → v4/i3s-server}/certs/cert.pem +0 -0
  128. /package/src/{i3s-server → v4/i3s-server}/certs/key.pem +0 -0
  129. /package/src/{i3s-server → v4/i3s-server}/controllers/slpk-controller.ts +0 -0
  130. /package/src/{i3s-server → v4/i3s-server}/utils/create-scene-server.ts +0 -0
  131. /package/src/{i3s-server → v4/i3s-server}/utils/server-utils.ts +0 -0
  132. /package/src/{lib → v4/lib}/json-schemas/conversion-dump-json-schema.ts +0 -0
  133. /package/src/{lib → v4/lib}/utils/cli-utils.ts +0 -0
  134. /package/src/{lib → v4/lib}/utils/conversion-dump.ts +0 -0
  135. /package/src/{lib → v4/lib}/utils/file-utils.ts +0 -0
  136. /package/src/{lib → v4/lib}/utils/geometry-utils.ts +0 -0
  137. /package/src/{lib → v4/lib}/utils/lod-conversion-utils.ts +0 -0
  138. /package/src/{lib → v4/lib}/utils/queue.ts +0 -0
  139. /package/src/{lib → v4/lib}/utils/statistic-utils.ts +0 -0
  140. /package/src/{lib → v4/lib}/utils/write-queue.ts +0 -0
  141. /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
+ }