@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.
Files changed (125) hide show
  1. package/LICENSE +14 -3
  2. package/README.md +464 -0
  3. package/dist/converter.min.cjs +199 -260
  4. package/dist/i3s-server/bin/i3s-server.min.cjs +106 -341
  5. package/dist/index.cjs +38482 -0
  6. package/dist/index.js +38453 -0
  7. package/dist/v5/adapters.cjs +1992 -0
  8. package/dist/v5/adapters.d.ts +22 -0
  9. package/dist/v5/adapters.js +1972 -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 +64 -0
  14. package/dist/v5/browser.d.ts +34 -0
  15. package/dist/v5/browser.js +57 -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 +863 -0
  19. package/dist/v5/core.d.ts +16 -0
  20. package/dist/v5/core.js +845 -0
  21. package/dist/v5/feature-arrow.d.ts +36 -0
  22. package/dist/v5/i3s-mesh-conversion.d.ts +26 -0
  23. package/dist/v5/index.cjs +63 -0
  24. package/dist/v5/index.d.ts +32 -0
  25. package/dist/v5/index.js +59 -0
  26. package/dist/v5/mesh-conversion.d.ts +62 -0
  27. package/dist/v5/mesh-draco.d.ts +8 -0
  28. package/dist/v5/mesh-source-features.d.ts +21 -0
  29. package/dist/v5/mesh-source-texture.d.ts +4 -0
  30. package/dist/v5/mesh-source.d.ts +34 -0
  31. package/dist/v5/mesh-tileset-archive.d.ts +18 -0
  32. package/dist/v5/mesh-tileset-sink.d.ts +51 -0
  33. package/dist/v5/mesh.d.ts +81 -0
  34. package/dist/v5/point-cloud-conversion.d.ts +31 -0
  35. package/dist/v5/point-cloud-source-encoder.d.ts +49 -0
  36. package/dist/v5/point-cloud-source.d.ts +25 -0
  37. package/dist/v5/point-cloud.d.ts +24 -0
  38. package/dist/v5/resource-manifest.d.ts +58 -0
  39. package/dist/v5/spatial-conversion.d.ts +59 -0
  40. package/dist/v5/tileset-source.d.ts +21 -0
  41. package/package.json +53 -27
  42. package/src/common/README.md +5 -0
  43. package/src/index.ts +1 -2
  44. package/src/{3d-tiles-converter → v4/3d-tiles-converter}/3d-tiles-converter.ts +14 -3
  45. package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/3d-tiles-content-converter.ts +2 -2
  46. package/src/v4/3d-tiles-converter/helpers/validate-i3s-spatial-reference.ts +124 -0
  47. package/src/{3d-tiles-converter → v4/3d-tiles-converter}/json-templates/tileset.ts +4 -4
  48. package/src/{converter-cli.ts → v4/converter-cli.ts} +4 -4
  49. package/src/{deps-installer → v4/deps-installer}/deps-installer.ts +10 -7
  50. package/src/{i3s-converter → v4/i3s-converter}/helpers/attribute-metadata-info.ts +1 -1
  51. package/src/{i3s-converter → v4/i3s-converter}/helpers/batch-ids-extensions.ts +1 -1
  52. package/src/v4/i3s-converter/helpers/encode-string-attribute.ts +23 -0
  53. package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-attributes.ts +1 -1
  54. package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-converter.ts +34 -59
  55. package/src/{i3s-converter → v4/i3s-converter}/helpers/gltf-attributes.ts +7 -6
  56. package/src/{i3s-converter → v4/i3s-converter}/helpers/load-3d-tiles.ts +5 -9
  57. package/src/{i3s-converter → v4/i3s-converter}/helpers/node-debug.ts +5 -6
  58. package/src/{i3s-converter → v4/i3s-converter}/i3s-converter.ts +16 -19
  59. package/src/{i3s-converter → v4/i3s-converter}/json-templates/geometry-definitions.ts +4 -4
  60. package/src/{i3s-converter → v4/i3s-converter}/json-templates/layers.ts +6 -6
  61. package/src/{i3s-converter → v4/i3s-converter}/json-templates/node.ts +1 -1
  62. package/src/{i3s-converter → v4/i3s-converter}/json-templates/scene-server.ts +1 -1
  63. package/src/{i3s-converter → v4/i3s-converter}/json-templates/shared-resources.ts +1 -1
  64. package/src/{i3s-converter → v4/i3s-converter}/json-templates/store.ts +1 -1
  65. package/src/{i3s-server → v4/i3s-server}/README.md +2 -2
  66. package/src/{i3s-server → v4/i3s-server}/controllers/slpk-controller.ts +1 -1
  67. package/src/{i3s-server → v4/i3s-server}/routes/slpk-router.ts +2 -1
  68. package/src/v4/index.ts +2 -0
  69. package/src/{lib → v4/lib}/utils/compress-util.ts +8 -7
  70. package/src/{lib → v4/lib}/utils/conversion-dump.ts +4 -4
  71. package/src/{lib → v4/lib}/utils/file-utils.ts +2 -2
  72. package/src/{lib → v4/lib}/utils/lod-conversion-utils.ts +2 -4
  73. package/src/v5/adapters.ts +56 -0
  74. package/src/v5/browser-sink.ts +130 -0
  75. package/src/v5/browser-source.ts +155 -0
  76. package/src/v5/browser-tileset-source.ts +328 -0
  77. package/src/v5/browser.ts +105 -0
  78. package/src/v5/conversion-api.ts +287 -0
  79. package/src/v5/conversion-archive.ts +85 -0
  80. package/src/v5/core.ts +61 -0
  81. package/src/v5/feature-arrow.ts +429 -0
  82. package/src/v5/i3s-mesh-conversion.ts +152 -0
  83. package/src/v5/index.ts +99 -0
  84. package/src/v5/mesh-conversion.ts +200 -0
  85. package/src/v5/mesh-draco.ts +138 -0
  86. package/src/v5/mesh-source-features.ts +232 -0
  87. package/src/v5/mesh-source-texture.ts +122 -0
  88. package/src/v5/mesh-source.ts +360 -0
  89. package/src/v5/mesh-tileset-archive.ts +50 -0
  90. package/src/v5/mesh-tileset-sink.ts +324 -0
  91. package/src/v5/mesh.ts +517 -0
  92. package/src/v5/point-cloud-conversion.ts +80 -0
  93. package/src/v5/point-cloud-source-encoder.ts +105 -0
  94. package/src/v5/point-cloud-source.ts +84 -0
  95. package/src/v5/point-cloud.ts +271 -0
  96. package/src/v5/resource-manifest.ts +213 -0
  97. package/src/v5/spatial-conversion.ts +305 -0
  98. package/src/v5/tileset-source.ts +54 -0
  99. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/i3s-obb-to-3d-tiles-obb.ts +0 -0
  100. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/load-i3s.ts +0 -0
  101. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/texture-atlas.ts +0 -0
  102. /package/src/{constants.ts → v4/constants.ts} +0 -0
  103. /package/src/{i3s-converter → v4/i3s-converter}/helpers/coordinate-converter.ts +0 -0
  104. /package/src/{i3s-converter → v4/i3s-converter}/helpers/create-scene-server-path.ts +0 -0
  105. /package/src/{i3s-converter → v4/i3s-converter}/helpers/feature-attributes.ts +0 -0
  106. /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-index-document.ts +0 -0
  107. /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-pages.ts +0 -0
  108. /package/src/{i3s-converter → v4/i3s-converter}/helpers/preprocess-3d-tiles.ts +0 -0
  109. /package/src/{i3s-converter → v4/i3s-converter}/helpers/progress.ts +0 -0
  110. /package/src/{i3s-converter → v4/i3s-converter}/helpers/tileset-traversal.ts +0 -0
  111. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/metadata.ts +0 -0
  112. /package/src/{i3s-converter → v4/i3s-converter}/types.ts +0 -0
  113. /package/src/{i3s-server → v4/i3s-server}/app.ts +0 -0
  114. /package/src/{i3s-server → v4/i3s-server}/bin/www.ts +0 -0
  115. /package/src/{i3s-server → v4/i3s-server}/certs/cert.pem +0 -0
  116. /package/src/{i3s-server → v4/i3s-server}/certs/key.pem +0 -0
  117. /package/src/{i3s-server → v4/i3s-server}/utils/create-scene-server.ts +0 -0
  118. /package/src/{i3s-server → v4/i3s-server}/utils/server-utils.ts +0 -0
  119. /package/src/{lib → v4/lib}/json-schemas/conversion-dump-json-schema.ts +0 -0
  120. /package/src/{lib → v4/lib}/utils/cli-utils.ts +0 -0
  121. /package/src/{lib → v4/lib}/utils/geometry-utils.ts +0 -0
  122. /package/src/{lib → v4/lib}/utils/queue.ts +0 -0
  123. /package/src/{lib → v4/lib}/utils/statistic-utils.ts +0 -0
  124. /package/src/{lib → v4/lib}/utils/write-queue.ts +0 -0
  125. /package/src/{pgm-loader.ts → v4/pgm-loader.ts} +0 -0
