@loaders.gl/splats 5.0.0-alpha.1 → 5.0.0-alpha.2
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.
- package/LICENSE +14 -3
- package/dist/dist.dev.js +21416 -5335
- package/dist/dist.min.js +46 -7
- package/dist/index.cjs +942 -180
- package/dist/index.cjs.map +3 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/ksplat-loader-types.js +1 -1
- package/dist/lib/parse-rad.d.ts +23 -7
- package/dist/lib/parse-rad.d.ts.map +1 -1
- package/dist/lib/parse-rad.js +10 -135
- package/dist/lib/parse-rad.js.map +1 -1
- package/dist/lib/parse-spz.d.ts.map +1 -1
- package/dist/lib/parse-spz.js +38 -3
- package/dist/lib/parse-spz.js.map +1 -1
- package/dist/rad-loader-types.js +1 -1
- package/dist/rad-metadata.schema.json +133 -0
- package/dist/rad-source-loader.js +1 -1
- package/dist/rad-zod-schema.cjs +81 -0
- package/dist/rad-zod-schema.cjs.map +6 -0
- package/dist/rad-zod-schema.d.ts +88 -0
- package/dist/rad-zod-schema.d.ts.map +1 -0
- package/dist/rad-zod-schema.js +70 -0
- package/dist/rad-zod-schema.js.map +1 -0
- package/dist/radc-metadata.schema.json +112 -0
- package/dist/splat-loader-types.js +1 -1
- package/dist/spz-loader-types.js +1 -1
- package/package.json +17 -8
- package/src/index.ts +2 -0
- package/src/lib/parse-rad.ts +46 -167
- package/src/lib/parse-spz.ts +57 -3
- package/src/rad-zod-schema.ts +83 -0
package/src/lib/parse-rad.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
// Copyright (c) vis.gl contributors
|
|
4
4
|
|
|
5
|
+
import {RADChunkMetadataJSONSchema, RADMetadataJSONSchema} from '../rad-zod-schema';
|
|
6
|
+
|
|
5
7
|
const RAD_MAGIC = 0x30444152;
|
|
6
8
|
const RAD_CHUNK_MAGIC = 0x43444152;
|
|
7
9
|
const RAD_HEADER_BYTE_LENGTH = 8;
|
|
@@ -26,6 +28,8 @@ export type RADSplatEncoding = {
|
|
|
26
28
|
sh3Max?: number;
|
|
27
29
|
/** Whether opacity is encoded for LoD blending. */
|
|
28
30
|
lodOpacity?: boolean;
|
|
31
|
+
/** Additional splat-encoding properties are preserved verbatim. */
|
|
32
|
+
[key: string]: unknown;
|
|
29
33
|
};
|
|
30
34
|
|
|
31
35
|
/** One RAD chunk location in the top-level RAD chunk table. */
|
|
@@ -40,14 +44,16 @@ export type RADChunkRange = {
|
|
|
40
44
|
count?: number;
|
|
41
45
|
/** Optional sidecar `.radc` filename relative to the RAD file URL. */
|
|
42
46
|
filename?: string;
|
|
47
|
+
/** Additional chunk-range properties are preserved verbatim. */
|
|
48
|
+
[key: string]: unknown;
|
|
43
49
|
};
|
|
44
50
|
|
|
45
|
-
/**
|
|
46
|
-
export type
|
|
51
|
+
/** Handwritten type for the JSON metadata stored in a Spark RAD header. */
|
|
52
|
+
export type RADMetadataJSON = {
|
|
47
53
|
/** RAD container version. Version 1 is currently supported. */
|
|
48
|
-
version:
|
|
54
|
+
version: 1;
|
|
49
55
|
/** RAD payload type. Spark currently writes `gsplat`. */
|
|
50
|
-
type:
|
|
56
|
+
type: 'gsplat';
|
|
51
57
|
/** Total splat count represented by the RAD source. */
|
|
52
58
|
count: number;
|
|
53
59
|
/** Maximum spherical harmonics degree present in the chunks. */
|
|
@@ -66,6 +72,12 @@ export type RADMetadata = {
|
|
|
66
72
|
shCodeCount?: number;
|
|
67
73
|
/** Optional RAD writer comment. */
|
|
68
74
|
comment?: string;
|
|
75
|
+
/** Additional RAD metadata properties are preserved verbatim. */
|
|
76
|
+
[key: string]: unknown;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Parsed Spark RAD top-level metadata with loader-derived byte offsets. */
|
|
80
|
+
export type RADMetadata = RADMetadataJSON & {
|
|
69
81
|
/** Byte length of the JSON metadata block. */
|
|
70
82
|
headerJsonByteLength: number;
|
|
71
83
|
/** Byte offset where inline RAD chunks begin. */
|
|
@@ -124,12 +136,14 @@ export type RADChunkProperty = {
|
|
|
124
136
|
min?: number;
|
|
125
137
|
/** Optional decode maximum for quantized properties. */
|
|
126
138
|
max?: number;
|
|
139
|
+
/** Additional chunk-property metadata is preserved verbatim. */
|
|
140
|
+
[key: string]: unknown;
|
|
127
141
|
};
|
|
128
142
|
|
|
129
|
-
/**
|
|
130
|
-
export type
|
|
143
|
+
/** Handwritten type for the JSON metadata stored in a Spark RADC chunk header. */
|
|
144
|
+
export type RADChunkMetadataJSON = {
|
|
131
145
|
/** RAD chunk version. Version 1 is currently supported. */
|
|
132
|
-
version:
|
|
146
|
+
version: 1;
|
|
133
147
|
/** First global splat index represented by this chunk. */
|
|
134
148
|
base: number;
|
|
135
149
|
/** Number of splats represented by this chunk. */
|
|
@@ -144,6 +158,12 @@ export type RADChunkMetadata = {
|
|
|
144
158
|
splatEncoding?: RADSplatEncoding;
|
|
145
159
|
/** Property table for the chunk payload. */
|
|
146
160
|
properties: RADChunkProperty[];
|
|
161
|
+
/** Additional RADC metadata properties are preserved verbatim. */
|
|
162
|
+
[key: string]: unknown;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** Parsed Spark RADC chunk metadata with loader-derived byte offsets. */
|
|
166
|
+
export type RADChunkMetadata = RADChunkMetadataJSON & {
|
|
147
167
|
/** Byte length of the chunk JSON metadata block. */
|
|
148
168
|
headerJsonByteLength: number;
|
|
149
169
|
/** Byte offset where the chunk payload begins. */
|
|
@@ -191,9 +211,11 @@ export function tryParseRADHeader(data: ArrayBuffer | ArrayBufferView): RADMetad
|
|
|
191
211
|
return null;
|
|
192
212
|
}
|
|
193
213
|
|
|
194
|
-
const rawMetadata =
|
|
195
|
-
|
|
196
|
-
|
|
214
|
+
const rawMetadata = RADMetadataJSONSchema.parse(
|
|
215
|
+
parseJSON(
|
|
216
|
+
bytes.subarray(RAD_HEADER_BYTE_LENGTH, headerJsonEnd),
|
|
217
|
+
'RADLoader: failed to parse RAD metadata JSON.'
|
|
218
|
+
)
|
|
197
219
|
);
|
|
198
220
|
return normalizeRADMetadata(rawMetadata, headerJsonByteLength);
|
|
199
221
|
}
|
|
@@ -219,9 +241,11 @@ export function parseRADChunkHeader(data: ArrayBuffer | ArrayBufferView): RADChu
|
|
|
219
241
|
throw new Error('RADLoader: RADC chunk must contain a complete metadata header.');
|
|
220
242
|
}
|
|
221
243
|
|
|
222
|
-
const rawMetadata =
|
|
223
|
-
|
|
224
|
-
|
|
244
|
+
const rawMetadata = RADChunkMetadataJSONSchema.parse(
|
|
245
|
+
parseJSON(
|
|
246
|
+
bytes.subarray(RAD_HEADER_BYTE_LENGTH, headerJsonEnd),
|
|
247
|
+
'RADLoader: failed to parse RADC metadata JSON.'
|
|
248
|
+
)
|
|
225
249
|
);
|
|
226
250
|
const payloadBytes = readSafeUint64(
|
|
227
251
|
dataView,
|
|
@@ -242,188 +266,43 @@ export function roundUpToEight(byteLength: number): number {
|
|
|
242
266
|
}
|
|
243
267
|
|
|
244
268
|
function normalizeRADMetadata(
|
|
245
|
-
rawMetadata:
|
|
269
|
+
rawMetadata: RADMetadataJSON,
|
|
246
270
|
headerJsonByteLength: number
|
|
247
271
|
): RADMetadata {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
throw new Error(`RADLoader: version ${version} is not supported.`);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const type = requireString(rawMetadata.type, 'RAD type');
|
|
254
|
-
if (type !== 'gsplat') {
|
|
255
|
-
throw new Error(`RADLoader: RAD type ${type} is not supported.`);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const chunks = requireArray(rawMetadata.chunks, 'RAD chunks').map((chunk, chunkIndex) =>
|
|
259
|
-
normalizeRADChunkRange(chunk, chunkIndex)
|
|
260
|
-
);
|
|
261
|
-
const metadata: RADMetadata = {
|
|
262
|
-
version,
|
|
263
|
-
type,
|
|
264
|
-
count: requireSafeInteger(rawMetadata.count, 'RAD count'),
|
|
265
|
-
maxSh: optionalSafeInteger(rawMetadata.maxSh, 'RAD maxSh'),
|
|
266
|
-
lodTree: optionalBoolean(rawMetadata.lodTree, 'RAD lodTree'),
|
|
267
|
-
chunkSize: optionalSafeInteger(rawMetadata.chunkSize, 'RAD chunkSize'),
|
|
268
|
-
allChunkBytes: optionalSafeInteger(rawMetadata.allChunkBytes, 'RAD allChunkBytes'),
|
|
269
|
-
chunks,
|
|
270
|
-
splatEncoding: optionalSplatEncoding(rawMetadata.splatEncoding, 'RAD splatEncoding'),
|
|
271
|
-
shCodeCount: optionalSafeInteger(rawMetadata.shCodeCount, 'RAD shCodeCount'),
|
|
272
|
-
comment: optionalString(rawMetadata.comment, 'RAD comment'),
|
|
272
|
+
return {
|
|
273
|
+
...rawMetadata,
|
|
273
274
|
headerJsonByteLength,
|
|
274
275
|
chunksByteOffset: RAD_HEADER_BYTE_LENGTH + roundUpToEight(headerJsonByteLength)
|
|
275
276
|
};
|
|
276
|
-
|
|
277
|
-
return metadata;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function normalizeRADChunkRange(rawChunk: unknown, chunkIndex: number): RADChunkRange {
|
|
281
|
-
const chunk = requireRecord(rawChunk, `RAD chunk ${chunkIndex}`);
|
|
282
|
-
return {
|
|
283
|
-
offset: requireSafeInteger(chunk.offset, `RAD chunk ${chunkIndex} offset`),
|
|
284
|
-
bytes: requireSafeInteger(chunk.bytes, `RAD chunk ${chunkIndex} bytes`),
|
|
285
|
-
base: optionalSafeInteger(chunk.base, `RAD chunk ${chunkIndex} base`),
|
|
286
|
-
count: optionalSafeInteger(chunk.count, `RAD chunk ${chunkIndex} count`),
|
|
287
|
-
filename: optionalString(chunk.filename, `RAD chunk ${chunkIndex} filename`)
|
|
288
|
-
};
|
|
289
277
|
}
|
|
290
278
|
|
|
291
279
|
function normalizeRADChunkMetadata(
|
|
292
|
-
rawMetadata:
|
|
280
|
+
rawMetadata: RADChunkMetadataJSON,
|
|
293
281
|
headerJsonByteLength: number,
|
|
294
282
|
payloadByteOffset: number,
|
|
295
283
|
payloadBytes: number
|
|
296
284
|
): RADChunkMetadata {
|
|
297
|
-
|
|
298
|
-
if (version !== 1) {
|
|
299
|
-
throw new Error(`RADLoader: RADC version ${version} is not supported.`);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const metadataPayloadBytes = requireSafeInteger(rawMetadata.payloadBytes, 'RADC payloadBytes');
|
|
303
|
-
if (metadataPayloadBytes !== payloadBytes) {
|
|
285
|
+
if (rawMetadata.payloadBytes !== payloadBytes) {
|
|
304
286
|
throw new Error('RADLoader: RADC metadata payload byte length does not match binary header.');
|
|
305
287
|
}
|
|
306
288
|
|
|
307
289
|
return {
|
|
308
|
-
|
|
309
|
-
base: requireSafeInteger(rawMetadata.base, 'RADC base'),
|
|
310
|
-
count: requireSafeInteger(rawMetadata.count, 'RADC count'),
|
|
290
|
+
...rawMetadata,
|
|
311
291
|
payloadBytes,
|
|
312
|
-
maxSh: optionalSafeInteger(rawMetadata.maxSh, 'RADC maxSh'),
|
|
313
|
-
lodTree: optionalBoolean(rawMetadata.lodTree, 'RADC lodTree'),
|
|
314
|
-
splatEncoding: optionalSplatEncoding(rawMetadata.splatEncoding, 'RADC splatEncoding'),
|
|
315
|
-
properties: requireArray(rawMetadata.properties, 'RADC properties').map((property, index) =>
|
|
316
|
-
normalizeRADChunkProperty(property, index)
|
|
317
|
-
),
|
|
318
292
|
headerJsonByteLength,
|
|
319
293
|
payloadByteOffset,
|
|
320
294
|
chunkByteLength: payloadByteOffset + payloadBytes
|
|
321
295
|
};
|
|
322
296
|
}
|
|
323
297
|
|
|
324
|
-
function
|
|
325
|
-
const property = requireRecord(rawProperty, `RADC property ${propertyIndex}`);
|
|
326
|
-
return {
|
|
327
|
-
offset: requireSafeInteger(property.offset, `RADC property ${propertyIndex} offset`),
|
|
328
|
-
bytes: requireSafeInteger(property.bytes, `RADC property ${propertyIndex} bytes`),
|
|
329
|
-
property: requireString(property.property, `RADC property ${propertyIndex} name`),
|
|
330
|
-
encoding: requireString(property.encoding, `RADC property ${propertyIndex} encoding`),
|
|
331
|
-
compression: optionalString(property.compression, `RADC property ${propertyIndex} compression`),
|
|
332
|
-
min: optionalFiniteNumber(property.min, `RADC property ${propertyIndex} min`),
|
|
333
|
-
max: optionalFiniteNumber(property.max, `RADC property ${propertyIndex} max`)
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function optionalSplatEncoding(value: unknown, fieldName: string): RADSplatEncoding | undefined {
|
|
338
|
-
if (value === undefined) {
|
|
339
|
-
return undefined;
|
|
340
|
-
}
|
|
341
|
-
const encoding = requireRecord(value, fieldName);
|
|
342
|
-
return {
|
|
343
|
-
rgbMin: optionalFiniteNumber(encoding.rgbMin, `${fieldName}.rgbMin`),
|
|
344
|
-
rgbMax: optionalFiniteNumber(encoding.rgbMax, `${fieldName}.rgbMax`),
|
|
345
|
-
lnScaleMin: optionalFiniteNumber(encoding.lnScaleMin, `${fieldName}.lnScaleMin`),
|
|
346
|
-
lnScaleMax: optionalFiniteNumber(encoding.lnScaleMax, `${fieldName}.lnScaleMax`),
|
|
347
|
-
sh1Max: optionalFiniteNumber(encoding.sh1Max, `${fieldName}.sh1Max`),
|
|
348
|
-
sh2Max: optionalFiniteNumber(encoding.sh2Max, `${fieldName}.sh2Max`),
|
|
349
|
-
sh3Max: optionalFiniteNumber(encoding.sh3Max, `${fieldName}.sh3Max`),
|
|
350
|
-
lodOpacity: optionalBoolean(encoding.lodOpacity, `${fieldName}.lodOpacity`)
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function parseJSONRecord(bytes: Uint8Array, message: string): Record<string, unknown> {
|
|
298
|
+
function parseJSON(bytes: Uint8Array, message: string): unknown {
|
|
355
299
|
try {
|
|
356
|
-
return
|
|
357
|
-
} catch
|
|
358
|
-
if (error instanceof Error && error.message.startsWith('RADLoader:')) {
|
|
359
|
-
throw error;
|
|
360
|
-
}
|
|
300
|
+
return JSON.parse(TEXT_DECODER.decode(bytes));
|
|
301
|
+
} catch {
|
|
361
302
|
throw new Error(message);
|
|
362
303
|
}
|
|
363
304
|
}
|
|
364
305
|
|
|
365
|
-
function requireRecord(value: unknown, fieldName: string): Record<string, unknown> {
|
|
366
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
367
|
-
throw new Error(`RADLoader: ${fieldName} must be an object.`);
|
|
368
|
-
}
|
|
369
|
-
return value as Record<string, unknown>;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function requireArray(value: unknown, fieldName: string): unknown[] {
|
|
373
|
-
if (!Array.isArray(value)) {
|
|
374
|
-
throw new Error(`RADLoader: ${fieldName} must be an array.`);
|
|
375
|
-
}
|
|
376
|
-
return value;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function requireString(value: unknown, fieldName: string): string {
|
|
380
|
-
if (typeof value !== 'string') {
|
|
381
|
-
throw new Error(`RADLoader: ${fieldName} must be a string.`);
|
|
382
|
-
}
|
|
383
|
-
return value;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function optionalString(value: unknown, fieldName: string): string | undefined {
|
|
387
|
-
if (value === undefined) {
|
|
388
|
-
return undefined;
|
|
389
|
-
}
|
|
390
|
-
return requireString(value, fieldName);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function optionalBoolean(value: unknown, fieldName: string): boolean | undefined {
|
|
394
|
-
if (value === undefined) {
|
|
395
|
-
return undefined;
|
|
396
|
-
}
|
|
397
|
-
if (typeof value !== 'boolean') {
|
|
398
|
-
throw new Error(`RADLoader: ${fieldName} must be a boolean.`);
|
|
399
|
-
}
|
|
400
|
-
return value;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function requireSafeInteger(value: unknown, fieldName: string): number {
|
|
404
|
-
if (!Number.isSafeInteger(value) || Number(value) < 0) {
|
|
405
|
-
throw new Error(`RADLoader: ${fieldName} must be a non-negative safe integer.`);
|
|
406
|
-
}
|
|
407
|
-
return Number(value);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function optionalSafeInteger(value: unknown, fieldName: string): number | undefined {
|
|
411
|
-
if (value === undefined) {
|
|
412
|
-
return undefined;
|
|
413
|
-
}
|
|
414
|
-
return requireSafeInteger(value, fieldName);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function optionalFiniteNumber(value: unknown, fieldName: string): number | undefined {
|
|
418
|
-
if (value === undefined) {
|
|
419
|
-
return undefined;
|
|
420
|
-
}
|
|
421
|
-
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
422
|
-
throw new Error(`RADLoader: ${fieldName} must be a finite number.`);
|
|
423
|
-
}
|
|
424
|
-
return value;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
306
|
function readSafeUint64(dataView: DataView, byteOffset: number, fieldName: string): number {
|
|
428
307
|
const value = dataView.getBigUint64(byteOffset, true);
|
|
429
308
|
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
package/src/lib/parse-spz.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
// Copyright (c) vis.gl contributors
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {decompressWithNativeDecompressionStream} from '@loaders.gl/compression/native-decompression';
|
|
6
|
+
import {getJSModuleOrNull, registerJSModules} from '@loaders.gl/loader-utils';
|
|
6
7
|
import type {MeshArrowTable} from '@loaders.gl/schema';
|
|
7
8
|
import type {GaussianSplats, SplatsLoaderOptions} from '../types';
|
|
8
9
|
import {makeGaussianSplatsArrowTable} from './splats-arrow-table';
|
|
@@ -51,10 +52,24 @@ export async function parseSPZToGaussianSplats(
|
|
|
51
52
|
const compressedStreams = streamInfos.map(streamInfo =>
|
|
52
53
|
data.slice(streamInfo.compressedOffset, streamInfo.compressedOffset + streamInfo.compressedSize)
|
|
53
54
|
);
|
|
54
|
-
|
|
55
|
+
registerJSModules(options?.modules);
|
|
56
|
+
const useNativeDecompressionStream = !getJSModuleOrNull('zstd-codec');
|
|
57
|
+
let compressionPromise:
|
|
58
|
+
| Promise<{
|
|
59
|
+
decompress(input: ArrayBuffer, size?: number): Promise<ArrayBuffer>;
|
|
60
|
+
}>
|
|
61
|
+
| undefined;
|
|
55
62
|
const streams = await Promise.all(
|
|
56
63
|
compressedStreams.map((compressedStream, streamIndex) =>
|
|
57
|
-
|
|
64
|
+
decompressSPZStream(
|
|
65
|
+
compressedStream,
|
|
66
|
+
streamInfos[streamIndex].uncompressedSize,
|
|
67
|
+
useNativeDecompressionStream,
|
|
68
|
+
() => {
|
|
69
|
+
compressionPromise ||= createZstdCompression(options?.modules);
|
|
70
|
+
return compressionPromise;
|
|
71
|
+
}
|
|
72
|
+
)
|
|
58
73
|
)
|
|
59
74
|
);
|
|
60
75
|
|
|
@@ -62,6 +77,45 @@ export async function parseSPZToGaussianSplats(
|
|
|
62
77
|
return decodeSPZStreams(header, streams, data);
|
|
63
78
|
}
|
|
64
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Decompresses one SPZ stream natively when available, then lazily loads the codec fallback.
|
|
82
|
+
*
|
|
83
|
+
* @param compressedStream Compressed Zstandard stream.
|
|
84
|
+
* @param uncompressedSize Expected uncompressed byte length.
|
|
85
|
+
* @param useNativeDecompressionStream Whether no provided codec should take precedence.
|
|
86
|
+
* @param getCompression Lazily resolves the codec-backed Zstandard implementation.
|
|
87
|
+
* @returns Decompressed stream bytes.
|
|
88
|
+
*/
|
|
89
|
+
async function decompressSPZStream(
|
|
90
|
+
compressedStream: ArrayBuffer,
|
|
91
|
+
uncompressedSize: number,
|
|
92
|
+
useNativeDecompressionStream: boolean,
|
|
93
|
+
getCompression: () => Promise<{
|
|
94
|
+
decompress(input: ArrayBuffer, size?: number): Promise<ArrayBuffer>;
|
|
95
|
+
}>
|
|
96
|
+
): Promise<ArrayBuffer> {
|
|
97
|
+
if (useNativeDecompressionStream) {
|
|
98
|
+
const nativeOutput = await decompressWithNativeDecompressionStream(compressedStream, 'zstd');
|
|
99
|
+
if (nativeOutput) {
|
|
100
|
+
return nativeOutput;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const compression = await getCompression();
|
|
105
|
+
return await compression.decompress(compressedStream, uncompressedSize);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Dynamically loads the codec-backed Zstandard class for unsupported native runtimes.
|
|
110
|
+
*
|
|
111
|
+
* @param modules Optional registered codec modules.
|
|
112
|
+
* @returns Codec-backed Zstandard implementation.
|
|
113
|
+
*/
|
|
114
|
+
async function createZstdCompression(modules?: {[key: string]: any}) {
|
|
115
|
+
const {ZstdCompression} = await import('@loaders.gl/compression/zstd-compression');
|
|
116
|
+
return new ZstdCompression({modules});
|
|
117
|
+
}
|
|
118
|
+
|
|
65
119
|
/** Parses and validates the SPZ v4 plaintext header. */
|
|
66
120
|
function parseSPZHeader(data: ArrayBuffer): SPZHeader {
|
|
67
121
|
if (data.byteLength < SPZ_HEADER_BYTE_LENGTH) {
|
|
@@ -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>;
|