@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,234 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {
6
+ createBoundedMemoryTileConversionSink,
7
+ TileConversionError
8
+ } from '@loaders.gl/tile-converter/v5/core';
9
+ import type {
10
+ BrowserTileConversionFile,
11
+ TileConversionSink
12
+ } from '@loaders.gl/tile-converter/v5/core';
13
+ import type {EncodedPointCloudSourceTile} from './point-cloud-source-encoder.js';
14
+
15
+ /** Output limits and geometric error for independent ECEF point tiles. */
16
+ export interface PointCloudTilesetSinkOptions {
17
+ /** Maximum retained PNTS and tileset JSON bytes, excluding peak serialization memory. */
18
+ readonly maxTotalBytes: number;
19
+ /** Maximum independent tile placements retained by the sink. */
20
+ readonly maxTiles: number;
21
+ /** Leaf error in meters, including source sampling error and measured position rounding. */
22
+ readonly geometricError: number;
23
+ }
24
+
25
+ /** Atomic flat 3D Tiles package suitable for the existing 3TZ archive adapter. */
26
+ export interface PointCloudTilesetSink extends TileConversionSink<EncodedPointCloudSourceTile> {
27
+ /** Returns named PNTS files and tileset.json after successful finalization; empty after abort. */
28
+ getFiles(): readonly BrowserTileConversionFile[];
29
+ }
30
+
31
+ /**
32
+ * Packages independent point tiles as a bounded 3D Tiles 1.0 collection with an ADD root.
33
+ * Requires unmodified encoder outputs in declared EPSG:4978 xyz meters and ellipsoidal heights,
34
+ * cartesian positions, zero source origin and no additional placement transform. PNTS RTC_CENTER
35
+ * is already encoded and is applied once to bounds. Geographic/projected/offset source frames
36
+ * must be converted before this sink. This authors a flat partial dataset, not source LOD;
37
+ * callers must select independent, non-overlapping samples and supply their sampling error.
38
+ * Use createTileConversionArchive with format 3tz to package the finalized files.
39
+ * @param options - Retained byte/tile limits and explicit leaf geometric error.
40
+ * @returns A sink exposing the completed package atomically.
41
+ */
42
+ export function createPointCloudTilesetSink(
43
+ options: PointCloudTilesetSinkOptions
44
+ ): PointCloudTilesetSink {
45
+ const {maxTiles, geometricError} = options;
46
+ if (
47
+ !Number.isSafeInteger(maxTiles) ||
48
+ maxTiles < 1 ||
49
+ !Number.isFinite(geometricError) ||
50
+ geometricError < 0
51
+ ) {
52
+ throw new TileConversionError(
53
+ 'POINT_CLOUD_TILESET_OPTIONS_INVALID',
54
+ 'Positive maxTiles and finite nonnegative geometricError are required'
55
+ );
56
+ }
57
+ const memory = createBoundedMemoryTileConversionSink(options);
58
+ const children: {
59
+ boundingVolume: {box: number[]};
60
+ geometricError: number;
61
+ content: {uri: string};
62
+ }[] = [];
63
+ const identifiers = new Set<string>();
64
+ const minimum = [Infinity, Infinity, Infinity];
65
+ const maximum = [-Infinity, -Infinity, -Infinity];
66
+ let state: 'open' | 'writing' | 'closed' = 'open';
67
+ let completed = false;
68
+
69
+ return {
70
+ /** Exposes finalized files without exposing a partial package. */
71
+ getFiles: () => (completed ? memory.getFiles() : []),
72
+ /** Captures one validated placement and encoded PNTS resource. */
73
+ async write(resource, signal) {
74
+ signal?.throwIfAborted();
75
+ if (state !== 'open')
76
+ throw new TileConversionError(
77
+ 'POINT_CLOUD_TILESET_SINK_UNAVAILABLE',
78
+ 'Point sink is busy or closed'
79
+ );
80
+ if (!resource.id || identifiers.has(resource.id) || children.length >= maxTiles) {
81
+ throw new TileConversionError(
82
+ 'POINT_CLOUD_TILESET_PLACEMENT_INVALID',
83
+ 'Placements require unique nonempty identifiers within maxTiles'
84
+ );
85
+ }
86
+ const [lower, upper] = getPointTileBounds(resource, geometricError);
87
+ const resourceId = `points/${children.length}.pnts`;
88
+ state = 'writing';
89
+ await memory.write(
90
+ {resourceId, parts: [resource.pnts], contentType: 'application/octet-stream'},
91
+ signal
92
+ );
93
+ if (state !== 'writing')
94
+ throw new TileConversionError(
95
+ 'POINT_CLOUD_TILESET_SINK_UNAVAILABLE',
96
+ 'Point sink was aborted during writing'
97
+ );
98
+ children.push({
99
+ boundingVolume: {box: createPointBoundsBox(lower, upper)},
100
+ geometricError,
101
+ content: {uri: resourceId}
102
+ });
103
+ identifiers.add(resource.id);
104
+ for (let axis = 0; axis < 3; axis++) {
105
+ minimum[axis] = Math.min(minimum[axis], lower[axis]);
106
+ maximum[axis] = Math.max(maximum[axis], upper[axis]);
107
+ }
108
+ state = 'open';
109
+ },
110
+ /** Serializes the enclosing root after all placements have completed. */
111
+ async finalize(report) {
112
+ if (state !== 'open' || !children.length)
113
+ throw new TileConversionError(
114
+ 'POINT_CLOUD_TILESET_SINK_INCOMPLETE',
115
+ 'Finalization requires successfully written point tiles'
116
+ );
117
+ state = 'writing';
118
+ const rootError = Math.max(
119
+ geometricError,
120
+ Math.hypot(...minimum.map((value, axis) => maximum[axis] - value))
121
+ );
122
+ if (!Number.isFinite(rootError))
123
+ throw new TileConversionError(
124
+ 'POINT_CLOUD_TILESET_BOUNDS_INVALID',
125
+ 'Enclosing bounds must have a finite diagonal'
126
+ );
127
+ const json = JSON.stringify({
128
+ asset: {version: '1.0'},
129
+ geometricError: rootError,
130
+ root: {
131
+ boundingVolume: {box: createPointBoundsBox(minimum, maximum)},
132
+ geometricError: rootError,
133
+ refine: 'ADD',
134
+ children
135
+ }
136
+ });
137
+ await memory.write({
138
+ resourceId: 'tileset.json',
139
+ parts: [json],
140
+ contentType: 'application/json'
141
+ });
142
+ if (state !== 'writing')
143
+ throw new TileConversionError(
144
+ 'POINT_CLOUD_TILESET_SINK_UNAVAILABLE',
145
+ 'Point sink was aborted during finalization'
146
+ );
147
+ await memory.finalize(report);
148
+ if (state !== 'writing')
149
+ throw new TileConversionError(
150
+ 'POINT_CLOUD_TILESET_SINK_UNAVAILABLE',
151
+ 'Point sink was aborted during finalization'
152
+ );
153
+ state = 'closed';
154
+ completed = true;
155
+ },
156
+ /** Clears resource and hierarchy storage on failure or cancellation. */
157
+ async abort(error) {
158
+ state = 'closed';
159
+ completed = false;
160
+ children.length = 0;
161
+ identifiers.clear();
162
+ await memory.abort(error);
163
+ }
164
+ };
165
+ }
166
+
167
+ /** Validates the declared frame and returns bounds reconstructed from trusted encoded positions. */
168
+ function getPointTileBounds(resource: EncodedPointCloudSourceTile, geometricError: number) {
169
+ const reference = resource.spatialReference;
170
+ const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
171
+ if (
172
+ !reference ||
173
+ !['native', 'transformed'].includes(reference.status) ||
174
+ (reference.targetCrs || reference.sourceCrs) !== 'EPSG:4978' ||
175
+ (reference.targetHeightReference === 'native'
176
+ ? reference.heightReference
177
+ : reference.targetHeightReference) !== 'ellipsoidal' ||
178
+ resource.coordinateSystem !== 'cartesian' ||
179
+ resource.cartographicOrigin.length !== 3 ||
180
+ resource.cartographicOrigin.some(value => value !== 0) ||
181
+ (resource.modelMatrix &&
182
+ (resource.modelMatrix.length !== 16 ||
183
+ resource.modelMatrix.some((value, index) => value !== identity[index]))) ||
184
+ (reference.status === 'native' &&
185
+ (reference.coordinateFrame !== 'geocentric' ||
186
+ reference.axisOrder !== 'xyz' ||
187
+ reference.verticalUnitScale !== 1 ||
188
+ reference.units?.some(unit => unit !== 'meter')))
189
+ ) {
190
+ throw new TileConversionError(
191
+ 'POINT_CLOUD_TILESET_FRAME_UNSUPPORTED',
192
+ 'Point tiles require declared EPSG:4978 xyz meters, ellipsoidal heights and no additional source placement'
193
+ );
194
+ }
195
+ if (
196
+ !Number.isFinite(resource.maximumPositionError) ||
197
+ resource.maximumPositionError < 0 ||
198
+ resource.maximumPositionError > geometricError
199
+ ) {
200
+ throw new TileConversionError(
201
+ 'POINT_CLOUD_GEOMETRIC_ERROR_INVALID',
202
+ 'geometricError must include measured position reconstruction error'
203
+ );
204
+ }
205
+ const [minimum, maximum] = resource.localBoundingBox;
206
+ const center = resource.rtcCenter;
207
+ if (
208
+ center.length !== 3 ||
209
+ minimum.length !== 3 ||
210
+ maximum.length !== 3 ||
211
+ [...center, ...minimum, ...maximum].some(value => !Number.isFinite(value)) ||
212
+ minimum.some((value, axis) => value > maximum[axis])
213
+ ) {
214
+ throw new TileConversionError(
215
+ 'POINT_CLOUD_TILESET_BOUNDS_INVALID',
216
+ 'RTC center and encoded xyz bounds must be finite and ordered'
217
+ );
218
+ }
219
+ const lower = minimum.map((value, axis) => value + center[axis]);
220
+ const upper = maximum.map((value, axis) => value + center[axis]);
221
+ if ([...lower, ...upper].some(value => !Number.isFinite(value)))
222
+ throw new TileConversionError(
223
+ 'POINT_CLOUD_TILESET_BOUNDS_INVALID',
224
+ 'Reconstructed ECEF bounds must be finite'
225
+ );
226
+ return [lower, upper] as const;
227
+ }
228
+
229
+ /** Creates an axis-aligned 3D Tiles box in the PNTS position frame without glTF axis rotation. */
230
+ function createPointBoundsBox(minimum: readonly number[], maximum: readonly number[]): number[] {
231
+ const center = minimum.map((value, axis) => value / 2 + maximum[axis] / 2);
232
+ const halfSize = minimum.map((value, axis) => maximum[axis] / 2 - value / 2);
233
+ return [center[0], center[1], center[2], halfSize[0], 0, 0, 0, halfSize[1], 0, 0, 0, halfSize[2]];
234
+ }
@@ -0,0 +1,398 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {Tile3DWriter, TILE3D_TYPE} from '@loaders.gl/3d-tiles';
6
+ import type {
7
+ Mesh,
8
+ MeshArrowTable,
9
+ MeshAttribute,
10
+ MeshAttributeQuantizationTransform
11
+ } from '@loaders.gl/schema';
12
+ import {convertTableToMesh} from '@loaders.gl/schema-utils';
13
+ import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
14
+
15
+ /** Options for encoding one Arrow or mesh point batch as a 3D Tiles point tile. */
16
+ export interface EncodePointCloudTileOptions {
17
+ /** Optional origin subtracted from absolute positions and stored as the PNTS RTC_CENTER. */
18
+ readonly rtcCenter?: readonly [number, number, number];
19
+ /** Maximum Euclidean position reconstruction error in source units; omission permits float32 rounding. */
20
+ readonly maxPositionError?: number;
21
+ /** Optional source-wide RGBA color stored as the PNTS CONSTANT_RGBA property. */
22
+ readonly constantRGBA?: readonly number[];
23
+ /** Optional per-point values indexed by the tile's `BATCH_ID` attribute. */
24
+ readonly batchTableJson?: Readonly<Record<string, readonly (string | number)[]>>;
25
+ }
26
+
27
+ /** Encoded PNTS and measured placement information from its float32 POSITION stream. */
28
+ export interface EncodedPointCloudTile {
29
+ /** Self-contained PNTS payload. */
30
+ readonly pnts: ArrayBuffer;
31
+ /** Actual number of encoded points. */
32
+ readonly pointCount: number;
33
+ /** RTC center added to encoded positions; zero when no RTC center was selected. */
34
+ readonly rtcCenter: readonly [number, number, number];
35
+ /** Axis-aligned bounds of encoded positions before adding the RTC center. */
36
+ readonly localBoundingBox: readonly [readonly number[], readonly number[]];
37
+ /** Maximum Euclidean error after reconstructing absolute positions, in source units. */
38
+ readonly maximumPositionError: number;
39
+ }
40
+
41
+ /**
42
+ * Encodes one mesh or Mesh Arrow point batch as a PNTS resource.
43
+ *
44
+ * The input position field must be named `POSITION` and contain xyz values. `COLOR_0` or `COLOR`
45
+ * is written when present; `constantRGBA` supplies a fallback PNTS `CONSTANT_RGBA` feature-table
46
+ * property. `NORMAL` and `BATCH_ID` are also written when present. Arrow batches should be passed
47
+ * one at a time so callers can bound memory and apply their own tiling and output policy.
48
+ * Unsupported attributes, including a second color attribute, fail instead of being discarded.
49
+ *
50
+ * @param pointBatch - Point data produced by a LAS, COPC, I3S, or other point source.
51
+ * @param options - Optional RTC center and batch-table values.
52
+ * @returns A 3D Tiles PNTS resource.
53
+ */
54
+ export function encodePointCloudTile(
55
+ pointBatch: Mesh | MeshArrowTable,
56
+ options: EncodePointCloudTileOptions = {}
57
+ ): ArrayBuffer {
58
+ return encodePointCloudTileWithMetadata(pointBatch, options).pnts;
59
+ }
60
+
61
+ /**
62
+ * Encodes a point batch and measures its decoded float32 bounds and position rounding.
63
+ * The optional error gate runs before PNTS serialization. Source arrays remain unchanged.
64
+ * @param pointBatch - One mesh or Arrow point batch.
65
+ * @param options - RTC center, precision gate, appearance and batch metadata.
66
+ * @returns PNTS bytes paired with bounds, RTC placement and maximum position error.
67
+ */
68
+ export function encodePointCloudTileWithMetadata(
69
+ pointBatch: Mesh | MeshArrowTable,
70
+ options: EncodePointCloudTileOptions = {}
71
+ ): EncodedPointCloudTile {
72
+ const mesh = 'attributes' in pointBatch ? pointBatch : convertTableToMesh(pointBatch);
73
+ validatePointAttributes(mesh.attributes);
74
+ const positionAttribute = mesh.attributes.POSITION;
75
+ validatePointCloudPosition(positionAttribute);
76
+ const placement = encodePointPositions(positionAttribute, options);
77
+ const {positions} = placement;
78
+ const pointCount = positions.length / 3;
79
+ const colors = getColorAttribute(mesh.attributes.COLOR_0 || mesh.attributes.COLOR, pointCount);
80
+ const constantRGBA = colors ? null : getConstantRgba(options.constantRGBA);
81
+ const normals = getNormalAttribute(mesh.attributes.NORMAL, pointCount);
82
+ const batchIds = getBatchIds(mesh.attributes.BATCH_ID, pointCount);
83
+ const batchTableJson = validateBatchTable(options.batchTableJson, batchIds);
84
+
85
+ const pnts = Tile3DWriter.encodeSync(
86
+ {
87
+ type: TILE3D_TYPE.POINT_CLOUD,
88
+ featureTableJson:
89
+ options.rtcCenter || constantRGBA
90
+ ? {
91
+ ...(options.rtcCenter ? {RTC_CENTER: [...options.rtcCenter]} : {}),
92
+ ...(constantRGBA ? {CONSTANT_RGBA: constantRGBA} : {})
93
+ }
94
+ : undefined,
95
+ batchTableJson,
96
+ attributes: {positions, colors, normals, batchIds}
97
+ },
98
+ {}
99
+ );
100
+ return {
101
+ pnts,
102
+ pointCount,
103
+ rtcCenter: placement.rtcCenter,
104
+ localBoundingBox: placement.localBoundingBox,
105
+ maximumPositionError: placement.maximumPositionError
106
+ };
107
+ }
108
+
109
+ /** Rejects attributes that this PNTS encoder would otherwise silently discard. */
110
+ function validatePointAttributes(attributes: Mesh['attributes']): void {
111
+ const supportedAttributes = new Set([
112
+ 'POSITION',
113
+ 'NORMAL',
114
+ 'BATCH_ID',
115
+ attributes.COLOR_0 ? 'COLOR_0' : 'COLOR'
116
+ ]);
117
+ const unsupportedAttributes = Object.keys(attributes).filter(
118
+ name => !supportedAttributes.has(name)
119
+ );
120
+ if (unsupportedAttributes.length > 0) {
121
+ throw new TileConversionError(
122
+ 'POINT_CLOUD_ATTRIBUTE_UNSUPPORTED',
123
+ `PNTS encoding does not support these point attributes: ${unsupportedAttributes.join(', ')}`
124
+ );
125
+ }
126
+ }
127
+
128
+ /** Validates an optional source-wide PNTS RGBA color. */
129
+ function getConstantRgba(
130
+ constantRGBA?: readonly number[]
131
+ ): [number, number, number, number] | null {
132
+ if (!constantRGBA) {
133
+ return null;
134
+ }
135
+ if (
136
+ constantRGBA.length !== 4 ||
137
+ constantRGBA.some(value => !Number.isInteger(value) || value < 0 || value > 255)
138
+ ) {
139
+ throw new TileConversionError(
140
+ 'POINT_CLOUD_CONSTANT_RGBA_INVALID',
141
+ 'Point cloud constantRGBA must contain four unsigned byte values'
142
+ );
143
+ }
144
+ return [constantRGBA[0], constantRGBA[1], constantRGBA[2], constantRGBA[3]];
145
+ }
146
+
147
+ /** Validates packed point positions and their optional quantization descriptor. */
148
+ function validatePointCloudPosition(
149
+ attribute: MeshAttribute | undefined
150
+ ): asserts attribute is MeshAttribute & {transform?: MeshAttributeQuantizationTransform} {
151
+ if (
152
+ !attribute ||
153
+ attribute.size !== 3 ||
154
+ attribute.byteOffset ||
155
+ attribute.byteStride ||
156
+ attribute.normalized ||
157
+ attribute.componentType
158
+ ) {
159
+ throw new TileConversionError(
160
+ 'POINT_CLOUD_POSITION_REQUIRED',
161
+ 'Point cloud batches must contain packed, non-normalized three-component POSITION values'
162
+ );
163
+ }
164
+
165
+ if (!attribute.value.length || attribute.value.length % 3) {
166
+ throw new TileConversionError(
167
+ 'POINT_CLOUD_POSITION_COUNT_INVALID',
168
+ 'Point cloud POSITION must contain one or more xyz triples'
169
+ );
170
+ }
171
+ const transform = attribute.transform;
172
+ if (
173
+ transform &&
174
+ (transform.type !== 'quantization' ||
175
+ !Number.isInteger(transform.bits) ||
176
+ transform.bits < 1 ||
177
+ transform.bits > 32 ||
178
+ transform.origin.length !== 3 ||
179
+ transform.origin.some(value => !Number.isFinite(value)) ||
180
+ !Number.isFinite(transform.range) ||
181
+ transform.range < 0)
182
+ ) {
183
+ throw new TileConversionError(
184
+ 'POINT_CLOUD_POSITION_TRANSFORM_INVALID',
185
+ 'Position quantization requires finite xyz origin, nonnegative range and 1–32 bits'
186
+ );
187
+ }
188
+ }
189
+
190
+ /** Decodes packed point positions to doubles before applying a spatial operation. */
191
+ export function decodePointCloudPositions(attribute: MeshAttribute | undefined): Float64Array {
192
+ validatePointCloudPosition(attribute);
193
+ const transform = attribute.transform;
194
+ return Float64Array.from(attribute.value, (value, index) =>
195
+ transform
196
+ ? transform.origin[index % 3] + (value / (2 ** transform.bits - 1)) * transform.range
197
+ : value
198
+ );
199
+ }
200
+
201
+ /** Decodes source positions, rebases them and measures the actual float32 output. */
202
+ function encodePointPositions(
203
+ attribute: MeshAttribute & {transform?: MeshAttributeQuantizationTransform},
204
+ options: EncodePointCloudTileOptions
205
+ ) {
206
+ const {rtcCenter = [0, 0, 0], maxPositionError = Infinity} = options;
207
+ if (rtcCenter.length !== 3 || rtcCenter.some(value => !Number.isFinite(value))) {
208
+ throw new TileConversionError(
209
+ 'POINT_CLOUD_RTC_CENTER_INVALID',
210
+ 'Point cloud RTC_CENTER must contain three finite values'
211
+ );
212
+ }
213
+ if (
214
+ options.maxPositionError !== undefined &&
215
+ (!Number.isFinite(maxPositionError) || maxPositionError < 0)
216
+ ) {
217
+ throw new TileConversionError(
218
+ 'POINT_CLOUD_PRECISION_INVALID',
219
+ 'maxPositionError must be finite and nonnegative'
220
+ );
221
+ }
222
+ const values = attribute.value;
223
+ const positions = new Float32Array(values.length);
224
+ const minimum = [Infinity, Infinity, Infinity];
225
+ const maximum = [-Infinity, -Infinity, -Infinity];
226
+ let maximumPositionError = 0;
227
+ const transform = attribute.transform;
228
+ const errors = [0, 0, 0];
229
+ for (let point = 0; point < values.length; point += 3) {
230
+ for (let axis = 0; axis < 3; axis++) {
231
+ const value = values[point + axis];
232
+ const absoluteValue = transform
233
+ ? transform.origin[axis] + (value / (2 ** transform.bits - 1)) * transform.range
234
+ : value;
235
+ const relativeValue = absoluteValue - rtcCenter[axis];
236
+ positions[point + axis] = relativeValue;
237
+ const encodedValue = positions[point + axis];
238
+ const reconstructed = encodedValue + rtcCenter[axis];
239
+ if (
240
+ !Number.isFinite(absoluteValue) ||
241
+ !Number.isFinite(relativeValue) ||
242
+ !Number.isFinite(encodedValue) ||
243
+ !Number.isFinite(reconstructed)
244
+ ) {
245
+ throw new TileConversionError(
246
+ 'POINT_CLOUD_POSITION_INVALID',
247
+ 'Point positions and their float32 reconstruction must be finite'
248
+ );
249
+ }
250
+ errors[axis] = reconstructed - absoluteValue;
251
+ minimum[axis] = Math.min(minimum[axis], encodedValue);
252
+ maximum[axis] = Math.max(maximum[axis], encodedValue);
253
+ }
254
+ maximumPositionError = Math.max(maximumPositionError, Math.hypot(...errors));
255
+ }
256
+ if (!Number.isFinite(maximumPositionError) || maximumPositionError > maxPositionError) {
257
+ throw new TileConversionError(
258
+ 'POINT_CLOUD_PRECISION_EXCEEDED',
259
+ 'Float32 position reconstruction exceeds maxPositionError; select a closer RTC center'
260
+ );
261
+ }
262
+ return {
263
+ positions,
264
+ rtcCenter: [...rtcCenter] as [number, number, number],
265
+ localBoundingBox: [minimum, maximum] as const,
266
+ maximumPositionError
267
+ };
268
+ }
269
+
270
+ /** Converts an optional mesh color attribute into the byte colors accepted by PNTS. */
271
+ function getColorAttribute(
272
+ attribute: MeshAttribute | undefined,
273
+ pointCount: number
274
+ ): {type: number; value: Uint8Array; size: number; normalized: boolean} | null {
275
+ if (!attribute) {
276
+ return null;
277
+ }
278
+ if (attribute.size !== 3 && attribute.size !== 4) {
279
+ throw new TileConversionError(
280
+ 'POINT_CLOUD_COLOR_SIZE_UNSUPPORTED',
281
+ 'Point cloud color attributes must contain three or four components'
282
+ );
283
+ }
284
+ if (attribute.value.length !== pointCount * attribute.size) {
285
+ throw new TileConversionError(
286
+ 'POINT_CLOUD_COLOR_COUNT_MISMATCH',
287
+ 'Point cloud color count does not match the POSITION count'
288
+ );
289
+ }
290
+
291
+ const colors = new Uint8Array(attribute.value.length);
292
+ const maximum = getIntegerColorMaximum(attribute.value);
293
+ for (let index = 0; index < attribute.value.length; index++) {
294
+ const value = attribute.value[index];
295
+ const scaledValue = maximum ? (value / maximum) * 255 : value <= 1 ? value * 255 : value;
296
+ if (!Number.isFinite(scaledValue) || scaledValue < 0 || scaledValue > 255) {
297
+ throw new TileConversionError(
298
+ 'POINT_CLOUD_COLOR_VALUE_INVALID',
299
+ 'Point cloud color components must be finite values from 0 through 255'
300
+ );
301
+ }
302
+ colors[index] = Math.round(scaledValue);
303
+ }
304
+ return {type: 5121, value: colors, size: attribute.size, normalized: false};
305
+ }
306
+
307
+ /** Returns the maximum value represented by an integer color component array. */
308
+ function getIntegerColorMaximum(values: ArrayLike<number>): number | null {
309
+ if (values instanceof Uint8Array || values instanceof Uint8ClampedArray) {
310
+ return 255;
311
+ }
312
+ if (values instanceof Uint16Array) {
313
+ return 65_535;
314
+ }
315
+ if (values instanceof Uint32Array) {
316
+ return 4_294_967_295;
317
+ }
318
+ return null;
319
+ }
320
+
321
+ /** Returns float normals and rejects unsupported component shapes. */
322
+ function getNormalAttribute(
323
+ attribute: MeshAttribute | undefined,
324
+ pointCount: number
325
+ ): {type: number; value: Float32Array; size: number} | null {
326
+ if (!attribute) {
327
+ return null;
328
+ }
329
+ if (attribute.size !== 3 || attribute.value.length !== pointCount * 3) {
330
+ throw new TileConversionError(
331
+ 'POINT_CLOUD_NORMAL_COUNT_MISMATCH',
332
+ 'Point cloud normals must contain one xyz triple per position'
333
+ );
334
+ }
335
+ return {type: 5126, value: Float32Array.from(attribute.value), size: 3};
336
+ }
337
+
338
+ /** Validates optional stable feature IDs for the PNTS uint16 batch-ID field. */
339
+ function getBatchIds(attribute: MeshAttribute | undefined, pointCount: number): Uint16Array | null {
340
+ if (!attribute) {
341
+ return null;
342
+ }
343
+ if (attribute.size !== 1 || attribute.value.length !== pointCount) {
344
+ throw new TileConversionError(
345
+ 'POINT_CLOUD_BATCH_ID_COUNT_MISMATCH',
346
+ 'Point cloud BATCH_ID must contain one value per position'
347
+ );
348
+ }
349
+ const batchIds = new Uint16Array(pointCount);
350
+ for (let index = 0; index < pointCount; index++) {
351
+ const batchId = attribute.value[index];
352
+ if (!Number.isInteger(batchId) || batchId < 0 || batchId > 65_535) {
353
+ throw new TileConversionError(
354
+ 'POINT_CLOUD_BATCH_ID_OUT_OF_RANGE',
355
+ 'Point cloud BATCH_ID values must be unsigned 16-bit integers'
356
+ );
357
+ }
358
+ batchIds[index] = batchId;
359
+ }
360
+ return batchIds;
361
+ }
362
+
363
+ /** Checks that each batch-table property is indexable by the batch IDs used by this tile. */
364
+ function validateBatchTable(
365
+ batchTableJson: EncodePointCloudTileOptions['batchTableJson'],
366
+ batchIds: Uint16Array | null
367
+ ): Record<string, (string | number)[]> | undefined {
368
+ if (!batchTableJson) {
369
+ if (batchIds) {
370
+ throw new TileConversionError(
371
+ 'POINT_CLOUD_BATCH_TABLE_REQUIRED',
372
+ 'BATCH_ID values require matching batchTableJson properties'
373
+ );
374
+ }
375
+ return undefined;
376
+ }
377
+ if (!batchIds) {
378
+ throw new TileConversionError(
379
+ 'POINT_CLOUD_BATCH_ID_REQUIRED',
380
+ 'batchTableJson requires a BATCH_ID attribute'
381
+ );
382
+ }
383
+ let batchLength = 0;
384
+ for (const batchId of batchIds) {
385
+ batchLength = Math.max(batchLength, batchId + 1);
386
+ }
387
+ const normalizedBatchTable: Record<string, (string | number)[]> = {};
388
+ for (const [name, values] of Object.entries(batchTableJson)) {
389
+ if (values.length !== batchLength) {
390
+ throw new TileConversionError(
391
+ 'POINT_CLOUD_BATCH_TABLE_LENGTH_MISMATCH',
392
+ `Batch table property "${name}" must contain ${batchLength} values`
393
+ );
394
+ }
395
+ normalizedBatchTable[name] = [...values];
396
+ }
397
+ return normalizedBatchTable;
398
+ }