@@ -0,0 +1,360 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {Matrix4} from '@math.gl/core';
6
+ import type {MeshAttribute} from '@loaders.gl/schema';
7
+ import type {
8
+ GLTFPostprocessed,
9
+ GLTFNodePostprocessed,
10
+ GLTFMeshPrimitivePostprocessed,
11
+ GLTFMaterialPostprocessed
12
+ } from '@loaders.gl/gltf';
13
+ import type {Tileset3D, TilesetSourceMetadata} from '@loaders.gl/tiles';
14
+ import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
15
+ import type {TileConversionSource} from '@loaders.gl/tile-converter/v5/core';
16
+ import {createTilesetConversionSource} from '@loaders.gl/tile-converter/v5/core';
17
+ import type {TilesetConversionSourceOptions} from '@loaders.gl/tile-converter/v5/core';
18
+ import type {MeshConversionInput} from './mesh-conversion.js';
19
+ import type {MeshTileMaterial} from './mesh.js';
20
+ import {validateMeshGeometry} from './mesh.js';
21
+ import {mapMeshSourceTexture} from './mesh-source-texture.js';
22
+ import {extractMeshFeatures} from './mesh-source-features.js';
23
+ import type {MeshSourceFeatureOptions} from './mesh-source-features.js';
24
+ import type {I3SMeshFeatures} from '@loaders.gl/i3s';
25
+ export type {MeshSourceFeatureOptions} from './mesh-source-features.js';
26
+
27
+ /** Source mesh with optional explicit-schema feature data and triangle association. */
28
+ export interface MeshSourceResource extends MeshConversionInput {
29
+ /** Arrow features associated with the selected primitive; IDs are distinct from table row indices. */
30
+ readonly features?: I3SMeshFeatures;
31
+ }
32
+
33
+ /** Narrow source profile and content lifetime controls. */
34
+ export interface MeshTilesetSourceOptions extends TilesetConversionSourceOptions {
35
+ /** Required when the source has batch/structural metadata; no schema is inferred. */
36
+ readonly features?: MeshSourceFeatureOptions;
37
+ }
38
+
39
+ /**
40
+ * Extracts static GLB/B3DM primitives from a native ECEF 3D Tiles runtime, including
41
+ * optional buffer-view or base64 data-URI PNG/JPEG images, TEXCOORD_0, glTF sampling and
42
+ * KHR_texture_transform controls on TEXCOORD_0. Decode content with
43
+ * `gltf.excludeExtensions: {KHR_texture_transform: false}` to retain authored UVs/transforms.
44
+ * Shared traversal retains placement identity and unloads content according to the supplied policy.
45
+ * The adapter applies node transforms, glTF up-axis correction, RTC translation, and tile placement
46
+ * once, retaining Float64 absolute positions and inverse-transpose unit normals. Animation, skins,
47
+ * morphs, instancing, broader texture semantics, unknown extensions, non-affine/mirrored
48
+ * placements, and
49
+ * metadata outside the declared feature profile fail explicitly. Multiple resources can be read;
50
+ * the single-mesh I3S sink rejects a second resource and aborts the entire output.
51
+ * @param tileset - Dedicated native EPSG:4978 runtime with decoded glTF content enabled.
52
+ * @param options - Explicit feature schema/mapping and optional decoded-content cleanup.
53
+ * @returns Portable conversion source; COLOR_0 is preserved for GLB, while I3S rejects vertex colors.
54
+ */
55
+ export function createMeshTilesetConversionSource(
56
+ tileset: Tileset3D,
57
+ options: MeshTilesetSourceOptions = {}
58
+ ): TileConversionSource<TilesetSourceMetadata, MeshSourceResource> {
59
+ const source = createTilesetConversionSource(tileset, options);
60
+ return {
61
+ /** Validates the source frame and supported dataset metadata before traversal. */
62
+ async inspect(signal) {
63
+ const metadata = await source.inspect(signal);
64
+ const reference = metadata.spatialReference;
65
+ if (
66
+ metadata.type !== 'TILES3D' ||
67
+ !reference ||
68
+ reference.status !== 'native' ||
69
+ reference.sourceCrs !== 'EPSG:4978' ||
70
+ reference.heightReference !== 'ellipsoidal' ||
71
+ reference.axisOrder !== 'xyz' ||
72
+ reference.verticalUnitScale !== 1 ||
73
+ reference.units?.some(unit => unit !== 'meter')
74
+ ) {
75
+ throw new TileConversionError(
76
+ 'MESH_SOURCE_FRAME_UNSUPPORTED',
77
+ 'Mesh extraction requires native EPSG:4978 xyz coordinates in meters and ellipsoidal heights'
78
+ );
79
+ }
80
+ if (metadata.tileset.metadata || metadata.tileset.groups?.length) {
81
+ throw new TileConversionError(
82
+ 'MESH_SOURCE_METADATA_UNSUPPORTED',
83
+ 'Tileset/group metadata requires a separate mapping'
84
+ );
85
+ }
86
+ return metadata;
87
+ },
88
+ /** Yields each primitive placement while the shared traversal owns content lifetime. */
89
+ async *read(inspection, signal) {
90
+ for await (const item of source.read(inspection, signal)) {
91
+ const placementReference = item.tile.header._spatialReference || tileset.spatialReference;
92
+ if (
93
+ placementReference &&
94
+ (placementReference.status !== 'native' || placementReference.sourceCrs !== 'EPSG:4978')
95
+ )
96
+ throw new TileConversionError(
97
+ 'MESH_SOURCE_FRAME_UNSUPPORTED',
98
+ 'Nested mesh placements require native ECEF coordinates'
99
+ );
100
+ if (item.tile.header.metadata || item.tile.header.implicitMetadata)
101
+ throw new TileConversionError(
102
+ 'MESH_SOURCE_METADATA_UNSUPPORTED',
103
+ 'Tile metadata requires a separate mapping'
104
+ );
105
+ for (const content of item.contents) {
106
+ signal?.throwIfAborted();
107
+ if (content.metadata || content.group !== undefined)
108
+ throw new TileConversionError(
109
+ 'MESH_SOURCE_METADATA_UNSUPPORTED',
110
+ 'Content metadata requires a separate mapping'
111
+ );
112
+ const payload = content.payload as {
113
+ gltf?: GLTFPostprocessed;
114
+ gltfUpAxis?: string;
115
+ rtcCenter?: number[];
116
+ type?: string;
117
+ batchTableJson?: Record<string, unknown>;
118
+ header?: {batchLength?: number};
119
+ featureTableJson?: Record<string, unknown>;
120
+ };
121
+ if (
122
+ !payload?.gltf ||
123
+ !['glTF', 'gltf', 'b3dm'].includes(payload.type || content.type || '')
124
+ ) {
125
+ throw new TileConversionError(
126
+ 'MESH_SOURCE_CONTENT_UNSUPPORTED',
127
+ 'Mesh extraction requires decoded GLB/glTF or B3DM content'
128
+ );
129
+ }
130
+ const gltf = payload.gltf;
131
+ if (
132
+ gltf.animations?.length ||
133
+ gltf.skins?.length ||
134
+ Object.keys(gltf.extensions || {}).some(
135
+ name => !['EXT_structural_metadata', 'CESIUM_RTC'].includes(name)
136
+ )
137
+ ) {
138
+ throw new TileConversionError(
139
+ 'MESH_SOURCE_SCENE_UNSUPPORTED',
140
+ 'Animated/skinned scenes and unmapped scene extensions are unsupported'
141
+ );
142
+ }
143
+ const axis = payload.gltfUpAxis || 'Y';
144
+ if (!['X', 'Y', 'Z'].includes(axis))
145
+ throw new TileConversionError('MESH_SOURCE_AXIS_UNSUPPORTED', 'Unknown glTF up axis');
146
+ const placement = new Matrix4(item.tile.computedTransform);
147
+ const rtc = payload.rtcCenter || gltf.extensions?.CESIUM_RTC?.center;
148
+ if (rtc) {
149
+ if (rtc.length !== 3 || rtc.some((value: number) => !Number.isFinite(value)))
150
+ throw new TileConversionError(
151
+ 'MESH_SOURCE_TRANSFORM_INVALID',
152
+ 'RTC center must contain finite xyz coordinates'
153
+ );
154
+ placement.translate(rtc);
155
+ }
156
+ if (axis === 'Y')
157
+ placement.multiplyRight(new Matrix4([1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1]));
158
+ if (axis === 'X')
159
+ placement.multiplyRight(new Matrix4([0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 1]));
160
+ const roots =
161
+ gltf.scene?.nodes || (gltf.scenes?.length === 1 ? gltf.scenes[0].nodes : undefined);
162
+ if (!roots?.length)
163
+ throw new TileConversionError(
164
+ 'MESH_SOURCE_SCENE_UNSUPPORTED',
165
+ 'A default scene or exactly one scene is required'
166
+ );
167
+ let primitiveIndex = 0;
168
+ for (const selected of traverseMeshNodes(roots, placement)) {
169
+ const resource = extractPrimitive(
170
+ selected.primitive,
171
+ selected.transform,
172
+ `${item.tile.id}/${content.index}/${primitiveIndex++}`
173
+ );
174
+ const features = extractMeshFeatures(
175
+ gltf,
176
+ selected.primitive,
177
+ payload,
178
+ resource.mesh,
179
+ options.features
180
+ );
181
+ yield {...resource, features};
182
+ }
183
+ if (!primitiveIndex)
184
+ throw new TileConversionError(
185
+ 'MESH_SOURCE_CONTENT_EMPTY',
186
+ 'Declared content contains no supported mesh primitive'
187
+ );
188
+ }
189
+ }
190
+ }
191
+ };
192
+ }
193
+
194
+ /** Walks scene placements without deduplicating a shared mesh or mutating postprocessed nodes. */
195
+ function* traverseMeshNodes(
196
+ nodes: readonly GLTFNodePostprocessed[],
197
+ parent: Matrix4,
198
+ ancestors = new Set<GLTFNodePostprocessed>()
199
+ ): Generator<{primitive: GLTFMeshPrimitivePostprocessed; transform: Matrix4}> {
200
+ for (const node of nodes) {
201
+ if (
202
+ ancestors.has(node) ||
203
+ node.skin ||
204
+ node.weights ||
205
+ Object.keys(node.extensions || {}).length
206
+ )
207
+ throw new TileConversionError(
208
+ 'MESH_SOURCE_NODE_UNSUPPORTED',
209
+ 'Cyclic, skinned, morphed, or extended nodes are unsupported'
210
+ );
211
+ const transform = new Matrix4(parent).multiplyRight(
212
+ node.matrix
213
+ ? new Matrix4(node.matrix)
214
+ : new Matrix4()
215
+ .translate(node.translation || [0, 0, 0])
216
+ .multiplyRight(new Matrix4().fromQuaternion(node.rotation || [0, 0, 0, 1]))
217
+ .scale(node.scale || [1, 1, 1])
218
+ );
219
+ if (node.mesh) {
220
+ if (node.mesh.weights || Object.keys(node.mesh.extensions || {}).length)
221
+ throw new TileConversionError(
222
+ 'MESH_SOURCE_NODE_UNSUPPORTED',
223
+ 'Morph weights and mesh extensions are unsupported'
224
+ );
225
+ for (const primitive of node.mesh.primitives) yield {primitive, transform};
226
+ }
227
+ yield* traverseMeshNodes(node.children || [], transform, new Set([...ancestors, node]));
228
+ }
229
+ }
230
+
231
+ /** Copies one packed primitive, places it once, and derives a useful ECEF local origin. */
232
+ function extractPrimitive(
233
+ primitive: GLTFMeshPrimitivePostprocessed,
234
+ transform: Matrix4,
235
+ id: string
236
+ ): MeshSourceResource {
237
+ if (
238
+ primitive.targets?.length ||
239
+ Object.keys(primitive.extensions || {}).some(name => name !== 'EXT_mesh_features')
240
+ )
241
+ throw new TileConversionError(
242
+ 'MESH_SOURCE_PRIMITIVE_UNSUPPORTED',
243
+ 'Morph targets and unmapped primitive extensions are unsupported'
244
+ );
245
+ if (
246
+ Array.from(transform).some(value => !Number.isFinite(value)) ||
247
+ transform[3] !== 0 ||
248
+ transform[7] !== 0 ||
249
+ transform[11] !== 0 ||
250
+ transform[15] !== 1 ||
251
+ transform.determinant() <= 0
252
+ )
253
+ throw new TileConversionError(
254
+ 'MESH_SOURCE_TRANSFORM_INVALID',
255
+ 'Mesh placement requires a finite invertible affine transform with positive determinant'
256
+ );
257
+ const attributes: Record<string, MeshAttribute> = {};
258
+ for (const [name, accessor] of Object.entries(primitive.attributes)) {
259
+ if (name === '_BATCHID' || /^_FEATURE_ID_\d+$/.test(name)) continue;
260
+ if (!['POSITION', 'NORMAL', 'COLOR_0', 'TEXCOORD_0'].includes(name))
261
+ throw new TileConversionError(
262
+ 'MESH_SOURCE_ATTRIBUTE_UNSUPPORTED',
263
+ `Unsupported mesh attribute ${name}`
264
+ );
265
+ attributes[name] = {
266
+ value: accessor.value as MeshAttribute['value'],
267
+ size: accessor.components,
268
+ ...(accessor.normalized ? {normalized: true} : {})
269
+ };
270
+ }
271
+ const mesh = validateMeshGeometry(
272
+ {
273
+ topology: 'triangle-list',
274
+ mode: primitive.mode ?? 4,
275
+ attributes,
276
+ indices: primitive.indices
277
+ ? {value: primitive.indices.value as MeshAttribute['value'], size: 1}
278
+ : undefined
279
+ },
280
+ true
281
+ );
282
+ const sourcePositions = mesh.attributes.POSITION.value;
283
+ const positions = new Float64Array(sourcePositions.length);
284
+ const minimum = [Infinity, Infinity, Infinity];
285
+ const maximum = [-Infinity, -Infinity, -Infinity];
286
+ for (let index = 0; index < positions.length; index += 3) {
287
+ const placed = transform.transformAsPoint(
288
+ Array.from(sourcePositions.subarray(index, index + 3))
289
+ );
290
+ positions.set(placed, index);
291
+ for (let axis = 0; axis < 3; axis++) {
292
+ minimum[axis] = Math.min(minimum[axis], placed[axis]);
293
+ maximum[axis] = Math.max(maximum[axis], placed[axis]);
294
+ }
295
+ }
296
+ mesh.attributes.POSITION = {value: positions, size: 3};
297
+ if (mesh.attributes.NORMAL) {
298
+ const values = mesh.attributes.NORMAL.value;
299
+ const normalMatrix = new Matrix4(transform).invert().transpose();
300
+ const normals = new Float32Array(values.length);
301
+ for (let index = 0; index < values.length; index += 3) {
302
+ const normal = normalMatrix.transformAsVector(Array.from(values.subarray(index, index + 3)));
303
+ const length = Math.hypot(...normal);
304
+ normals.set(
305
+ normal.map(value => value / length),
306
+ index
307
+ );
308
+ }
309
+ mesh.attributes.NORMAL = {value: normals, size: 3};
310
+ }
311
+ return {
312
+ id,
313
+ mesh,
314
+ origin: minimum.map((value, axis) => value / 2 + maximum[axis] / 2) as [number, number, number],
315
+ material: mapMaterial(primitive.material)
316
+ };
317
+ }
318
+
319
+ /** Maps supported PBR controls and an embedded base-color image, rejecting unmapped semantics. */
320
+ function mapMaterial(
321
+ material: GLTFMaterialPostprocessed | undefined
322
+ ): MeshTileMaterial | undefined {
323
+ if (!material) return undefined;
324
+ const pbr = material.pbrMetallicRoughness;
325
+ if (
326
+ Object.keys(material).some(
327
+ name =>
328
+ ![
329
+ 'id',
330
+ 'name',
331
+ 'pbrMetallicRoughness',
332
+ 'alphaMode',
333
+ 'alphaCutoff',
334
+ 'doubleSided',
335
+ 'emissiveFactor',
336
+ 'extras'
337
+ ].includes(name)
338
+ ) ||
339
+ Object.keys(pbr || {}).some(
340
+ name =>
341
+ !['baseColorFactor', 'baseColorTexture', 'metallicFactor', 'roughnessFactor'].includes(name)
342
+ ) ||
343
+ material.emissiveFactor?.some(value => value !== 0)
344
+ )
345
+ throw new TileConversionError(
346
+ 'MESH_SOURCE_MATERIAL_UNSUPPORTED',
347
+ 'Only metallic-roughness factors and one embedded base-color image are supported'
348
+ );
349
+ return {
350
+ baseColorFactor: pbr?.baseColorFactor as [number, number, number, number] | undefined,
351
+ ...('baseColorTexture' in (pbr || {})
352
+ ? {baseColorTexture: mapMeshSourceTexture(pbr!.baseColorTexture)}
353
+ : {}),
354
+ metallicFactor: pbr?.metallicFactor,
355
+ roughnessFactor: pbr?.roughnessFactor,
356
+ alphaMode: material.alphaMode as MeshTileMaterial['alphaMode'],
357
+ ...(material.alphaMode === 'MASK' ? {alphaCutoff: material.alphaCutoff ?? 0.5} : {}),
358
+ doubleSided: material.doubleSided
359
+ };
360
+ }
@@ -0,0 +1,50 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {Tiles3DArchiveWriter} from '@loaders.gl/3d-tiles/3d-tiles-archive-writer';
6
+ import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
7
+ import type {BrowserTileConversionFile} from '@loaders.gl/tile-converter/v5/core';
8
+
9
+ /** Output-size budget for a single-mesh 3TZ archive. */
10
+ export interface SingleMeshTilesetArchiveOptions {
11
+ /** Maximum final archive bytes, including ZIP headers and index; not peak serialization memory. */
12
+ readonly maxArchiveBytes: number;
13
+ /** Cooperative packaging cancellation; active Blob reads and entry encodes finish first. */
14
+ readonly signal?: AbortSignal;
15
+ }
16
+
17
+ /**
18
+ * Packages the two unmodified files from a finalized single-mesh sink as a downloadable 3TZ Blob.
19
+ * The writer checks the complete archive budget before reading either Blob. Applications own
20
+ * object-URL creation/revocation and download UI. Retained input Blobs and transient ZIP buffers
21
+ * are additional memory. SLPK requires separately authored I3S scene-layer resources.
22
+ * @param files - Files returned by a successfully finalized createSingleMeshTilesetSink.
23
+ * @param options - Required final archive-size budget.
24
+ * @returns An indexed 3TZ Blob; save with the .3tz extension.
25
+ */
26
+ export async function createSingleMeshTilesetArchive(
27
+ files: readonly BrowserTileConversionFile[],
28
+ options: SingleMeshTilesetArchiveOptions
29
+ ): Promise<Blob> {
30
+ const {maxArchiveBytes, signal} = options;
31
+ if (!Number.isSafeInteger(maxArchiveBytes) || maxArchiveBytes < 0) {
32
+ throw new TileConversionError(
33
+ 'INVALID_ARCHIVE_BYTE_LIMIT',
34
+ 'Archive byte limit must be a non-negative safe integer'
35
+ );
36
+ }
37
+ if (
38
+ files.length !== 2 ||
39
+ !files.some(file => file.resourceId === 'mesh.glb') ||
40
+ !files.some(file => file.resourceId === 'tileset.json')
41
+ ) {
42
+ throw new TileConversionError(
43
+ 'INVALID_SINGLE_MESH_ARCHIVE',
44
+ 'Expected mesh.glb and tileset.json from a finalized single-mesh sink'
45
+ );
46
+ }
47
+ const resources = Object.fromEntries(files.map(file => [file.resourceId, file.blob]));
48
+ const archive = await Tiles3DArchiveWriter.encode(resources, {'3tz': {maxArchiveBytes, signal}});
49
+ return new Blob([archive], {type: Tiles3DArchiveWriter.mimeTypes[0]});
50
+ }