@manycore/aholo-splat-transform 1.2.7 → 1.2.9

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 (98) hide show
  1. package/CHANGELOG.md +120 -106
  2. package/COPYRIGHT.md +17 -0
  3. package/README.md +39 -39
  4. package/THIRD_PARTY_LICENSES.txt +1373 -0
  5. package/bin/cli.js +125 -118
  6. package/dist/SplatData.d.ts +67 -67
  7. package/dist/SplatData.js +167 -156
  8. package/dist/constant.d.ts +3 -3
  9. package/dist/constant.js +13 -13
  10. package/dist/file/IFile.d.ts +5 -5
  11. package/dist/file/IFile.js +1 -1
  12. package/dist/file/esz.d.ts +11 -0
  13. package/dist/file/esz.js +337 -0
  14. package/dist/file/index.d.ts +8 -7
  15. package/dist/file/index.js +7 -6
  16. package/dist/file/ksplat.d.ts +12 -12
  17. package/dist/file/ksplat.js +293 -232
  18. package/dist/file/lcc.d.ts +11 -11
  19. package/dist/file/lcc.js +161 -157
  20. package/dist/file/ply.d.ts +13 -13
  21. package/dist/file/ply.js +439 -388
  22. package/dist/file/sog.d.ts +80 -80
  23. package/dist/file/sog.js +525 -504
  24. package/dist/file/splat.d.ts +6 -6
  25. package/dist/file/splat.js +119 -99
  26. package/dist/file/spz.d.ts +11 -8
  27. package/dist/file/spz.js +597 -400
  28. package/dist/file/voxel.d.ts +43 -37
  29. package/dist/file/voxel.js +411 -280
  30. package/dist/index.d.ts +33 -33
  31. package/dist/index.js +54 -54
  32. package/dist/native/index.d.ts +54 -54
  33. package/dist/native/index.js +122 -128
  34. package/dist/native/utils.d.ts +1 -0
  35. package/dist/native/utils.js +54 -0
  36. package/dist/tasks/AutoChunkLodTask.d.ts +13 -13
  37. package/dist/tasks/AutoChunkLodTask.js +117 -117
  38. package/dist/tasks/AutoLodTask.d.ts +10 -10
  39. package/dist/tasks/AutoLodTask.js +20 -20
  40. package/dist/tasks/BaseTask.d.ts +15 -15
  41. package/dist/tasks/BaseTask.js +5 -5
  42. package/dist/tasks/FlexLodTask.d.ts +12 -12
  43. package/dist/tasks/FlexLodTask.js +54 -44
  44. package/dist/tasks/ModifyTask.d.ts +9 -9
  45. package/dist/tasks/ModifyTask.js +166 -156
  46. package/dist/tasks/ReadTask.d.ts +9 -9
  47. package/dist/tasks/ReadTask.js +29 -29
  48. package/dist/tasks/SkeletonLodTask.d.ts +10 -10
  49. package/dist/tasks/SkeletonLodTask.js +176 -156
  50. package/dist/tasks/VoxelTask.d.ts +35 -30
  51. package/dist/tasks/VoxelTask.js +40 -37
  52. package/dist/tasks/WriteTask.d.ts +12 -11
  53. package/dist/tasks/WriteTask.js +70 -70
  54. package/dist/utils/BufferReader.d.ts +12 -12
  55. package/dist/utils/BufferReader.js +45 -47
  56. package/dist/utils/Logger.d.ts +11 -11
  57. package/dist/utils/Logger.js +40 -38
  58. package/dist/utils/StreamChunkDecoder.d.ts +16 -16
  59. package/dist/utils/StreamChunkDecoder.js +31 -36
  60. package/dist/utils/index.d.ts +27 -27
  61. package/dist/utils/index.js +101 -101
  62. package/dist/utils/k-means.d.ts +4 -4
  63. package/dist/utils/k-means.js +340 -350
  64. package/dist/utils/math.d.ts +46 -46
  65. package/dist/utils/math.js +350 -351
  66. package/dist/utils/quantize-1d.d.ts +4 -4
  67. package/dist/utils/quantize-1d.js +164 -164
  68. package/dist/utils/sh-rotate.d.ts +2 -2
  69. package/dist/utils/sh-rotate.js +236 -175
  70. package/dist/utils/splat.d.ts +21 -20
  71. package/dist/utils/splat.js +397 -378
  72. package/dist/utils/voxel/binary.d.ts +8 -0
  73. package/dist/utils/voxel/binary.js +176 -0
  74. package/dist/utils/voxel/common.d.ts +178 -162
  75. package/dist/utils/voxel/common.js +1752 -1700
  76. package/dist/utils/voxel/coplanar-merge.d.ts +63 -63
  77. package/dist/utils/voxel/coplanar-merge.js +818 -819
  78. package/dist/utils/voxel/filter-cluster.d.ts +20 -0
  79. package/dist/utils/voxel/filter-cluster.js +628 -0
  80. package/dist/utils/voxel/gpu-dilation.d.ts +2 -2
  81. package/dist/utils/voxel/gpu-dilation.js +677 -665
  82. package/dist/utils/voxel/marching-cubes.d.ts +42 -42
  83. package/dist/utils/voxel/marching-cubes.js +1645 -1657
  84. package/dist/utils/voxel/mesh.d.ts +3 -3
  85. package/dist/utils/voxel/mesh.js +130 -130
  86. package/dist/utils/voxel/nav.d.ts +29 -29
  87. package/dist/utils/voxel/nav.js +1068 -1043
  88. package/dist/utils/voxel/postprocess.d.ts +23 -23
  89. package/dist/utils/voxel/postprocess.js +408 -375
  90. package/dist/utils/voxel/voxel-faces.d.ts +18 -18
  91. package/dist/utils/voxel/voxel-faces.js +662 -663
  92. package/dist/utils/voxel/voxelize.d.ts +34 -33
  93. package/dist/utils/voxel/voxelize.js +1208 -1193
  94. package/dist/utils/webgpu.d.ts +8 -8
  95. package/dist/utils/webgpu.js +122 -122
  96. package/package.json +37 -30
  97. package/dist/native/cpp/bin/linux/binding.node +0 -0
  98. package/dist/native/cpp/bin/windows/binding.node +0 -0
