@loaders.gl/splats 4.5.0-alpha.1

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 (121) hide show
  1. package/LICENSE +41 -0
  2. package/README.md +3 -0
  3. package/dist/bundled.d.ts +2 -0
  4. package/dist/bundled.d.ts.map +1 -0
  5. package/dist/bundled.js +5 -0
  6. package/dist/bundled.js.map +1 -0
  7. package/dist/dist.dev.js +33741 -0
  8. package/dist/dist.min.js +82 -0
  9. package/dist/index.cjs +1786 -0
  10. package/dist/index.cjs.map +7 -0
  11. package/dist/index.d.ts +10 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +10 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/ksplat-loader-types.d.ts +19 -0
  16. package/dist/ksplat-loader-types.d.ts.map +1 -0
  17. package/dist/ksplat-loader-types.js +20 -0
  18. package/dist/ksplat-loader-types.js.map +1 -0
  19. package/dist/ksplat-loader.d.ts +21 -0
  20. package/dist/ksplat-loader.d.ts.map +1 -0
  21. package/dist/ksplat-loader.js +12 -0
  22. package/dist/ksplat-loader.js.map +1 -0
  23. package/dist/lib/parse-ksplat.d.ts +7 -0
  24. package/dist/lib/parse-ksplat.d.ts.map +1 -0
  25. package/dist/lib/parse-ksplat.js +312 -0
  26. package/dist/lib/parse-ksplat.js.map +1 -0
  27. package/dist/lib/parse-rad-chunk.d.ts +19 -0
  28. package/dist/lib/parse-rad-chunk.d.ts.map +1 -0
  29. package/dist/lib/parse-rad-chunk.js +514 -0
  30. package/dist/lib/parse-rad-chunk.js.map +1 -0
  31. package/dist/lib/parse-rad.d.ts +136 -0
  32. package/dist/lib/parse-rad.d.ts.map +1 -0
  33. package/dist/lib/parse-rad.js +110 -0
  34. package/dist/lib/parse-rad.js.map +1 -0
  35. package/dist/lib/parse-splat.d.ts +7 -0
  36. package/dist/lib/parse-splat.d.ts.map +1 -0
  37. package/dist/lib/parse-splat.js +58 -0
  38. package/dist/lib/parse-splat.js.map +1 -0
  39. package/dist/lib/parse-spz.d.ts +7 -0
  40. package/dist/lib/parse-spz.d.ts.map +1 -0
  41. package/dist/lib/parse-spz.js +268 -0
  42. package/dist/lib/parse-spz.js.map +1 -0
  43. package/dist/lib/splat-utils.d.ts +12 -0
  44. package/dist/lib/splat-utils.d.ts.map +1 -0
  45. package/dist/lib/splat-utils.js +38 -0
  46. package/dist/lib/splat-utils.js.map +1 -0
  47. package/dist/lib/splats-arrow-table.d.ts +5 -0
  48. package/dist/lib/splats-arrow-table.d.ts.map +1 -0
  49. package/dist/lib/splats-arrow-table.js +133 -0
  50. package/dist/lib/splats-arrow-table.js.map +1 -0
  51. package/dist/rad-loader-types.d.ts +16 -0
  52. package/dist/rad-loader-types.d.ts.map +1 -0
  53. package/dist/rad-loader-types.js +16 -0
  54. package/dist/rad-loader-types.js.map +1 -0
  55. package/dist/rad-loader.d.ts +20 -0
  56. package/dist/rad-loader.d.ts.map +1 -0
  57. package/dist/rad-loader.js +14 -0
  58. package/dist/rad-loader.js.map +1 -0
  59. package/dist/rad-metadata.schema.json +133 -0
  60. package/dist/rad-source-loader.d.ts +112 -0
  61. package/dist/rad-source-loader.d.ts.map +1 -0
  62. package/dist/rad-source-loader.js +329 -0
  63. package/dist/rad-source-loader.js.map +1 -0
  64. package/dist/rad-zod-schema.cjs +81 -0
  65. package/dist/rad-zod-schema.cjs.map +7 -0
  66. package/dist/rad-zod-schema.d.ts +88 -0
  67. package/dist/rad-zod-schema.d.ts.map +1 -0
  68. package/dist/rad-zod-schema.js +70 -0
  69. package/dist/rad-zod-schema.js.map +1 -0
  70. package/dist/radc-metadata.schema.json +112 -0
  71. package/dist/splat-loader-types.d.ts +19 -0
  72. package/dist/splat-loader-types.d.ts.map +1 -0
  73. package/dist/splat-loader-types.js +20 -0
  74. package/dist/splat-loader-types.js.map +1 -0
  75. package/dist/splat-loader.d.ts +21 -0
  76. package/dist/splat-loader.d.ts.map +1 -0
  77. package/dist/splat-loader.js +12 -0
  78. package/dist/splat-loader.js.map +1 -0
  79. package/dist/splats-format.d.ts +39 -0
  80. package/dist/splats-format.d.ts.map +1 -0
  81. package/dist/splats-format.js +42 -0
  82. package/dist/splats-format.js.map +1 -0
  83. package/dist/spz-loader-types.d.ts +20 -0
  84. package/dist/spz-loader-types.d.ts.map +1 -0
  85. package/dist/spz-loader-types.js +20 -0
  86. package/dist/spz-loader-types.js.map +1 -0
  87. package/dist/spz-loader.d.ts +22 -0
  88. package/dist/spz-loader.d.ts.map +1 -0
  89. package/dist/spz-loader.js +11 -0
  90. package/dist/spz-loader.js.map +1 -0
  91. package/dist/types.d.ts +34 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +5 -0
  94. package/dist/types.js.map +1 -0
  95. package/dist/unbundled.d.ts +2 -0
  96. package/dist/unbundled.d.ts.map +1 -0
  97. package/dist/unbundled.js +5 -0
  98. package/dist/unbundled.js.map +1 -0
  99. package/package.json +94 -0
  100. package/src/bundled.ts +5 -0
  101. package/src/index.ts +28 -0
  102. package/src/ksplat-loader-types.ts +25 -0
  103. package/src/ksplat-loader.ts +16 -0
  104. package/src/lib/parse-ksplat.ts +540 -0
  105. package/src/lib/parse-rad-chunk.ts +721 -0
  106. package/src/lib/parse-rad.ts +318 -0
  107. package/src/lib/parse-splat.ts +77 -0
  108. package/src/lib/parse-spz.ts +368 -0
  109. package/src/lib/splat-utils.ts +49 -0
  110. package/src/lib/splats-arrow-table.ts +190 -0
  111. package/src/rad-loader-types.ts +20 -0
  112. package/src/rad-loader.ts +22 -0
  113. package/src/rad-source-loader.ts +491 -0
  114. package/src/rad-zod-schema.ts +83 -0
  115. package/src/splat-loader-types.ts +25 -0
  116. package/src/splat-loader.ts +16 -0
  117. package/src/splats-format.ts +47 -0
  118. package/src/spz-loader-types.ts +25 -0
  119. package/src/spz-loader.ts +16 -0
  120. package/src/types.ts +39 -0
  121. package/src/unbundled.ts +5 -0
