@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,169 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {getBinaryImageMetadata} from '@loaders.gl/images';
6
+ import type {GLTFMaterialPostprocessed, GLTFImagePostprocessed} from '@loaders.gl/gltf';
7
+ import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
8
+ import type {MeshTileSampler, MeshTileTexture, MeshTileTextureTransform} from './mesh.js';
9
+
10
+ /** Maps a buffer-view or base64 PNG/JPEG image, sampling and UV transform without decoding pixels. */
11
+ export function mapMeshSourceTexture(
12
+ textureInfo: NonNullable<GLTFMaterialPostprocessed['pbrMetallicRoughness']>['baseColorTexture']
13
+ ): MeshTileTexture {
14
+ const texture = textureInfo?.texture;
15
+ const image = texture?.source;
16
+ const sampler = texture?.sampler;
17
+ const extensions = textureInfo?.extensions || {};
18
+ const hasTransform = Object.hasOwn(extensions, 'KHR_texture_transform');
19
+ const transform = extensions.KHR_texture_transform;
20
+ if (
21
+ !textureInfo ||
22
+ (textureInfo.texCoord !== undefined && textureInfo.texCoord !== 0) ||
23
+ Object.keys(extensions).some(name => name !== 'KHR_texture_transform') ||
24
+ (hasTransform &&
25
+ (!transform ||
26
+ typeof transform !== 'object' ||
27
+ Array.isArray(transform) ||
28
+ (transform.texCoord !== undefined && transform.texCoord !== 0) ||
29
+ Object.keys(transform.extensions || {}).length ||
30
+ Object.keys(transform).some(
31
+ name =>
32
+ !['offset', 'rotation', 'scale', 'texCoord', 'extensions', 'extras'].includes(name)
33
+ ))) ||
34
+ Object.keys(textureInfo).some(
35
+ name => !['id', 'index', 'texture', 'texCoord', 'extensions', 'extras'].includes(name)
36
+ ) ||
37
+ !texture ||
38
+ Object.keys(texture).some(
39
+ name => !['id', 'source', 'sampler', 'name', 'extensions', 'extras'].includes(name)
40
+ ) ||
41
+ Object.keys(texture.extensions || {}).length ||
42
+ !image ||
43
+ (image.uri !== undefined && image.bufferView !== undefined) ||
44
+ Object.keys(image.extensions || {}).length ||
45
+ (image.uri === undefined &&
46
+ (!(image.bufferView?.data instanceof Uint8Array) ||
47
+ Object.keys(image.bufferView.extensions || {}).length ||
48
+ !['image/png', 'image/jpeg'].includes(image.mimeType || ''))) ||
49
+ (sampler &&
50
+ (Object.keys(sampler.extensions || {}).length ||
51
+ Object.keys(sampler).some(
52
+ name =>
53
+ ![
54
+ 'id',
55
+ 'parameters',
56
+ 'wrapS',
57
+ 'wrapT',
58
+ 'magFilter',
59
+ 'minFilter',
60
+ 'name',
61
+ 'extensions',
62
+ 'extras'
63
+ ].includes(name)
64
+ )))
65
+ ) {
66
+ throw new TileConversionError(
67
+ 'MESH_SOURCE_TEXTURE_UNSUPPORTED',
68
+ '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'
69
+ );
70
+ }
71
+ const selectedSampler = sampler
72
+ ? ({
73
+ wrapS: sampler.wrapS,
74
+ wrapT: sampler.wrapT,
75
+ magFilter: sampler.magFilter,
76
+ minFilter: sampler.minFilter
77
+ } as MeshTileSampler)
78
+ : undefined;
79
+ return {
80
+ ...mapMeshSourceImage(image),
81
+ ...(hasTransform
82
+ ? {
83
+ transform: {
84
+ offset: transform.offset,
85
+ rotation: transform.rotation,
86
+ scale: transform.scale
87
+ } as MeshTileTextureTransform
88
+ }
89
+ : {}),
90
+ ...(selectedSampler && Object.values(selectedSampler).some(value => value !== undefined)
91
+ ? {sampler: selectedSampler}
92
+ : {})
93
+ };
94
+ }
95
+
96
+ /** Reads self-contained encoded bytes without fetch, pixel decoding, or Node APIs. */
97
+ function mapMeshSourceImage(
98
+ image: GLTFImagePostprocessed
99
+ ): Pick<MeshTileTexture, 'data' | 'mimeType'> {
100
+ if (image.uri === undefined) {
101
+ return {data: image.bufferView!.data, mimeType: image.mimeType as MeshTileTexture['mimeType']};
102
+ }
103
+ const match =
104
+ typeof image.uri === 'string'
105
+ ? /^data:(image\/(?:png|jpeg));base64,([A-Za-z0-9+/]+={0,2})$/.exec(image.uri)
106
+ : null;
107
+ if (!match || (image.mimeType !== undefined && image.mimeType !== match[1])) {
108
+ throw new TileConversionError(
109
+ 'MESH_SOURCE_TEXTURE_UNSUPPORTED',
110
+ 'Inline images require a PNG/JPEG base64 data URI and matching optional mimeType'
111
+ );
112
+ }
113
+ let encodedBytes: string;
114
+ try {
115
+ encodedBytes = atob(match[2]);
116
+ } catch {
117
+ throw new TileConversionError('MESH_SOURCE_TEXTURE_UNSUPPORTED', 'Invalid inline image base64');
118
+ }
119
+ return {
120
+ data: Uint8Array.from(encodedBytes, character => character.charCodeAt(0)),
121
+ mimeType: match[1] as MeshTileTexture['mimeType']
122
+ };
123
+ }
124
+
125
+ /** Resolves one external encoded image through an explicitly supplied application reader. */
126
+ export async function resolveMeshSourceTexture(
127
+ textureInfo: NonNullable<GLTFMaterialPostprocessed['pbrMetallicRoughness']>['baseColorTexture'],
128
+ readExternalResource:
129
+ | ((uri: string, contentUri: string | undefined, signal?: AbortSignal) => Promise<Uint8Array>)
130
+ | undefined,
131
+ contentUri: string | undefined,
132
+ signal?: AbortSignal
133
+ ): Promise<MeshTileTexture> {
134
+ const image = textureInfo?.texture?.source;
135
+ if (!image || image.uri === undefined || image.uri.startsWith('data:'))
136
+ return mapMeshSourceTexture(textureInfo);
137
+ if (!readExternalResource || image.bufferView !== undefined)
138
+ throw new TileConversionError(
139
+ 'MESH_SOURCE_TEXTURE_UNSUPPORTED',
140
+ 'External images require an explicit bounded resource reader'
141
+ );
142
+ signal?.throwIfAborted();
143
+ const data = await readExternalResource(image.uri, contentUri, signal);
144
+ signal?.throwIfAborted();
145
+ let metadata;
146
+ try {
147
+ metadata = getBinaryImageMetadata(new DataView(data.buffer, data.byteOffset, data.byteLength));
148
+ } catch {
149
+ /* Report an unsupported encoded image consistently. */
150
+ }
151
+ if (
152
+ !metadata ||
153
+ !['image/png', 'image/jpeg'].includes(metadata.mimeType) ||
154
+ (image.mimeType !== undefined && image.mimeType !== metadata.mimeType)
155
+ )
156
+ throw new TileConversionError(
157
+ 'MESH_SOURCE_TEXTURE_UNSUPPORTED',
158
+ 'External images require matching PNG/JPEG headers'
159
+ );
160
+ // Select encoded bytes without decoding pixels or modifying the caller-owned glTF image.
161
+ const resolved = {
162
+ ...textureInfo,
163
+ texture: {
164
+ ...textureInfo!.texture,
165
+ source: {...image, uri: undefined, mimeType: metadata.mimeType, bufferView: {data}}
166
+ }
167
+ };
168
+ return mapMeshSourceTexture(resolved as typeof textureInfo);
169
+ }
@@ -0,0 +1,422 @@
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, validateMeshIndices} from './mesh.js';
21
+ import {resolveMeshSourceTexture} 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
+ /** Explicit bounded reader for external encoded images, relative to the content URI. */
38
+ readonly readExternalResource?: (
39
+ uri: string,
40
+ contentUri: string | undefined,
41
+ signal?: AbortSignal
42
+ ) => Promise<Uint8Array>;
43
+ }
44
+
45
+ /**
46
+ * Extracts static GLB/B3DM primitives from a native ECEF 3D Tiles runtime, including
47
+ * optional buffer-view or base64 data-URI PNG/JPEG images, TEXCOORD_0, glTF sampling and
48
+ * KHR_texture_transform controls on TEXCOORD_0. Decode content with
49
+ * `gltf.excludeExtensions: {KHR_texture_transform: false}` to retain authored UVs/transforms.
50
+ * Triangle strips are expanded with alternating winding; the glTF loader already expands fans.
51
+ * Shared traversal retains placement identity and unloads content according to the supplied policy.
52
+ * The adapter applies node transforms, glTF up-axis correction, RTC translation, and tile placement
53
+ * once, retaining Float64 absolute positions and inverse-transpose unit normals. Animation, skins,
54
+ * morphs, instancing, broader texture semantics, unknown extensions, non-affine/mirrored
55
+ * placements, and
56
+ * metadata outside the declared feature profile fail explicitly. Multiple resources can be read;
57
+ * collection sinks accept bounded multiple placements; the compatibility single-mesh sink rejects a second resource.
58
+ * @param tileset - Dedicated native EPSG:4978 runtime with decoded glTF content enabled.
59
+ * @param options - Explicit feature schema/mapping and optional decoded-content cleanup.
60
+ * @returns Portable conversion source; COLOR_0 is preserved for GLB, while I3S rejects vertex colors.
61
+ */
62
+ export function createMeshTilesetConversionSource(
63
+ tileset: Tileset3D,
64
+ options: MeshTilesetSourceOptions = {}
65
+ ): TileConversionSource<TilesetSourceMetadata, MeshSourceResource> {
66
+ const source = createTilesetConversionSource(tileset, options);
67
+ return {
68
+ /** Validates the source frame and supported dataset metadata before traversal. */
69
+ async inspect(signal) {
70
+ const metadata = await source.inspect(signal);
71
+ const reference = metadata.spatialReference;
72
+ if (
73
+ metadata.type !== 'TILES3D' ||
74
+ !reference ||
75
+ reference.status !== 'native' ||
76
+ reference.sourceCrs !== 'EPSG:4978' ||
77
+ reference.heightReference !== 'ellipsoidal' ||
78
+ reference.axisOrder !== 'xyz' ||
79
+ reference.verticalUnitScale !== 1 ||
80
+ reference.units?.some(unit => unit !== 'meter')
81
+ ) {
82
+ throw new TileConversionError(
83
+ 'MESH_SOURCE_FRAME_UNSUPPORTED',
84
+ 'Mesh extraction requires native EPSG:4978 xyz coordinates in meters and ellipsoidal heights'
85
+ );
86
+ }
87
+ if (metadata.tileset.metadata || metadata.tileset.groups?.length) {
88
+ throw new TileConversionError(
89
+ 'MESH_SOURCE_METADATA_UNSUPPORTED',
90
+ 'Tileset/group metadata requires a separate mapping'
91
+ );
92
+ }
93
+ return metadata;
94
+ },
95
+ /** Yields each primitive placement while the shared traversal owns content lifetime. */
96
+ async *read(inspection, signal) {
97
+ for await (const item of source.read(inspection, signal)) {
98
+ const placementReference = item.tile.header._spatialReference || tileset.spatialReference;
99
+ if (
100
+ placementReference &&
101
+ (placementReference.status !== 'native' || placementReference.sourceCrs !== 'EPSG:4978')
102
+ )
103
+ throw new TileConversionError(
104
+ 'MESH_SOURCE_FRAME_UNSUPPORTED',
105
+ 'Nested mesh placements require native ECEF coordinates'
106
+ );
107
+ if (item.tile.header.metadata || item.tile.header.implicitMetadata)
108
+ throw new TileConversionError(
109
+ 'MESH_SOURCE_METADATA_UNSUPPORTED',
110
+ 'Tile metadata requires a separate mapping'
111
+ );
112
+ for (const content of item.contents) {
113
+ signal?.throwIfAborted();
114
+ if (content.metadata || content.group !== undefined)
115
+ throw new TileConversionError(
116
+ 'MESH_SOURCE_METADATA_UNSUPPORTED',
117
+ 'Content metadata requires a separate mapping'
118
+ );
119
+ const payload = content.payload as {
120
+ gltf?: GLTFPostprocessed;
121
+ gltfUpAxis?: string;
122
+ rtcCenter?: number[];
123
+ type?: string;
124
+ batchTableJson?: Record<string, unknown>;
125
+ header?: {batchLength?: number};
126
+ featureTableJson?: Record<string, unknown>;
127
+ };
128
+ if (
129
+ !payload?.gltf ||
130
+ !['glTF', 'gltf', 'b3dm'].includes(payload.type || content.type || '')
131
+ ) {
132
+ throw new TileConversionError(
133
+ 'MESH_SOURCE_CONTENT_UNSUPPORTED',
134
+ 'Mesh extraction requires decoded GLB/glTF or B3DM content'
135
+ );
136
+ }
137
+ const gltf = payload.gltf;
138
+ if (
139
+ gltf.animations?.length ||
140
+ gltf.skins?.length ||
141
+ Object.keys(gltf.extensions || {}).some(
142
+ name => !['EXT_structural_metadata', 'CESIUM_RTC'].includes(name)
143
+ )
144
+ ) {
145
+ throw new TileConversionError(
146
+ 'MESH_SOURCE_SCENE_UNSUPPORTED',
147
+ 'Animated/skinned scenes and unmapped scene extensions are unsupported'
148
+ );
149
+ }
150
+ const axis = payload.gltfUpAxis || 'Y';
151
+ if (!['X', 'Y', 'Z'].includes(axis))
152
+ throw new TileConversionError('MESH_SOURCE_AXIS_UNSUPPORTED', 'Unknown glTF up axis');
153
+ const placement = new Matrix4(item.tile.computedTransform);
154
+ const rtc = payload.rtcCenter || gltf.extensions?.CESIUM_RTC?.center;
155
+ if (rtc) {
156
+ if (rtc.length !== 3 || rtc.some((value: number) => !Number.isFinite(value)))
157
+ throw new TileConversionError(
158
+ 'MESH_SOURCE_TRANSFORM_INVALID',
159
+ 'RTC center must contain finite xyz coordinates'
160
+ );
161
+ placement.translate(rtc);
162
+ }
163
+ if (axis === 'Y')
164
+ placement.multiplyRight(new Matrix4([1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1]));
165
+ if (axis === 'X')
166
+ placement.multiplyRight(new Matrix4([0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 1]));
167
+ const roots =
168
+ gltf.scene?.nodes || (gltf.scenes?.length === 1 ? gltf.scenes[0].nodes : undefined);
169
+ if (!roots?.length)
170
+ throw new TileConversionError(
171
+ 'MESH_SOURCE_SCENE_UNSUPPORTED',
172
+ 'A default scene or exactly one scene is required'
173
+ );
174
+ let primitiveIndex = 0;
175
+ for (const selected of traverseMeshNodes(roots, placement)) {
176
+ const resource = await extractPrimitive(
177
+ selected.primitive,
178
+ selected.transform,
179
+ `${item.tile.id}/${content.index}/${primitiveIndex++}`,
180
+ options.readExternalResource,
181
+ content.uri,
182
+ signal
183
+ );
184
+ const features = extractMeshFeatures(
185
+ gltf,
186
+ selected.primitive,
187
+ payload,
188
+ resource.mesh,
189
+ options.features
190
+ );
191
+ yield {...resource, features};
192
+ }
193
+ if (!primitiveIndex)
194
+ throw new TileConversionError(
195
+ 'MESH_SOURCE_CONTENT_EMPTY',
196
+ 'Declared content contains no supported mesh primitive'
197
+ );
198
+ }
199
+ }
200
+ }
201
+ };
202
+ }
203
+
204
+ /** Walks scene placements without deduplicating a shared mesh or mutating postprocessed nodes. */
205
+ function* traverseMeshNodes(
206
+ nodes: readonly GLTFNodePostprocessed[],
207
+ parent: Matrix4,
208
+ ancestors = new Set<GLTFNodePostprocessed>()
209
+ ): Generator<{primitive: GLTFMeshPrimitivePostprocessed; transform: Matrix4}> {
210
+ for (const node of nodes) {
211
+ if (
212
+ ancestors.has(node) ||
213
+ node.skin ||
214
+ node.weights ||
215
+ Object.keys(node.extensions || {}).length
216
+ )
217
+ throw new TileConversionError(
218
+ 'MESH_SOURCE_NODE_UNSUPPORTED',
219
+ 'Cyclic, skinned, morphed, or extended nodes are unsupported'
220
+ );
221
+ const transform = new Matrix4(parent).multiplyRight(
222
+ node.matrix
223
+ ? new Matrix4(node.matrix)
224
+ : new Matrix4()
225
+ .translate(node.translation || [0, 0, 0])
226
+ .multiplyRight(new Matrix4().fromQuaternion(node.rotation || [0, 0, 0, 1]))
227
+ .scale(node.scale || [1, 1, 1])
228
+ );
229
+ if (node.mesh) {
230
+ if (node.mesh.weights || Object.keys(node.mesh.extensions || {}).length)
231
+ throw new TileConversionError(
232
+ 'MESH_SOURCE_NODE_UNSUPPORTED',
233
+ 'Morph weights and mesh extensions are unsupported'
234
+ );
235
+ for (const primitive of node.mesh.primitives) yield {primitive, transform};
236
+ }
237
+ yield* traverseMeshNodes(node.children || [], transform, new Set([...ancestors, node]));
238
+ }
239
+ }
240
+
241
+ /** Copies one packed primitive, places it once, and derives a useful ECEF local origin. */
242
+ async function extractPrimitive(
243
+ primitive: GLTFMeshPrimitivePostprocessed,
244
+ transform: Matrix4,
245
+ id: string,
246
+ readExternalResource: MeshTilesetSourceOptions['readExternalResource'],
247
+ contentUri: string | undefined,
248
+ signal?: AbortSignal
249
+ ): Promise<MeshSourceResource> {
250
+ if (
251
+ primitive.targets?.length ||
252
+ Object.keys(primitive.extensions || {}).some(name => name !== 'EXT_mesh_features')
253
+ )
254
+ throw new TileConversionError(
255
+ 'MESH_SOURCE_PRIMITIVE_UNSUPPORTED',
256
+ 'Morph targets and unmapped primitive extensions are unsupported'
257
+ );
258
+ if (
259
+ Array.from(transform).some(value => !Number.isFinite(value)) ||
260
+ transform[3] !== 0 ||
261
+ transform[7] !== 0 ||
262
+ transform[11] !== 0 ||
263
+ transform[15] !== 1 ||
264
+ transform.determinant() <= 0
265
+ )
266
+ throw new TileConversionError(
267
+ 'MESH_SOURCE_TRANSFORM_INVALID',
268
+ 'Mesh placement requires a finite invertible affine transform with positive determinant'
269
+ );
270
+ const attributes: Record<string, MeshAttribute> = {};
271
+ for (const [name, accessor] of Object.entries(primitive.attributes)) {
272
+ if (name === '_BATCHID' || /^_FEATURE_ID_\d+$/.test(name)) continue;
273
+ if (!['POSITION', 'NORMAL', 'COLOR_0', 'TEXCOORD_0'].includes(name))
274
+ throw new TileConversionError(
275
+ 'MESH_SOURCE_ATTRIBUTE_UNSUPPORTED',
276
+ `Unsupported mesh attribute ${name}`
277
+ );
278
+ attributes[name] = {
279
+ value: accessor.value as MeshAttribute['value'],
280
+ size: accessor.components,
281
+ ...(accessor.normalized ? {normalized: true} : {})
282
+ };
283
+ }
284
+ const mesh = validateMeshGeometry(
285
+ {
286
+ topology: 'triangle-list',
287
+ mode: primitive.mode === 5 ? 4 : (primitive.mode ?? 4),
288
+ attributes,
289
+ indices: createTriangleIndices(primitive, (attributes.POSITION?.value.length ?? 0) / 3)
290
+ },
291
+ true
292
+ );
293
+ const sourcePositions = mesh.attributes.POSITION.value;
294
+ const positions = new Float64Array(sourcePositions.length);
295
+ const minimum = [Infinity, Infinity, Infinity];
296
+ const maximum = [-Infinity, -Infinity, -Infinity];
297
+ for (let index = 0; index < positions.length; index += 3) {
298
+ const placed = transform.transformAsPoint(
299
+ Array.from(sourcePositions.subarray(index, index + 3))
300
+ );
301
+ positions.set(placed, index);
302
+ for (let axis = 0; axis < 3; axis++) {
303
+ minimum[axis] = Math.min(minimum[axis], placed[axis]);
304
+ maximum[axis] = Math.max(maximum[axis], placed[axis]);
305
+ }
306
+ }
307
+ mesh.attributes.POSITION = {value: positions, size: 3};
308
+ if (mesh.attributes.NORMAL) {
309
+ const values = mesh.attributes.NORMAL.value;
310
+ const normalMatrix = new Matrix4(transform).invert().transpose();
311
+ const normals = new Float32Array(values.length);
312
+ for (let index = 0; index < values.length; index += 3) {
313
+ const normal = normalMatrix.transformAsVector(Array.from(values.subarray(index, index + 3)));
314
+ const length = Math.hypot(...normal);
315
+ normals.set(
316
+ normal.map(value => value / length),
317
+ index
318
+ );
319
+ }
320
+ mesh.attributes.NORMAL = {value: normals, size: 3};
321
+ }
322
+ return {
323
+ id,
324
+ mesh,
325
+ origin: minimum.map((value, axis) => value / 2 + maximum[axis] / 2) as [number, number, number],
326
+ material: await mapMeshSourceMaterial(
327
+ primitive.material,
328
+ readExternalResource,
329
+ contentUri,
330
+ signal
331
+ )
332
+ };
333
+ }
334
+
335
+ /** Maps supported PBR controls and an embedded base-color image, rejecting unmapped semantics. */
336
+ export async function mapMeshSourceMaterial(
337
+ material: GLTFMaterialPostprocessed | undefined,
338
+ readExternalResource: MeshTilesetSourceOptions['readExternalResource'],
339
+ contentUri: string | undefined,
340
+ signal?: AbortSignal
341
+ ): Promise<MeshTileMaterial | undefined> {
342
+ if (!material) return undefined;
343
+ const pbr = material.pbrMetallicRoughness;
344
+ if (
345
+ Object.entries(material).some(
346
+ ([name, value]) =>
347
+ value !== undefined &&
348
+ ![
349
+ 'id',
350
+ 'name',
351
+ 'pbrMetallicRoughness',
352
+ 'alphaMode',
353
+ 'alphaCutoff',
354
+ 'doubleSided',
355
+ 'emissiveFactor',
356
+ 'extras'
357
+ ].includes(name)
358
+ ) ||
359
+ Object.entries(pbr || {}).some(
360
+ ([name, value]) =>
361
+ value !== undefined &&
362
+ !['baseColorFactor', 'baseColorTexture', 'metallicFactor', 'roughnessFactor'].includes(name)
363
+ ) ||
364
+ material.emissiveFactor?.some(value => value !== 0)
365
+ )
366
+ throw new TileConversionError(
367
+ 'MESH_SOURCE_MATERIAL_UNSUPPORTED',
368
+ 'Only metallic-roughness factors and one embedded base-color image are supported'
369
+ );
370
+ return {
371
+ baseColorFactor: pbr?.baseColorFactor as [number, number, number, number] | undefined,
372
+ ...('baseColorTexture' in (pbr || {})
373
+ ? {
374
+ baseColorTexture: await resolveMeshSourceTexture(
375
+ pbr!.baseColorTexture,
376
+ readExternalResource,
377
+ contentUri,
378
+ signal
379
+ )
380
+ }
381
+ : {}),
382
+ metallicFactor: pbr?.metallicFactor,
383
+ roughnessFactor: pbr?.roughnessFactor,
384
+ alphaMode: material.alphaMode as MeshTileMaterial['alphaMode'],
385
+ ...(material.alphaMode === 'MASK' ? {alphaCutoff: material.alphaCutoff ?? 0.5} : {}),
386
+ doubleSided: material.doubleSided
387
+ };
388
+ }
389
+
390
+ /** Expands a strip without changing vertex storage, feature IDs, source indices, or parity. */
391
+ function createTriangleIndices(
392
+ primitive: GLTFMeshPrimitivePostprocessed,
393
+ vertexCount: number
394
+ ): MeshAttribute | undefined {
395
+ const attribute = primitive.indices
396
+ ? {
397
+ value: primitive.indices.value as MeshAttribute['value'],
398
+ size: primitive.indices.components,
399
+ normalized: primitive.indices.normalized
400
+ }
401
+ : undefined;
402
+ if (primitive.mode !== 5) return attribute;
403
+ const sourceIndices = validateMeshIndices(attribute, vertexCount, 5);
404
+ const indexCount = sourceIndices?.length ?? vertexCount;
405
+ if (!Number.isSafeInteger(indexCount) || indexCount < 3) {
406
+ throw new TileConversionError(
407
+ 'MESH_TRIANGLE_COUNT_INVALID',
408
+ 'Triangle strips require at least three vertices'
409
+ );
410
+ }
411
+ const IndexArray = vertexCount < 65536 ? Uint16Array : Uint32Array;
412
+ const indices = new IndexArray((indexCount - 2) * 3);
413
+ for (let triangle = 0; triangle < indexCount - 2; triangle++) {
414
+ // Match the glTF strip equation, including degenerate connectors; they advance parity too.
415
+ const second = triangle + 1 + (triangle % 2);
416
+ const third = triangle + 2 - (triangle % 2);
417
+ indices[triangle * 3] = sourceIndices?.[triangle] ?? triangle;
418
+ indices[triangle * 3 + 1] = sourceIndices?.[second] ?? second;
419
+ indices[triangle * 3 + 2] = sourceIndices?.[third] ?? third;
420
+ }
421
+ return {value: indices, size: 1};
422
+ }
@@ -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
+ }