package/dist/file/spz.js CHANGED
@@ -1,400 +1,597 @@
1
- import { SH_C0, SH_MAPS } from '../constant.js';
2
- import { BufferReader, fromHalf, clamp, StreamChunkDecoder, mortonSort } from '../utils/index.js';
3
- const SPZ_MAGIC = 0x5053474e; // NGSP = Niantic gaussian splat
4
- const SPZ_VERSION = 3;
5
- const FLAG_ANTIALIASED = 0x1;
6
- const COLOR_SCALE = SH_C0 / 0.15;
7
- const rotation = new Array(4);
8
- const SH_SCALE1 = 1 << 3;
9
- const SH_SCALE2 = 1 << 4;
10
- export class SpzFile {
11
- compressLevel;
12
- constructor(compressLevel) {
13
- this.compressLevel = compressLevel;
14
- }
15
- async read(stream, _contentLength, data) {
16
- const setCenter = data.setCenter.bind(data);
17
- const setAlpha = data.setAlpha.bind(data);
18
- const setColor = data.setColor.bind(data);
19
- const setScale = data.setScale.bind(data);
20
- const setQuat = data.setQuat.bind(data);
21
- const setShN = data.setShN.bind(data);
22
- const SCALE_LUT = new Float32Array(256);
23
- for (let i = 0; i < 256; i++) {
24
- SCALE_LUT[i] = Math.exp(i / 16 - 10);
25
- }
26
- const COLOR_LUT = new Float32Array(256);
27
- for (let i = 0; i < 256; i++) {
28
- COLOR_LUT[i] = (i / 255 - 0.5) * COLOR_SCALE + 0.5;
29
- }
30
- let version = SPZ_VERSION;
31
- let counts = 0;
32
- let shDegree = 0;
33
- let fractionalBits = 12;
34
- let flags = FLAG_ANTIALIASED;
35
- let reserved = 0;
36
- let isF16 = false;
37
- let useSmallestThreeQuat = true;
38
- let fraction = 1;
39
- let fractionInv = 1;
40
- let shCounts = 0;
41
- let BlockOffset = 0;
42
- const shN = [];
43
- const reader = new BufferReader();
44
- const decoder = new StreamChunkDecoder(reader);
45
- decoder.setDecoders([
46
- {
47
- init: () => [1, 16],
48
- decode: async (_offset, _counts, buf) => {
49
- const header = new DataView(buf.buffer);
50
- if (header.getUint32(0, true) !== SPZ_MAGIC) {
51
- throw new Error('Invalid SPZ file');
52
- }
53
- version = header.getUint32(4, true);
54
- if (version < 1 || version > 3) {
55
- throw new Error(`Unsupported SPZ version: ${version}`);
56
- }
57
- counts = header.getUint32(8, true);
58
- shDegree = header.getUint8(12);
59
- fractionalBits = header.getUint8(13);
60
- flags = header.getUint8(14);
61
- reserved = header.getUint8(15);
62
- isF16 = version < 2;
63
- useSmallestThreeQuat = version >= 3;
64
- fraction = 1 << fractionalBits;
65
- fractionInv = 1 / fraction;
66
- shCounts = SH_MAPS[shDegree];
67
- BlockOffset = await data.initBlock(counts, shDegree);
68
- if (flags || reserved) {
69
- //
70
- }
71
- },
72
- },
73
- {
74
- init: () => [counts, isF16 ? 6 : 9],
75
- decode: (offset, counts, buf) => {
76
- offset += BlockOffset;
77
- let x, y, z;
78
- for (let i = 0; i < counts; i++) {
79
- if (isF16) {
80
- const o = i * 6;
81
- x = fromHalf((buf[o + 1] << 8) | buf[o]);
82
- y = fromHalf((buf[o + 3] << 8) | buf[o + 2]);
83
- z = fromHalf((buf[o + 5] << 8) | buf[o + 4]);
84
- }
85
- else {
86
- const o = i * 9;
87
- x = (((buf[o + 2] << 24) | (buf[o + 1] << 16) | (buf[o] << 8)) >> 8) * fractionInv;
88
- y = (((buf[o + 5] << 24) | (buf[o + 4] << 16) | (buf[o + 3] << 8)) >> 8) * fractionInv;
89
- z = (((buf[o + 8] << 24) | (buf[o + 7] << 16) | (buf[o + 6] << 8)) >> 8) * fractionInv;
90
- }
91
- setCenter(offset + i, x, y, z);
92
- }
93
- },
94
- },
95
- {
96
- init: () => [counts, 1],
97
- decode: (offset, counts, buf) => {
98
- offset += BlockOffset;
99
- for (let i = 0; i < counts; i++) {
100
- setAlpha(offset + i, buf[i] / 255);
101
- }
102
- },
103
- },
104
- {
105
- init: () => [counts, 3],
106
- decode: (offset, counts, buf) => {
107
- offset += BlockOffset;
108
- for (let i = 0; i < counts; i++) {
109
- const o = i * 3;
110
- setColor(offset + i, COLOR_LUT[buf[o]], COLOR_LUT[buf[o + 1]], COLOR_LUT[buf[o + 2]]);
111
- }
112
- },
113
- },
114
- {
115
- init: () => [counts, 3],
116
- decode: (offset, counts, buf) => {
117
- offset += BlockOffset;
118
- for (let i = 0; i < counts; i++) {
119
- const o = i * 3;
120
- setScale(offset + i, SCALE_LUT[buf[o]], SCALE_LUT[buf[o + 1]], SCALE_LUT[buf[o + 2]]);
121
- }
122
- },
123
- },
124
- {
125
- init: () => [counts, useSmallestThreeQuat ? 4 : 3],
126
- decode: (offset, counts, buf) => {
127
- offset += BlockOffset;
128
- let qx, qy, qz, qw;
129
- for (let i = 0; i < counts; i++) {
130
- if (!useSmallestThreeQuat) {
131
- const o = i * 3;
132
- qx = buf[o] / 127.5 - 1;
133
- qy = buf[o + 1] / 127.5 - 1;
134
- qz = buf[o + 2] / 127.5 - 1;
135
- qw = Math.sqrt(Math.max(0, 1 - qx * qx - qy * qy - qz * qz));
136
- }
137
- else {
138
- const o = i * 4;
139
- const packed = buf[o] | (buf[o + 1] << 8) | (buf[o + 2] << 16) | (buf[o + 3] << 24);
140
- const largest = packed >>> 30;
141
- let temp = packed;
142
- let sum = 0;
143
- for (let j = 3; j >= 0; j--) {
144
- if (j === largest) {
145
- continue;
146
- }
147
- const mag = temp & 0x1FF;
148
- const sign = (temp >>> 9) & 1;
149
- temp >>>= 10;
150
- const v = Math.SQRT1_2 * (mag / 0x1FF) * (sign ? -1 : 1);
151
- rotation[j] = v;
152
- sum += v * v;
153
- }
154
- rotation[largest] = Math.sqrt(1 - sum);
155
- qx = rotation[0];
156
- qy = rotation[1];
157
- qz = rotation[2];
158
- qw = rotation[3];
159
- }
160
- setQuat(offset + i, qx, qy, qz, qw);
161
- }
162
- },
163
- },
164
- {
165
- init: () => [counts, shCounts],
166
- decode: (offset, counts, buf) => {
167
- offset += BlockOffset;
168
- for (let i = 0; i < counts; i++) {
169
- const o = i * shCounts;
170
- for (let j = 0; j < shCounts; j++) {
171
- shN[j] = (buf[o + j] - 128) / 128;
172
- }
173
- setShN(offset + i, shN);
174
- }
175
- },
176
- },
177
- ]);
178
- let source;
179
- if (this.compressLevel === -1) {
180
- source = stream.getReader();
181
- }
182
- else {
183
- source = stream.pipeThrough(new DecompressionStream('gzip')).getReader();
184
- }
185
- while (true) {
186
- const { done, value } = await source.read();
187
- if (done) {
188
- break;
189
- }
190
- reader.write(value);
191
- decoder.flush();
192
- }
193
- data.finishBlock();
194
- }
195
- async write(writeStream, data, indices = mortonSort(data)) {
196
- let writer;
197
- let pipePromise;
198
- if (this.compressLevel === -1) {
199
- writer = writeStream.getWriter();
200
- pipePromise = Promise.resolve();
201
- }
202
- else {
203
- const compressStream = new CompressionStream('gzip');
204
- pipePromise = compressStream.readable.pipeTo(writeStream);
205
- writer = compressStream.writable.getWriter();
206
- }
207
- const version = SPZ_VERSION;
208
- const counts = data.counts;
209
- const shDegree = data.shDegree;
210
- const fractionalBits = 12;
211
- const flags = FLAG_ANTIALIASED;
212
- const reserved = 0;
213
- const fraction = 1 << fractionalBits;
214
- const shCounts = SH_MAPS[shDegree];
215
- // header
216
- {
217
- const buffer = new Uint8Array(16);
218
- const header = new DataView(buffer.buffer);
219
- header.setUint32(0, SPZ_MAGIC, true);
220
- header.setUint32(4, version, true);
221
- header.setUint32(8, counts, true);
222
- header.setUint8(12, shDegree);
223
- header.setUint8(13, fractionalBits);
224
- header.setUint8(14, flags);
225
- header.setUint8(15, reserved);
226
- writer.write(buffer);
227
- }
228
- const single = {
229
- x: 0, y: 0, z: 0,
230
- sx: 0, sy: 0, sz: 0,
231
- qx: 0, qy: 0, qz: 0, qw: 0,
232
- r: 0, g: 0, b: 0, a: 0,
233
- shN: new Array(shCounts),
234
- };
235
- // center
236
- {
237
- const ItemSize = 9;
238
- const chunkSize = 4096;
239
- const chunkCounts = Math.ceil(data.counts / chunkSize);
240
- for (let i = 0; i < chunkCounts; i++) {
241
- if (writer.desiredSize <= 0) {
242
- await writer.ready;
243
- }
244
- const currentChunkSize = Math.min(chunkSize, data.counts - i * chunkSize);
245
- const chunk = new Uint8Array(currentChunkSize * ItemSize);
246
- const offset = i * chunkSize;
247
- for (let j = 0; j < currentChunkSize; j++) {
248
- data.getCenter(indices[offset + j], single);
249
- const o = j * ItemSize;
250
- const ix = clamp(single.x * fraction, -0x7fffff, 0x7fffff);
251
- chunk[o + 0] = ix & 0xff;
252
- chunk[o + 1] = (ix >> 8) & 0xff;
253
- chunk[o + 2] = (ix >> 16) & 0xff;
254
- const iy = clamp(single.y * fraction, -0x7fffff, 0x7fffff);
255
- chunk[o + 3] = iy & 0xff;
256
- chunk[o + 4] = (iy >> 8) & 0xff;
257
- chunk[o + 5] = (iy >> 16) & 0xff;
258
- const iz = clamp(single.z * fraction, -0x7fffff, 0x7fffff);
259
- chunk[o + 6] = iz & 0xff;
260
- chunk[o + 7] = (iz >> 8) & 0xff;
261
- chunk[o + 8] = (iz >> 16) & 0xff;
262
- }
263
- writer.write(chunk);
264
- }
265
- }
266
- // alpha
267
- {
268
- const chunkSize = 65536;
269
- const chunkCounts = Math.ceil(data.counts / chunkSize);
270
- for (let i = 0; i < chunkCounts; i++) {
271
- if (writer.desiredSize <= 0) {
272
- await writer.ready;
273
- }
274
- const currentChunkSize = Math.min(chunkSize, data.counts - i * chunkSize);
275
- const chunk = new Uint8Array(currentChunkSize);
276
- const offset = i * chunkSize;
277
- for (let j = 0; j < currentChunkSize; j++) {
278
- data.getAlpha(indices[offset + j], single);
279
- chunk[j] = clamp(Math.round(single.a * 255), 0, 255);
280
- }
281
- writer.write(chunk);
282
- }
283
- }
284
- // color
285
- {
286
- const ItemSize = 3;
287
- const chunkSize = 16384;
288
- const chunkCounts = Math.ceil(data.counts / chunkSize);
289
- for (let i = 0; i < chunkCounts; i++) {
290
- if (writer.desiredSize <= 0) {
291
- await writer.ready;
292
- }
293
- const currentChunkSize = Math.min(chunkSize, data.counts - i * chunkSize);
294
- const chunk = new Uint8Array(currentChunkSize * ItemSize);
295
- const offset = i * chunkSize;
296
- for (let j = 0; j < currentChunkSize; j++) {
297
- data.getColor(indices[offset + j], single);
298
- const o = j * ItemSize;
299
- chunk[o + 0] = clamp(Math.round(((single.r - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
300
- chunk[o + 1] = clamp(Math.round(((single.g - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
301
- chunk[o + 2] = clamp(Math.round(((single.b - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
302
- }
303
- writer.write(chunk);
304
- }
305
- }
306
- // scale
307
- {
308
- const ItemSize = 3;
309
- const chunkSize = 16384;
310
- const chunkCounts = Math.ceil(data.counts / chunkSize);
311
- for (let i = 0; i < chunkCounts; i++) {
312
- if (writer.desiredSize <= 0) {
313
- await writer.ready;
314
- }
315
- const currentChunkSize = Math.min(chunkSize, data.counts - i * chunkSize);
316
- const chunk = new Uint8Array(currentChunkSize * ItemSize);
317
- const offset = i * chunkSize;
318
- for (let j = 0; j < currentChunkSize; j++) {
319
- data.getScale(indices[offset + j], single);
320
- const o = j * ItemSize;
321
- chunk[o + 0] = clamp(Math.round((Math.log(single.sx) + 10) * 16), 0, 255);
322
- chunk[o + 1] = clamp(Math.round((Math.log(single.sy) + 10) * 16), 0, 255);
323
- chunk[o + 2] = clamp(Math.round((Math.log(single.sz) + 10) * 16), 0, 255);
324
- }
325
- writer.write(chunk);
326
- }
327
- }
328
- // quat
329
- {
330
- const ItemSize = 4;
331
- const chunkSize = 16384;
332
- const chunkCounts = Math.ceil(data.counts / chunkSize);
333
- for (let i = 0; i < chunkCounts; i++) {
334
- if (writer.desiredSize <= 0) {
335
- await writer.ready;
336
- }
337
- const currentChunkSize = Math.min(chunkSize, data.counts - i * chunkSize);
338
- const chunk = new Uint8Array(currentChunkSize * ItemSize);
339
- const offset = i * chunkSize;
340
- for (let j = 0; j < currentChunkSize; j++) {
341
- data.getQuat(indices[offset + j], single);
342
- const o = j * ItemSize;
343
- rotation[0] = single.qx;
344
- rotation[1] = single.qy;
345
- rotation[2] = single.qz;
346
- rotation[3] = single.qw;
347
- let iLargest = 0;
348
- for (let i = 1; i < 4; ++i) {
349
- if (Math.abs(rotation[i]) > Math.abs(rotation[iLargest])) {
350
- iLargest = i;
351
- }
352
- }
353
- const negate = rotation[iLargest] < 0 ? 1 : 0;
354
- let comp = iLargest;
355
- for (let i = 0; i < 4; ++i) {
356
- if (i !== iLargest) {
357
- const negbit = (rotation[i] < 0 ? 1 : 0) ^ negate;
358
- const mag = Math.floor(((1 << 9) - 1) * (Math.abs(rotation[i]) / Math.SQRT1_2) + 0.5);
359
- comp = (comp << 10) | (negbit << 9) | mag;
360
- }
361
- }
362
- chunk[o + 0] = comp & 0xff;
363
- chunk[o + 1] = (comp >> 8) & 0xff;
364
- chunk[o + 2] = (comp >> 16) & 0xff;
365
- chunk[o + 3] = (comp >> 24) & 0xff;
366
- }
367
- writer.write(chunk);
368
- }
369
- }
370
- // shN
371
- if (shDegree > 0) {
372
- const shN = single.shN;
373
- const ItemSize = shCounts;
374
- const chunkSize = 1024;
375
- const chunkCounts = Math.ceil(data.counts / chunkSize);
376
- for (let i = 0; i < chunkCounts; i++) {
377
- if (writer.desiredSize <= 0) {
378
- await writer.ready;
379
- }
380
- const currentChunkSize = Math.min(chunkSize, data.counts - i * chunkSize);
381
- const chunk = new Uint8Array(currentChunkSize * ItemSize);
382
- const offset = i * chunkSize;
383
- for (let j = 0; j < currentChunkSize; j++) {
384
- data.getShN(indices[offset + j], shN);
385
- const o = j * ItemSize;
386
- for (let k = 0; k < ItemSize; k++) {
387
- if (k < 9) {
388
- chunk[o + k] = clamp(Math.floor((Math.round(shN[k] * 128) + 128 + SH_SCALE1 / 2) / SH_SCALE1) * SH_SCALE1, 0, 255);
389
- continue;
390
- }
391
- chunk[o + k] = clamp(Math.floor((Math.round(shN[k] * 128) + 128 + SH_SCALE2 / 2) / SH_SCALE2) * SH_SCALE2, 0, 255);
392
- }
393
- }
394
- writer.write(chunk);
395
- }
396
- }
397
- await writer.close();
398
- await pipePromise;
399
- }
400
- }
1
+ import { constants as zlibConstants, zstdCompressSync, zstdDecompressSync } from 'node:zlib';
2
+ import { SH_C0, SH_MAPS } from '../constant.js';
3
+ import { BufferReader, fromHalf, clamp, StreamChunkDecoder, mortonSort } from '../utils/index.js';
4
+ const SPZ_MAGIC = 0x5053474e; // NGSP = Niantic gaussian splat
5
+ const SPZ_VERSION = 3;
6
+ const ZSTD_COMPRESSION_LEVEL = 12;
7
+ const FLAG_ANTIALIASED = 0x1;
8
+ const COLOR_SCALE = SH_C0 / 0.15;
9
+ const rotation = new Array(4);
10
+ const SH_SCALE1 = 1 << 3;
11
+ const SH_SCALE2 = 1 << 4;
12
+ export class SpzFile {
13
+ constructor(compressLevel, spzVersion = SPZ_VERSION) {
14
+ if (spzVersion !== 3 && spzVersion !== 4) {
15
+ throw new Error(`Unsupported SPZ version: ${spzVersion}`);
16
+ }
17
+ this.compressLevel = compressLevel;
18
+ this.spzVersion = spzVersion;
19
+ }
20
+ async read(stream, _contentLength, data) {
21
+ const setCenter = data.setCenter.bind(data);
22
+ const setAlpha = data.setAlpha.bind(data);
23
+ const setColor = data.setColor.bind(data);
24
+ const setScale = data.setScale.bind(data);
25
+ const setQuat = data.setQuat.bind(data);
26
+ const setShN = data.setShN.bind(data);
27
+ const SCALE_LUT = new Float32Array(256);
28
+ for (let i = 0; i < 256; i++) {
29
+ SCALE_LUT[i] = Math.exp(i / 16 - 10);
30
+ }
31
+ const COLOR_LUT = new Float32Array(256);
32
+ for (let i = 0; i < 256; i++) {
33
+ COLOR_LUT[i] = (i / 255 - 0.5) * COLOR_SCALE + 0.5;
34
+ }
35
+ let version = SPZ_VERSION;
36
+ let counts = 0;
37
+ let shDegree = 0;
38
+ let fractionalBits = 12;
39
+ let flags = FLAG_ANTIALIASED;
40
+ let reserved = 0;
41
+ let isF16 = false;
42
+ let useSmallestThreeQuat = true;
43
+ let fraction = 1;
44
+ let fractionInv = 1;
45
+ let shCounts = 0;
46
+ let BlockOffset = 0;
47
+ const shN = [];
48
+ const reader = new BufferReader();
49
+ const decoder = new StreamChunkDecoder(reader);
50
+ decoder.setDecoders([
51
+ {
52
+ init: () => [1, 16],
53
+ decode: async (_offset, _counts, buf) => {
54
+ const header = new DataView(buf.buffer);
55
+ if (header.getUint32(0, true) !== SPZ_MAGIC) {
56
+ throw new Error('Invalid SPZ file');
57
+ }
58
+ ({ version, counts, shDegree, fractionalBits, flags, extra: reserved } = readSpzHeader(header));
59
+ if (version < 1 || version > 3) {
60
+ throw new Error(`Unsupported SPZ version: ${version}`);
61
+ }
62
+ isF16 = version < 2;
63
+ useSmallestThreeQuat = version >= 3;
64
+ fraction = 1 << fractionalBits;
65
+ fractionInv = 1 / fraction;
66
+ shCounts = SH_MAPS[shDegree];
67
+ BlockOffset = await data.initBlock(counts, shDegree);
68
+ if (flags || reserved) {
69
+ //
70
+ }
71
+ },
72
+ },
73
+ {
74
+ init: () => [counts, isF16 ? 6 : 9],
75
+ decode: (offset, counts, buf) => {
76
+ offset += BlockOffset;
77
+ let x, y, z;
78
+ for (let i = 0; i < counts; i++) {
79
+ if (isF16) {
80
+ const o = i * 6;
81
+ x = fromHalf((buf[o + 1] << 8) | buf[o]);
82
+ y = fromHalf((buf[o + 3] << 8) | buf[o + 2]);
83
+ z = fromHalf((buf[o + 5] << 8) | buf[o + 4]);
84
+ }
85
+ else {
86
+ const o = i * 9;
87
+ x = (((buf[o + 2] << 24) | (buf[o + 1] << 16) | (buf[o] << 8)) >> 8) * fractionInv;
88
+ y = (((buf[o + 5] << 24) | (buf[o + 4] << 16) | (buf[o + 3] << 8)) >> 8) * fractionInv;
89
+ z = (((buf[o + 8] << 24) | (buf[o + 7] << 16) | (buf[o + 6] << 8)) >> 8) * fractionInv;
90
+ }
91
+ setCenter(offset + i, x, y, z);
92
+ }
93
+ },
94
+ },
95
+ {
96
+ init: () => [counts, 1],
97
+ decode: (offset, counts, buf) => {
98
+ offset += BlockOffset;
99
+ for (let i = 0; i < counts; i++) {
100
+ setAlpha(offset + i, buf[i] / 255);
101
+ }
102
+ },
103
+ },
104
+ {
105
+ init: () => [counts, 3],
106
+ decode: (offset, counts, buf) => {
107
+ offset += BlockOffset;
108
+ for (let i = 0; i < counts; i++) {
109
+ const o = i * 3;
110
+ setColor(offset + i, COLOR_LUT[buf[o]], COLOR_LUT[buf[o + 1]], COLOR_LUT[buf[o + 2]]);
111
+ }
112
+ },
113
+ },
114
+ {
115
+ init: () => [counts, 3],
116
+ decode: (offset, counts, buf) => {
117
+ offset += BlockOffset;
118
+ for (let i = 0; i < counts; i++) {
119
+ const o = i * 3;
120
+ setScale(offset + i, SCALE_LUT[buf[o]], SCALE_LUT[buf[o + 1]], SCALE_LUT[buf[o + 2]]);
121
+ }
122
+ },
123
+ },
124
+ {
125
+ init: () => [counts, useSmallestThreeQuat ? 4 : 3],
126
+ decode: (offset, counts, buf) => {
127
+ offset += BlockOffset;
128
+ let qx, qy, qz, qw;
129
+ for (let i = 0; i < counts; i++) {
130
+ if (!useSmallestThreeQuat) {
131
+ const o = i * 3;
132
+ qx = buf[o] / 127.5 - 1;
133
+ qy = buf[o + 1] / 127.5 - 1;
134
+ qz = buf[o + 2] / 127.5 - 1;
135
+ qw = Math.sqrt(Math.max(0, 1 - qx * qx - qy * qy - qz * qz));
136
+ }
137
+ else {
138
+ const o = i * 4;
139
+ const packed = buf[o] | (buf[o + 1] << 8) | (buf[o + 2] << 16) | (buf[o + 3] << 24);
140
+ const largest = packed >>> 30;
141
+ let temp = packed;
142
+ let sum = 0;
143
+ for (let j = 3; j >= 0; j--) {
144
+ if (j === largest) {
145
+ continue;
146
+ }
147
+ const mag = temp & 0x1ff;
148
+ const sign = (temp >>> 9) & 1;
149
+ temp >>>= 10;
150
+ const v = Math.SQRT1_2 * (mag / 0x1ff) * (sign ? -1 : 1);
151
+ rotation[j] = v;
152
+ sum += v * v;
153
+ }
154
+ rotation[largest] = Math.sqrt(1 - sum);
155
+ qx = rotation[0];
156
+ qy = rotation[1];
157
+ qz = rotation[2];
158
+ qw = rotation[3];
159
+ }
160
+ setQuat(offset + i, qx, qy, qz, qw);
161
+ }
162
+ },
163
+ },
164
+ {
165
+ init: () => [counts, shCounts],
166
+ decode: (offset, counts, buf) => {
167
+ offset += BlockOffset;
168
+ for (let i = 0; i < counts; i++) {
169
+ const o = i * shCounts;
170
+ for (let j = 0; j < shCounts; j++) {
171
+ shN[j] = (buf[o + j] - 128) / 128;
172
+ }
173
+ setShN(offset + i, shN);
174
+ }
175
+ },
176
+ },
177
+ ]);
178
+ const peeked = await peekStream(stream, 8);
179
+ stream = peeked.stream;
180
+ if (isSpzV4(peeked.prefix)) {
181
+ await readSpzV4Stream(stream, reader, decoder);
182
+ data.finishBlock();
183
+ return;
184
+ }
185
+ let source;
186
+ if (this.compressLevel === -1) {
187
+ source = stream.getReader();
188
+ }
189
+ else {
190
+ source = stream
191
+ .pipeThrough(new DecompressionStream('gzip'))
192
+ .getReader();
193
+ }
194
+ while (true) {
195
+ const { done, value } = await source.read();
196
+ if (done) {
197
+ break;
198
+ }
199
+ reader.write(value);
200
+ decoder.flush();
201
+ }
202
+ data.finishBlock();
203
+ }
204
+ async write(writeStream, data, indices = mortonSort(data)) {
205
+ if (this.spzVersion === 4) {
206
+ await this.writeV4(writeStream, data, indices);
207
+ }
208
+ else {
209
+ await this.writeV3(writeStream, data, indices);
210
+ }
211
+ }
212
+ async writeV3(writeStream, data, indices) {
213
+ let writer;
214
+ let pipePromise;
215
+ if (this.compressLevel === -1) {
216
+ writer = writeStream.getWriter();
217
+ pipePromise = Promise.resolve();
218
+ }
219
+ else {
220
+ const compressStream = new CompressionStream('gzip');
221
+ pipePromise = compressStream.readable.pipeTo(writeStream);
222
+ writer = compressStream.writable.getWriter();
223
+ }
224
+ const version = SPZ_VERSION;
225
+ const counts = data.counts;
226
+ const shDegree = data.shDegree;
227
+ const fractionalBits = 12;
228
+ const flags = FLAG_ANTIALIASED;
229
+ const shCounts = getShCounts(shDegree);
230
+ const context = createSpzEncodeContext(data, indices, fractionalBits, shCounts);
231
+ // header
232
+ writer.write(createSpzHeader(version, counts, shDegree, fractionalBits, flags, 0));
233
+ for (const attribute of getSpzAttributes(shDegree)) {
234
+ await writeSpzAttribute(writer, context, attribute);
235
+ }
236
+ await writer.close();
237
+ await pipePromise;
238
+ }
239
+ async writeV4(writeStream, data, indices) {
240
+ const version = 4;
241
+ const counts = data.counts;
242
+ const shDegree = data.shDegree;
243
+ const fractionalBits = 12;
244
+ const flags = FLAG_ANTIALIASED;
245
+ const shCounts = getShCounts(shDegree);
246
+ const context = createSpzEncodeContext(data, indices, fractionalBits, shCounts);
247
+ const compressed = [];
248
+ const uncompressedSizes = [];
249
+ for (const attribute of getSpzAttributes(shDegree)) {
250
+ const chunk = createSpzAttributeChunk(context, attribute, 0, counts);
251
+ uncompressedSizes.push(chunk.byteLength);
252
+ compressed.push(zstdCompressSync(chunk, {
253
+ params: {
254
+ [zlibConstants.ZSTD_c_compressionLevel]: ZSTD_COMPRESSION_LEVEL,
255
+ },
256
+ }));
257
+ }
258
+ const tocByteOffset = 32;
259
+ const tocSize = compressed.length * 16;
260
+ const header = createSpzHeader(version, counts, shDegree, fractionalBits, flags, compressed.length, 32);
261
+ new DataView(header.buffer).setUint32(16, tocByteOffset, true);
262
+ const toc = new Uint8Array(tocSize);
263
+ const tocView = new DataView(toc.buffer);
264
+ for (let i = 0; i < compressed.length; i++) {
265
+ const entryOffset = i * 16;
266
+ writeUint64(tocView, entryOffset, compressed[i].byteLength);
267
+ writeUint64(tocView, entryOffset + 8, uncompressedSizes[i]);
268
+ }
269
+ const writer = writeStream.getWriter();
270
+ await writer.write(header);
271
+ await writer.write(toc);
272
+ for (const chunk of compressed) {
273
+ await writer.write(chunk);
274
+ }
275
+ await writer.close();
276
+ }
277
+ }
278
+ function getShCounts(shDegree) {
279
+ const shCounts = SH_MAPS[shDegree];
280
+ if (shCounts === undefined) {
281
+ throw new Error(`Unsupported SPZ SH degree: ${shDegree}`);
282
+ }
283
+ return shCounts;
284
+ }
285
+ function createSpzEncodeContext(data, indices, fractionalBits, shCounts) {
286
+ return {
287
+ data,
288
+ indices,
289
+ fractionalBits,
290
+ fraction: 1 << fractionalBits,
291
+ shCounts,
292
+ single: {
293
+ x: 0,
294
+ y: 0,
295
+ z: 0,
296
+ sx: 0,
297
+ sy: 0,
298
+ sz: 0,
299
+ qx: 0,
300
+ qy: 0,
301
+ qz: 0,
302
+ qw: 0,
303
+ r: 0,
304
+ g: 0,
305
+ b: 0,
306
+ a: 0,
307
+ shN: new Array(shCounts),
308
+ },
309
+ };
310
+ }
311
+ function getSpzAttributes(shDegree) {
312
+ return shDegree > 0
313
+ ? ['position', 'alpha', 'color', 'scale', 'quat', 'sh']
314
+ : ['position', 'alpha', 'color', 'scale', 'quat'];
315
+ }
316
+ function getSpzAttributeInfo(attribute, shCounts) {
317
+ switch (attribute) {
318
+ case 'position':
319
+ return { itemSize: 9, chunkSize: 4096 };
320
+ case 'alpha':
321
+ return { itemSize: 1, chunkSize: 65536 };
322
+ case 'color':
323
+ case 'scale':
324
+ return { itemSize: 3, chunkSize: 16384 };
325
+ case 'quat':
326
+ return { itemSize: 4, chunkSize: 16384 };
327
+ case 'sh':
328
+ return { itemSize: shCounts, chunkSize: 1024 };
329
+ }
330
+ }
331
+ function createSpzAttributeChunk(context, attribute, offset, counts) {
332
+ const { data, indices, single, shCounts } = context;
333
+ const { itemSize } = getSpzAttributeInfo(attribute, shCounts);
334
+ const chunk = new Uint8Array(counts * itemSize);
335
+ for (let i = 0; i < counts; i++) {
336
+ const index = indices[offset + i];
337
+ switch (attribute) {
338
+ case 'position': {
339
+ data.getCenter(index, single);
340
+ const o = i * itemSize;
341
+ const ix = clamp(single.x * context.fraction, -0x7fffff, 0x7fffff);
342
+ chunk[o + 0] = ix & 0xff;
343
+ chunk[o + 1] = (ix >> 8) & 0xff;
344
+ chunk[o + 2] = (ix >> 16) & 0xff;
345
+ const iy = clamp(single.y * context.fraction, -0x7fffff, 0x7fffff);
346
+ chunk[o + 3] = iy & 0xff;
347
+ chunk[o + 4] = (iy >> 8) & 0xff;
348
+ chunk[o + 5] = (iy >> 16) & 0xff;
349
+ const iz = clamp(single.z * context.fraction, -0x7fffff, 0x7fffff);
350
+ chunk[o + 6] = iz & 0xff;
351
+ chunk[o + 7] = (iz >> 8) & 0xff;
352
+ chunk[o + 8] = (iz >> 16) & 0xff;
353
+ break;
354
+ }
355
+ case 'alpha':
356
+ data.getAlpha(index, single);
357
+ chunk[i] = clamp(Math.round(single.a * 255), 0, 255);
358
+ break;
359
+ case 'color': {
360
+ data.getColor(index, single);
361
+ const o = i * itemSize;
362
+ chunk[o + 0] = clamp(Math.round(((single.r - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
363
+ chunk[o + 1] = clamp(Math.round(((single.g - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
364
+ chunk[o + 2] = clamp(Math.round(((single.b - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
365
+ break;
366
+ }
367
+ case 'scale': {
368
+ data.getScale(index, single);
369
+ const o = i * itemSize;
370
+ chunk[o + 0] = clamp(Math.round((Math.log(single.sx) + 10) * 16), 0, 255);
371
+ chunk[o + 1] = clamp(Math.round((Math.log(single.sy) + 10) * 16), 0, 255);
372
+ chunk[o + 2] = clamp(Math.round((Math.log(single.sz) + 10) * 16), 0, 255);
373
+ break;
374
+ }
375
+ case 'quat': {
376
+ data.getQuat(index, single);
377
+ const o = i * itemSize;
378
+ rotation[0] = single.qx;
379
+ rotation[1] = single.qy;
380
+ rotation[2] = single.qz;
381
+ rotation[3] = single.qw;
382
+ let iLargest = 0;
383
+ for (let j = 1; j < 4; ++j) {
384
+ if (Math.abs(rotation[j]) > Math.abs(rotation[iLargest])) {
385
+ iLargest = j;
386
+ }
387
+ }
388
+ const negate = rotation[iLargest] < 0 ? 1 : 0;
389
+ let comp = iLargest;
390
+ for (let j = 0; j < 4; ++j) {
391
+ if (j !== iLargest) {
392
+ const negbit = (rotation[j] < 0 ? 1 : 0) ^ negate;
393
+ const mag = Math.floor(((1 << 9) - 1) * (Math.abs(rotation[j]) / Math.SQRT1_2) + 0.5);
394
+ comp = (comp << 10) | (negbit << 9) | mag;
395
+ }
396
+ }
397
+ chunk[o + 0] = comp & 0xff;
398
+ chunk[o + 1] = (comp >> 8) & 0xff;
399
+ chunk[o + 2] = (comp >> 16) & 0xff;
400
+ chunk[o + 3] = (comp >> 24) & 0xff;
401
+ break;
402
+ }
403
+ case 'sh': {
404
+ data.getShN(index, single.shN);
405
+ const o = i * itemSize;
406
+ for (let j = 0; j < itemSize; j++) {
407
+ if (j < 9) {
408
+ chunk[o + j] = clamp(Math.floor((Math.round(single.shN[j] * 128) + 128 + SH_SCALE1 / 2) / SH_SCALE1) * SH_SCALE1, 0, 255);
409
+ continue;
410
+ }
411
+ chunk[o + j] = clamp(Math.floor((Math.round(single.shN[j] * 128) + 128 + SH_SCALE2 / 2) / SH_SCALE2) * SH_SCALE2, 0, 255);
412
+ }
413
+ break;
414
+ }
415
+ }
416
+ }
417
+ return chunk;
418
+ }
419
+ async function writeSpzAttribute(writer, context, attribute) {
420
+ const { chunkSize } = getSpzAttributeInfo(attribute, context.shCounts);
421
+ const chunkCounts = Math.ceil(context.data.counts / chunkSize);
422
+ for (let i = 0; i < chunkCounts; i++) {
423
+ if (writer.desiredSize <= 0) {
424
+ await writer.ready;
425
+ }
426
+ const offset = i * chunkSize;
427
+ const counts = Math.min(chunkSize, context.data.counts - offset);
428
+ writer.write(createSpzAttributeChunk(context, attribute, offset, counts));
429
+ }
430
+ }
431
+ function readUint64(view, offset) {
432
+ const low = view.getUint32(offset, true);
433
+ const high = view.getUint32(offset + 4, true);
434
+ const value = high * 0x100000000 + low;
435
+ if (!Number.isSafeInteger(value)) {
436
+ throw new Error(`SPZ stream size is too large: ${value}`);
437
+ }
438
+ return value;
439
+ }
440
+ function writeUint64(view, offset, value) {
441
+ if (!Number.isSafeInteger(value) || value < 0) {
442
+ throw new Error(`Invalid SPZ stream size: ${value}`);
443
+ }
444
+ view.setUint32(offset, value >>> 0, true);
445
+ view.setUint32(offset + 4, Math.floor(value / 0x100000000), true);
446
+ }
447
+ function createSpzHeader(version, counts, shDegree, fractionalBits, flags, extra, byteLength = 16) {
448
+ const header = new DataView(new ArrayBuffer(byteLength));
449
+ header.setUint32(0, SPZ_MAGIC, true);
450
+ header.setUint32(4, version, true);
451
+ header.setUint32(8, counts, true);
452
+ header.setUint8(12, shDegree);
453
+ header.setUint8(13, fractionalBits);
454
+ header.setUint8(14, flags);
455
+ header.setUint8(15, extra);
456
+ return new Uint8Array(header.buffer);
457
+ }
458
+ function readSpzHeader(view) {
459
+ return {
460
+ version: view.getUint32(4, true),
461
+ counts: view.getUint32(8, true),
462
+ shDegree: view.getUint8(12),
463
+ fractionalBits: view.getUint8(13),
464
+ flags: view.getUint8(14),
465
+ extra: view.getUint8(15),
466
+ };
467
+ }
468
+ function getSpzV4AttributeSizes(counts, shDegree) {
469
+ const shCounts = getShCounts(shDegree);
470
+ const sizes = [
471
+ counts * 9, // position
472
+ counts, // alpha
473
+ counts * 3, // color
474
+ counts * 3, // scale
475
+ counts * 4, // quat
476
+ ];
477
+ if (shDegree > 0) {
478
+ sizes.push(counts * shCounts); // sh
479
+ }
480
+ return sizes;
481
+ }
482
+ function isSpzV4(buffer) {
483
+ if (buffer.byteLength < 8) {
484
+ return false;
485
+ }
486
+ const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
487
+ return view.getUint32(0, true) === SPZ_MAGIC && view.getUint32(4, true) === 4;
488
+ }
489
+ async function readSpzV4Stream(stream, reader, decoder) {
490
+ const read = createExactReader(stream);
491
+ const header = await read(32);
492
+ const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
493
+ const { counts, shDegree, fractionalBits, flags, extra: numStreams } = readSpzHeader(view);
494
+ const tocByteOffset = view.getUint32(16, true);
495
+ const expectedSizes = getSpzV4AttributeSizes(counts, shDegree);
496
+ if (numStreams !== expectedSizes.length) {
497
+ throw new Error(`Invalid SPZ v4 stream count: ${numStreams}`);
498
+ }
499
+ if (tocByteOffset < 32) {
500
+ throw new Error(`Invalid SPZ v4 TOC offset: ${tocByteOffset}`);
501
+ }
502
+ if (tocByteOffset > 32) {
503
+ await read(tocByteOffset - 32);
504
+ }
505
+ const toc = await read(numStreams * 16);
506
+ const tocView = new DataView(toc.buffer, toc.byteOffset, toc.byteLength);
507
+ // Reuse the legacy v3 attribute decoder after parsing the v4 container.
508
+ reader.write(createSpzHeader(SPZ_VERSION, counts, shDegree, fractionalBits, flags & FLAG_ANTIALIASED, 0));
509
+ decoder.flush();
510
+ for (let i = 0; i < numStreams; i++) {
511
+ const entryOffset = i * 16;
512
+ const compressedSize = readUint64(tocView, entryOffset);
513
+ const uncompressedSize = readUint64(tocView, entryOffset + 8);
514
+ if (uncompressedSize !== expectedSizes[i]) {
515
+ throw new Error(`Invalid SPZ v4 stream size at index ${i}`);
516
+ }
517
+ const compressed = await read(compressedSize);
518
+ const decompressed = zstdDecompressSync(compressed, {
519
+ maxOutputLength: uncompressedSize,
520
+ });
521
+ if (decompressed.byteLength !== uncompressedSize) {
522
+ throw new Error(`Invalid SPZ v4 decompressed size at index ${i}`);
523
+ }
524
+ reader.write(new Uint8Array(decompressed.buffer, decompressed.byteOffset, decompressed.byteLength));
525
+ decoder.flush();
526
+ }
527
+ }
528
+ // Return a reader that resolves exactly byteLength bytes and keeps leftover bytes for the next read.
529
+ function createExactReader(stream) {
530
+ const reader = stream.getReader();
531
+ let chunk;
532
+ let chunkOffset = 0;
533
+ return async (byteLength) => {
534
+ const result = new Uint8Array(byteLength);
535
+ let offset = 0;
536
+ while (offset < byteLength) {
537
+ if (!chunk || chunkOffset >= chunk.byteLength) {
538
+ const { done, value } = await reader.read();
539
+ if (done || !value) {
540
+ throw new Error('Invalid SPZ v4 file: stream ended unexpectedly');
541
+ }
542
+ chunk = value;
543
+ chunkOffset = 0;
544
+ }
545
+ const copyLength = Math.min(byteLength - offset, chunk.byteLength - chunkOffset);
546
+ result.set(chunk.subarray(chunkOffset, chunkOffset + copyLength), offset);
547
+ chunkOffset += copyLength;
548
+ offset += copyLength;
549
+ }
550
+ return result;
551
+ };
552
+ }
553
+ // Peek leading bytes for format detection, then replay the consumed chunks through a replacement stream.
554
+ async function peekStream(stream, byteLength) {
555
+ const reader = stream.getReader();
556
+ const chunks = [];
557
+ let size = 0;
558
+ while (size < byteLength) {
559
+ const { done, value } = await reader.read();
560
+ if (done || !value) {
561
+ break;
562
+ }
563
+ chunks.push(value);
564
+ size += value.byteLength;
565
+ }
566
+ const prefix = new Uint8Array(Math.min(size, byteLength));
567
+ let offset = 0;
568
+ for (const chunk of chunks) {
569
+ const copyLength = Math.min(chunk.byteLength, prefix.byteLength - offset);
570
+ prefix.set(chunk.subarray(0, copyLength), offset);
571
+ offset += copyLength;
572
+ if (offset === prefix.byteLength) {
573
+ break;
574
+ }
575
+ }
576
+ return {
577
+ prefix,
578
+ stream: new ReadableStream({
579
+ start(controller) {
580
+ for (const chunk of chunks) {
581
+ controller.enqueue(chunk);
582
+ }
583
+ },
584
+ async pull(controller) {
585
+ const { done, value } = await reader.read();
586
+ if (done) {
587
+ controller.close();
588
+ return;
589
+ }
590
+ controller.enqueue(value);
591
+ },
592
+ cancel(reason) {
593
+ return reader.cancel(reason);
594
+ },
595
+ }),
596
+ };
597
+ }