@@ -0,0 +1,491 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {DataSourceOptions} from '@loaders.gl/loader-utils';
6
+ import {DataSource} from '@loaders.gl/loader-utils';
7
+ import type {MeshArrowTable} from '@loaders.gl/schema';
8
+ import {RADFormat} from './splats-format';
9
+ import {
10
+ parseRADChunkHeader,
11
+ tryParseRADHeader,
12
+ type RADChunkMetadata,
13
+ type RADChunkRange,
14
+ type RADMetadata
15
+ } from './lib/parse-rad';
16
+ import {
17
+ parseRADChunk,
18
+ parseRADChunkToGaussianSplats,
19
+ type RADChunkDecodeOptions
20
+ } from './lib/parse-rad-chunk';
21
+ import {makeGaussianSplatsArrowTable} from './lib/splats-arrow-table';
22
+ import type {GaussianSplats} from './types';
23
+
24
+ const DEFAULT_RAD_HEADER_BYTE_LENGTHS = [64 * 1024, 256 * 1024, 1024 * 1024];
25
+ const DEFAULT_RAD_MAX_CONCURRENT_CHUNK_REQUESTS = 4;
26
+
27
+ // __VERSION__ is injected by babel-plugin-version-inline
28
+ // @ts-ignore TS2304: Cannot find name '__VERSION__'.
29
+ const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';
30
+
31
+ /** Options for creating a Spark RAD source. */
32
+ export type RADSourceLoaderOptions = DataSourceOptions & {
33
+ rad?: {
34
+ /** Optional request headers forwarded to RAD and RADC fetches. */
35
+ headers?: Record<string, string>;
36
+ /** Whether remote RAD and RADC fetches should include credentials. */
37
+ withCredentials?: boolean;
38
+ /** Header byte lengths to try while probing remote RAD metadata. */
39
+ headerByteLengths?: number[];
40
+ };
41
+ };
42
+
43
+ /** Optional parameters for fetching RAD chunk bytes or metadata. */
44
+ export type RADChunkRequestOptions = {
45
+ /** Abort signal forwarded to the underlying fetch. */
46
+ signal?: AbortSignal;
47
+ } & RADChunkDecodeOptions;
48
+
49
+ /** Options for iterating decoded RAD chunk tables. */
50
+ export type RADChunkTableIteratorOptions = RADChunkRequestOptions & {
51
+ /** First chunk index to decode. */
52
+ startChunkIndex?: number;
53
+ /** Maximum number of chunks to decode. */
54
+ maxChunks?: number;
55
+ /** Maximum number of splats to decode before stopping. */
56
+ maxSplats?: number;
57
+ /** Maximum number of RAD chunks to fetch and decode at once when the iterator must inspect a chunk window before yielding. */
58
+ maxConcurrentChunkRequests?: number;
59
+ /** Whether loaded child splats should replace parent LoD splats in returned tables. */
60
+ pruneLoadedLoDParents?: boolean;
61
+ };
62
+
63
+ /** Source factory for Spark `.rad` paged LoD Gaussian splat containers. */
64
+ export const RADSourceLoader = {
65
+ dataType: null as unknown as RADSource,
66
+ batchType: null as never,
67
+ ...RADFormat,
68
+ name: 'RADSourceLoader',
69
+ version: VERSION,
70
+ type: 'rad',
71
+ fromUrl: true,
72
+ fromBlob: true,
73
+
74
+ options: {
75
+ rad: {
76
+ headers: undefined!,
77
+ withCredentials: false,
78
+ headerByteLengths: undefined!
79
+ }
80
+ },
81
+
82
+ defaultOptions: {
83
+ rad: {
84
+ headers: undefined!,
85
+ withCredentials: false,
86
+ headerByteLengths: undefined!
87
+ }
88
+ },
89
+
90
+ testURL: (url: string): boolean => /\.rad(?:$|[?#])/i.test(url),
91
+ createDataSource: (data: string | Blob, options: RADSourceLoaderOptions): RADSource =>
92
+ new RADSource(data, options)
93
+ } as const;
94
+
95
+ /** Runtime source for RAD metadata and chunk-byte access. */
96
+ export class RADSource extends DataSource<string | Blob, RADSourceLoaderOptions> {
97
+ /** Shared metadata promise exposed for source-style integrations. */
98
+ metadata: Promise<RADMetadata>;
99
+
100
+ /** Lazily initialized RAD header promise. */
101
+ private _metadataPromise: Promise<RADMetadata> | null = null;
102
+ /** Lazily initialized per-chunk metadata promises. */
103
+ private _chunkMetadataPromises: Map<number, Promise<RADChunkMetadata>> = new Map();
104
+
105
+ constructor(data: string | Blob, options: RADSourceLoaderOptions) {
106
+ super(data, options, RADSourceLoader.defaultOptions);
107
+ this.metadata = this.getMetadata();
108
+ }
109
+
110
+ /** Ensures the RAD header metadata has been loaded. */
111
+ async initialize(): Promise<void> {
112
+ await this.getMetadata();
113
+ }
114
+
115
+ /** Returns parsed Spark RAD top-level metadata. */
116
+ async getMetadata(): Promise<RADMetadata> {
117
+ if (!this._metadataPromise) {
118
+ this._metadataPromise = this._loadMetadata();
119
+ }
120
+ return await this._metadataPromise;
121
+ }
122
+
123
+ /** Returns the number of chunks declared by the RAD metadata. */
124
+ async getChunkCount(): Promise<number> {
125
+ const metadata = await this.getMetadata();
126
+ return metadata.chunks.length;
127
+ }
128
+
129
+ /** Returns the URL used for a chunk, or `null` for Blob-backed inline chunks. */
130
+ async getChunkUrl(chunkIndex: number): Promise<string | null> {
131
+ const metadata = await this.getMetadata();
132
+ const chunk = this._getChunkRange(metadata, chunkIndex);
133
+ if (chunk.filename) {
134
+ return resolveRADChunkUrl(this.url, chunk.filename);
135
+ }
136
+ return typeof this.data === 'string' ? this.url : null;
137
+ }
138
+
139
+ /** Fetches one raw RADC chunk by index. */
140
+ async getChunk(chunkIndex: number, options: RADChunkRequestOptions = {}): Promise<ArrayBuffer> {
141
+ const metadata = await this.getMetadata();
142
+ const chunk = this._getChunkRange(metadata, chunkIndex);
143
+
144
+ if (chunk.filename) {
145
+ if (typeof this.data !== 'string') {
146
+ throw new Error('RADSource: sidecar RADC chunks require a URL-backed RAD source.');
147
+ }
148
+ return await this._fetchArrayBuffer(resolveRADChunkUrl(this.url, chunk.filename), undefined, {
149
+ signal: options.signal
150
+ });
151
+ }
152
+
153
+ if (chunk.bytes === 0) {
154
+ return new ArrayBuffer(0);
155
+ }
156
+
157
+ const byteOffset = metadata.chunksByteOffset + chunk.offset;
158
+ if (typeof this.data !== 'string') {
159
+ return await this.data.slice(byteOffset, byteOffset + chunk.bytes).arrayBuffer();
160
+ }
161
+
162
+ return await this._fetchArrayBuffer(
163
+ this.url,
164
+ {offset: byteOffset, bytes: chunk.bytes},
165
+ {signal: options.signal}
166
+ );
167
+ }
168
+
169
+ /** Fetches and parses metadata for one RADC chunk. */
170
+ async getChunkMetadata(
171
+ chunkIndex: number,
172
+ options: RADChunkRequestOptions = {}
173
+ ): Promise<RADChunkMetadata> {
174
+ let chunkMetadataPromise = this._chunkMetadataPromises.get(chunkIndex);
175
+ if (!chunkMetadataPromise) {
176
+ chunkMetadataPromise = this.getChunk(chunkIndex, options).then((chunk) =>
177
+ parseRADChunkHeader(chunk)
178
+ );
179
+ this._chunkMetadataPromises.set(chunkIndex, chunkMetadataPromise);
180
+ }
181
+ return await chunkMetadataPromise;
182
+ }
183
+
184
+ /** Fetches and decodes one RADC chunk into raw Gaussian splat arrays. */
185
+ async getChunkSplats(
186
+ chunkIndex: number,
187
+ options: RADChunkRequestOptions = {}
188
+ ): Promise<GaussianSplats> {
189
+ const metadata = await this.getMetadata();
190
+ return parseRADChunkToGaussianSplats(
191
+ await this.getChunk(chunkIndex, options),
192
+ this._getChunkDecodeOptions(metadata, options)
193
+ );
194
+ }
195
+
196
+ /** Fetches and decodes one RADC chunk into a Mesh Arrow table. */
197
+ async getChunkTable(
198
+ chunkIndex: number,
199
+ options: RADChunkRequestOptions = {}
200
+ ): Promise<MeshArrowTable> {
201
+ const metadata = await this.getMetadata();
202
+ return parseRADChunk(
203
+ await this.getChunk(chunkIndex, options),
204
+ this._getChunkDecodeOptions(metadata, options)
205
+ );
206
+ }
207
+
208
+ /** Iterates decoded RADC chunks as Mesh Arrow tables. */
209
+ async *getChunkTables(options: RADChunkTableIteratorOptions = {}): AsyncIterable<MeshArrowTable> {
210
+ const metadata = await this.getMetadata();
211
+ const startChunkIndex = options.startChunkIndex ?? 0;
212
+ const maxChunks = options.maxChunks ?? Number.POSITIVE_INFINITY;
213
+ const maxSplats = options.maxSplats ?? Number.POSITIVE_INFINITY;
214
+ let chunkCount = 0;
215
+ let splatCount = 0;
216
+
217
+ if (options.pruneLoadedLoDParents) {
218
+ const chunkIndices = getRADChunkTableIteratorIndices(
219
+ metadata,
220
+ startChunkIndex,
221
+ maxChunks,
222
+ maxSplats
223
+ );
224
+ const splatChunks = await this._getChunkSplatsConcurrently(chunkIndices, options);
225
+ let loadedGlobalSplatEnd = 0;
226
+ for (const splats of splatChunks) {
227
+ chunkCount++;
228
+ splatCount += splats.splatCount;
229
+ const base = getRADChunkSplatBase(splats);
230
+ loadedGlobalSplatEnd = Math.max(loadedGlobalSplatEnd, base + splats.splatCount);
231
+ }
232
+ for (const splats of splatChunks) {
233
+ yield makeGaussianSplatsArrowTable(pruneLoadedLoDParents(splats, loadedGlobalSplatEnd));
234
+ }
235
+ return;
236
+ }
237
+
238
+ for (
239
+ let chunkIndex = startChunkIndex;
240
+ chunkIndex < metadata.chunks.length && chunkCount < maxChunks && splatCount < maxSplats;
241
+ chunkIndex++
242
+ ) {
243
+ const table = await this.getChunkTable(chunkIndex, options);
244
+ chunkCount++;
245
+ splatCount += table.data.numRows;
246
+ yield table;
247
+ }
248
+ }
249
+
250
+ /** Fetches and decodes RAD chunks with bounded concurrency while preserving chunk order. */
251
+ private async _getChunkSplatsConcurrently(
252
+ chunkIndices: number[],
253
+ options: RADChunkTableIteratorOptions
254
+ ): Promise<GaussianSplats[]> {
255
+ const splatChunks = new Array<GaussianSplats>(chunkIndices.length);
256
+ const maxConcurrentChunkRequests = getRADMaxConcurrentChunkRequests(
257
+ options.maxConcurrentChunkRequests
258
+ );
259
+ let nextResultIndex = 0;
260
+
261
+ async function loadNextChunk(source: RADSource): Promise<void> {
262
+ while (nextResultIndex < chunkIndices.length) {
263
+ const resultIndex = nextResultIndex++;
264
+ const chunkIndex = chunkIndices[resultIndex];
265
+ splatChunks[resultIndex] = await source.getChunkSplats(chunkIndex, options);
266
+ }
267
+ }
268
+
269
+ const workerCount = Math.min(maxConcurrentChunkRequests, chunkIndices.length);
270
+ await Promise.all(Array.from({length: workerCount}, () => loadNextChunk(this)));
271
+ return splatChunks;
272
+ }
273
+
274
+ /** Adds source-level RAD decode metadata to chunk decode options. */
275
+ private _getChunkDecodeOptions(
276
+ metadata: RADMetadata,
277
+ options: RADChunkRequestOptions
278
+ ): RADChunkRequestOptions {
279
+ return {
280
+ ...options,
281
+ radChunk: {
282
+ splatEncoding: metadata.splatEncoding,
283
+ ...options.radChunk
284
+ }
285
+ };
286
+ }
287
+
288
+ /** Loads and parses the top-level RAD metadata. */
289
+ private async _loadMetadata(): Promise<RADMetadata> {
290
+ const headerByteLengths = this.options.rad?.headerByteLengths?.length
291
+ ? this.options.rad.headerByteLengths
292
+ : DEFAULT_RAD_HEADER_BYTE_LENGTHS;
293
+
294
+ if (typeof this.data !== 'string') {
295
+ const byteLength = Math.min(this.data.size, Math.max(...headerByteLengths));
296
+ const metadata = tryParseRADHeader(await this.data.slice(0, byteLength).arrayBuffer());
297
+ if (metadata) {
298
+ return metadata;
299
+ }
300
+ throw new Error('RADSource: Blob does not contain a complete RAD metadata header.');
301
+ }
302
+
303
+ for (const byteLength of headerByteLengths) {
304
+ const metadata = tryParseRADHeader(
305
+ await this._fetchArrayBuffer(this.url, {offset: 0, bytes: byteLength})
306
+ );
307
+ if (metadata) {
308
+ return metadata;
309
+ }
310
+ }
311
+
312
+ throw new Error('RADSource: failed to load a complete RAD metadata header.');
313
+ }
314
+
315
+ /** Returns a validated chunk table entry. */
316
+ private _getChunkRange(metadata: RADMetadata, chunkIndex: number): RADChunkRange {
317
+ if (!Number.isInteger(chunkIndex) || chunkIndex < 0 || chunkIndex >= metadata.chunks.length) {
318
+ throw new Error(
319
+ `RADSource: chunk index ${chunkIndex} is outside 0-${metadata.chunks.length - 1}.`
320
+ );
321
+ }
322
+ return metadata.chunks[chunkIndex];
323
+ }
324
+
325
+ /** Fetches a full or ranged ArrayBuffer while tolerating non-206 range fallbacks. */
326
+ private async _fetchArrayBuffer(
327
+ url: string,
328
+ range?: {offset: number; bytes: number},
329
+ options: RADChunkRequestOptions = {}
330
+ ): Promise<ArrayBuffer> {
331
+ const response = await this._fetchResponse(url, this._getRequestInit(range, options));
332
+ if (!response.ok) {
333
+ throw new Error(
334
+ `RADSource: failed to fetch ${url}: ${response.status} ${response.statusText}`
335
+ );
336
+ }
337
+
338
+ const arrayBuffer = await response.arrayBuffer();
339
+ if (range && response.status !== 206 && arrayBuffer.byteLength >= range.offset + range.bytes) {
340
+ return arrayBuffer.slice(range.offset, range.offset + range.bytes);
341
+ }
342
+ if (range && arrayBuffer.byteLength > range.bytes) {
343
+ return arrayBuffer.slice(0, range.bytes);
344
+ }
345
+ return arrayBuffer;
346
+ }
347
+
348
+ /** Fetches through custom source fetch hooks or the injected core fetchFile helper. */
349
+ private async _fetchResponse(url: string, requestInit: RequestInit): Promise<Response> {
350
+ return await this.fetch(url, requestInit);
351
+ }
352
+
353
+ /** Builds request options for RAD fetches. */
354
+ private _getRequestInit(
355
+ range?: {offset: number; bytes: number},
356
+ options: RADChunkRequestOptions = {}
357
+ ): RequestInit {
358
+ const headers = new Headers(this.options.rad?.headers);
359
+ if (range && range.bytes > 0) {
360
+ headers.set('Range', `bytes=${range.offset}-${range.offset + range.bytes - 1}`);
361
+ }
362
+
363
+ return {
364
+ headers,
365
+ credentials: this.options.rad?.withCredentials ? 'include' : 'same-origin',
366
+ signal: options.signal
367
+ };
368
+ }
369
+ }
370
+
371
+ /** Returns the RAD chunk indices requested by a chunk-table iterator. */
372
+ function getRADChunkTableIteratorIndices(
373
+ metadata: RADMetadata,
374
+ startChunkIndex: number,
375
+ maxChunks: number,
376
+ maxSplats: number
377
+ ): number[] {
378
+ const chunkIndices: number[] = [];
379
+ let estimatedSplatCount = 0;
380
+ for (
381
+ let chunkIndex = startChunkIndex;
382
+ chunkIndex < metadata.chunks.length &&
383
+ chunkIndices.length < maxChunks &&
384
+ estimatedSplatCount < maxSplats;
385
+ chunkIndex++
386
+ ) {
387
+ const chunk = metadata.chunks[chunkIndex];
388
+ chunkIndices.push(chunkIndex);
389
+ estimatedSplatCount += chunk?.count ?? metadata.chunkSize ?? 0;
390
+ }
391
+ return chunkIndices;
392
+ }
393
+
394
+ /** Returns a positive integer chunk request concurrency limit. */
395
+ function getRADMaxConcurrentChunkRequests(maxConcurrentChunkRequests?: number): number {
396
+ return Number.isFinite(maxConcurrentChunkRequests) && maxConcurrentChunkRequests! > 0
397
+ ? Math.floor(maxConcurrentChunkRequests!)
398
+ : DEFAULT_RAD_MAX_CONCURRENT_CHUNK_REQUESTS;
399
+ }
400
+
401
+ /** Returns the first global splat index represented by decoded RAD chunk data. */
402
+ function getRADChunkSplatBase(splats: GaussianSplats): number {
403
+ return typeof splats.loaderData?.base === 'number' ? splats.loaderData.base : 0;
404
+ }
405
+
406
+ /** Remove parent LoD rows when their children are present in the loaded chunk range. */
407
+ function pruneLoadedLoDParents(
408
+ splats: GaussianSplats,
409
+ loadedGlobalSplatEnd: number
410
+ ): GaussianSplats {
411
+ const childCounts = splats.loaderData?.childCounts;
412
+ const childStarts = splats.loaderData?.childStarts;
413
+ if (!(childCounts instanceof Uint16Array) || !(childStarts instanceof Uint32Array)) {
414
+ return splats;
415
+ }
416
+
417
+ const keepRows = new Uint32Array(splats.splatCount);
418
+ let keepCount = 0;
419
+ for (let rowIndex = 0; rowIndex < splats.splatCount; rowIndex++) {
420
+ const childCount = childCounts[rowIndex];
421
+ const childStart = childStarts[rowIndex];
422
+ if (childCount > 0 && childStart < loadedGlobalSplatEnd) {
423
+ continue;
424
+ }
425
+ keepRows[keepCount++] = rowIndex;
426
+ }
427
+ if (keepCount === splats.splatCount) {
428
+ return splats;
429
+ }
430
+
431
+ const keptRows = keepRows.subarray(0, keepCount);
432
+ const sphericalHarmonicsComponentCount = splats.sphericalHarmonicsComponentCount ?? 0;
433
+ return {
434
+ ...splats,
435
+ splatCount: keepCount,
436
+ positions: copyInterleavedRows(splats.positions, 3, keptRows),
437
+ scales: copyInterleavedRows(splats.scales, 3, keptRows),
438
+ rotations: copyInterleavedRows(splats.rotations, 4, keptRows),
439
+ colors: copyInterleavedRows(splats.colors, 3, keptRows),
440
+ sphericalHarmonicDcs: splats.sphericalHarmonicDcs
441
+ ? copyInterleavedRows(splats.sphericalHarmonicDcs, 3, keptRows)
442
+ : undefined,
443
+ opacities: copyInterleavedRows(splats.opacities, 1, keptRows),
444
+ sphericalHarmonics:
445
+ splats.sphericalHarmonics && sphericalHarmonicsComponentCount
446
+ ? copyInterleavedRows(splats.sphericalHarmonics, sphericalHarmonicsComponentCount, keptRows)
447
+ : undefined,
448
+ loaderData: {
449
+ ...splats.loaderData,
450
+ count: keepCount,
451
+ childCounts: copyInterleavedRows(childCounts, 1, keptRows),
452
+ childStarts: copyInterleavedRows(childStarts, 1, keptRows)
453
+ }
454
+ };
455
+ }
456
+
457
+ /** Copy selected rows from an interleaved typed array. */
458
+ function copyInterleavedRows<T extends Float32Array | Uint8Array | Uint16Array | Uint32Array>(
459
+ values: T,
460
+ itemSize: number,
461
+ rows: Uint32Array
462
+ ): T {
463
+ const copiedValues = new (values.constructor as {new (length: number): T})(
464
+ rows.length * itemSize
465
+ );
466
+ for (let outputRowIndex = 0; outputRowIndex < rows.length; outputRowIndex++) {
467
+ const inputOffset = rows[outputRowIndex] * itemSize;
468
+ const outputOffset = outputRowIndex * itemSize;
469
+ copiedValues.set(values.subarray(inputOffset, inputOffset + itemSize), outputOffset);
470
+ }
471
+ return copiedValues;
472
+ }
473
+
474
+ /** Resolves a sidecar RADC chunk filename relative to its parent RAD URL. */
475
+ export function resolveRADChunkUrl(rootUrl: string, chunkFilename: string): string {
476
+ if (/^[a-z][a-z0-9+.-]*:/i.test(chunkFilename) || chunkFilename.startsWith('/')) {
477
+ return chunkFilename;
478
+ }
479
+ if (!rootUrl) {
480
+ throw new Error('RADSource: cannot resolve a sidecar RADC chunk without a RAD URL.');
481
+ }
482
+
483
+ try {
484
+ return new URL(chunkFilename, rootUrl).toString();
485
+ } catch {
486
+ const [rootPath, suffix = ''] = rootUrl.split(/([?#].*)/, 2);
487
+ const lastSlashIndex = Math.max(rootPath.lastIndexOf('/'), rootPath.lastIndexOf('\\'));
488
+ const rootDirectory = lastSlashIndex >= 0 ? rootPath.slice(0, lastSlashIndex + 1) : '';
489
+ return `${rootDirectory}${chunkFilename}${suffix}`;
490
+ }
491
+ }
@@ -0,0 +1,83 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {z} from 'zod';
6
+ import type {
7
+ RADChunkMetadataJSON,
8
+ RADChunkProperty,
9
+ RADChunkRange,
10
+ RADMetadataJSON,
11
+ RADSplatEncoding
12
+ } from './lib/parse-rad';
13
+
14
+ const nonNegativeSafeIntegerSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
15
+
16
+ /** Zod schema for Spark RAD splat quantization and shader decode ranges. */
17
+ export const RADSplatEncodingSchema = z
18
+ .object({
19
+ rgbMin: z.number().finite().optional(),
20
+ rgbMax: z.number().finite().optional(),
21
+ lnScaleMin: z.number().finite().optional(),
22
+ lnScaleMax: z.number().finite().optional(),
23
+ sh1Max: z.number().finite().optional(),
24
+ sh2Max: z.number().finite().optional(),
25
+ sh3Max: z.number().finite().optional(),
26
+ lodOpacity: z.boolean().optional()
27
+ })
28
+ .passthrough() satisfies z.ZodType<RADSplatEncoding>;
29
+
30
+ /** Zod schema for one chunk-table entry in top-level RAD metadata. */
31
+ export const RADChunkRangeSchema = z
32
+ .object({
33
+ offset: nonNegativeSafeIntegerSchema,
34
+ bytes: nonNegativeSafeIntegerSchema,
35
+ base: nonNegativeSafeIntegerSchema.optional(),
36
+ count: nonNegativeSafeIntegerSchema.optional(),
37
+ filename: z.string().optional()
38
+ })
39
+ .passthrough() satisfies z.ZodType<RADChunkRange>;
40
+
41
+ /** Zod schema for raw JSON metadata stored in a Spark RAD header. */
42
+ export const RADMetadataJSONSchema = z
43
+ .object({
44
+ version: z.literal(1),
45
+ type: z.literal('gsplat'),
46
+ count: nonNegativeSafeIntegerSchema,
47
+ maxSh: nonNegativeSafeIntegerSchema.optional(),
48
+ lodTree: z.boolean().optional(),
49
+ chunkSize: nonNegativeSafeIntegerSchema.optional(),
50
+ allChunkBytes: nonNegativeSafeIntegerSchema.optional(),
51
+ chunks: z.array(RADChunkRangeSchema),
52
+ splatEncoding: RADSplatEncodingSchema.optional(),
53
+ shCodeCount: nonNegativeSafeIntegerSchema.optional(),
54
+ comment: z.string().optional()
55
+ })
56
+ .passthrough() satisfies z.ZodType<RADMetadataJSON>;
57
+
58
+ /** Zod schema for one property-table entry in RADC metadata. */
59
+ export const RADChunkPropertySchema = z
60
+ .object({
61
+ offset: nonNegativeSafeIntegerSchema,
62
+ bytes: nonNegativeSafeIntegerSchema,
63
+ property: z.string(),
64
+ encoding: z.string(),
65
+ compression: z.string().optional(),
66
+ min: z.number().finite().optional(),
67
+ max: z.number().finite().optional()
68
+ })
69
+ .passthrough() satisfies z.ZodType<RADChunkProperty>;
70
+
71
+ /** Zod schema for raw JSON metadata stored in a Spark RADC chunk header. */
72
+ export const RADChunkMetadataJSONSchema = z
73
+ .object({
74
+ version: z.literal(1),
75
+ base: nonNegativeSafeIntegerSchema,
76
+ count: nonNegativeSafeIntegerSchema,
77
+ payloadBytes: nonNegativeSafeIntegerSchema,
78
+ maxSh: nonNegativeSafeIntegerSchema.optional(),
79
+ lodTree: z.boolean().optional(),
80
+ splatEncoding: RADSplatEncodingSchema.optional(),
81
+ properties: z.array(RADChunkPropertySchema)
82
+ })
83
+ .passthrough() satisfies z.ZodType<RADChunkMetadataJSON>;
@@ -0,0 +1,25 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {Loader} from '@loaders.gl/loader-utils';
6
+ import type {MeshArrowTable} from '@loaders.gl/schema';
7
+ import type {SplatsLoaderOptions} from './types';
8
+ import {SPLATFormat} from './splats-format';
9
+
10
+ // __VERSION__ is injected by babel-plugin-version-inline
11
+ // @ts-ignore TS2304: Cannot find name '__VERSION__'.
12
+ const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';
13
+
14
+ /** Loader metadata for raw `.splat` Gaussian splat files. */
15
+ export const SPLATLoader = {
16
+ dataType: null as unknown as MeshArrowTable,
17
+ batchType: null as never,
18
+ ...SPLATFormat,
19
+ version: VERSION,
20
+ options: {
21
+ splats: {
22
+ shape: 'arrow-table'
23
+ }
24
+ }
25
+ } as const satisfies Loader<MeshArrowTable, never, SplatsLoaderOptions>;
@@ -0,0 +1,16 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {LoaderWithParser} from '@loaders.gl/loader-utils';
6
+ import type {MeshArrowTable} from '@loaders.gl/schema';
7
+ import type {SplatsLoaderOptions} from './types';
8
+ import {parseSPLAT} from './lib/parse-splat';
9
+ import {SPLATLoader as SPLATLoaderMetadata} from './splat-loader-types';
10
+
11
+ /** Parser-bearing loader for raw `.splat` Gaussian splat files. */
12
+ export const SPLATLoaderWithParser = {
13
+ ...SPLATLoaderMetadata,
14
+ parse: async (arrayBuffer: ArrayBuffer) => parseSPLAT(arrayBuffer),
15
+ parseSync: (arrayBuffer: ArrayBuffer) => parseSPLAT(arrayBuffer)
16
+ } as const satisfies LoaderWithParser<MeshArrowTable, never, SplatsLoaderOptions>;
@@ -0,0 +1,47 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {Format} from '@loaders.gl/loader-utils';
6
+
7
+ /** SPLAT - raw binary Gaussian splat format. */
8
+ export const SPLATFormat = {
9
+ name: 'SPLAT',
10
+ id: 'splat',
11
+ module: 'splats',
12
+ extensions: ['splat'],
13
+ mimeTypes: ['application/octet-stream'],
14
+ binary: true
15
+ } as const satisfies Format;
16
+
17
+ /** KSPLAT - GaussianSplats3D optimized Gaussian splat format. */
18
+ export const KSPLATFormat = {
19
+ name: 'KSPLAT',
20
+ id: 'ksplat',
21
+ module: 'splats',
22
+ extensions: ['ksplat'],
23
+ mimeTypes: ['application/octet-stream'],
24
+ binary: true
25
+ } as const satisfies Format;
26
+
27
+ /** SPZ - Niantic Spatial compressed Gaussian splat format. */
28
+ export const SPZFormat = {
29
+ name: 'SPZ',
30
+ id: 'spz',
31
+ module: 'splats',
32
+ extensions: ['spz'],
33
+ mimeTypes: ['application/octet-stream'],
34
+ binary: true,
35
+ tests: [new Uint8Array([0x4e, 0x47, 0x53, 0x50]).buffer]
36
+ } as const satisfies Format;
37
+
38
+ /** RAD - Spark paged level-of-detail Gaussian splat container. */
39
+ export const RADFormat = {
40
+ name: 'RAD',
41
+ id: 'rad',
42
+ module: 'splats',
43
+ extensions: ['rad'],
44
+ mimeTypes: ['application/octet-stream'],
45
+ binary: true,
46
+ tests: [new Uint8Array([0x52, 0x41, 0x44, 0x30]).buffer]
47
+ } as const satisfies Format;
@@ -0,0 +1,25 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {Loader} from '@loaders.gl/loader-utils';
6
+ import type {MeshArrowTable} from '@loaders.gl/schema';
7
+ import type {SplatsLoaderOptions} from './types';
8
+ import {SPZFormat} from './splats-format';
9
+
10
+ // __VERSION__ is injected by babel-plugin-version-inline
11
+ // @ts-ignore TS2304: Cannot find name '__VERSION__'.
12
+ const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';
13
+
14
+ /** Loader metadata for Niantic Spatial `.spz` Gaussian splat files. */
15
+ export const SPZLoader = {
16
+ dataType: null as unknown as MeshArrowTable,
17
+ batchType: null as never,
18
+ ...SPZFormat,
19
+ version: VERSION,
20
+ options: {
21
+ splats: {
22
+ shape: 'arrow-table'
23
+ }
24
+ }
25
+ } as const satisfies Loader<MeshArrowTable, never, SplatsLoaderOptions>;