@driftengine/splats 3.61.0

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 (71) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +56 -0
  4. package/dist/half.d.ts +32 -0
  5. package/dist/half.js +88 -0
  6. package/dist/index.d.ts +32 -0
  7. package/dist/index.js +38 -0
  8. package/dist/shaders/generated/splat.wgsl.d.ts +89 -0
  9. package/dist/shaders/generated/splat.wgsl.js +95 -0
  10. package/dist/shaders/splat.d.ts +25 -0
  11. package/dist/shaders/splat.js +337 -0
  12. package/dist/splat.d.ts +26 -0
  13. package/dist/splat.js +63 -0
  14. package/dist/splatBudget.d.ts +40 -0
  15. package/dist/splatBudget.js +45 -0
  16. package/dist/splatCapture.d.ts +76 -0
  17. package/dist/splatCapture.js +108 -0
  18. package/dist/splatCull.d.ts +25 -0
  19. package/dist/splatCull.js +80 -0
  20. package/dist/splatData.d.ts +177 -0
  21. package/dist/splatData.js +223 -0
  22. package/dist/splatGl.d.ts +49 -0
  23. package/dist/splatGl.js +176 -0
  24. package/dist/splatGpu.d.ts +50 -0
  25. package/dist/splatGpu.js +180 -0
  26. package/dist/splatLayout.d.ts +52 -0
  27. package/dist/splatLayout.js +75 -0
  28. package/dist/splatMatrix.d.ts +29 -0
  29. package/dist/splatMatrix.js +68 -0
  30. package/dist/splatPass.d.ts +83 -0
  31. package/dist/splatPass.js +206 -0
  32. package/dist/splatPly.d.ts +14 -0
  33. package/dist/splatPly.js +242 -0
  34. package/dist/splatSog.d.ts +110 -0
  35. package/dist/splatSog.js +285 -0
  36. package/dist/splatSogDecoder.d.ts +26 -0
  37. package/dist/splatSogDecoder.js +29 -0
  38. package/dist/splatSort.d.ts +137 -0
  39. package/dist/splatSort.js +199 -0
  40. package/dist/splatSortWorker.d.ts +14 -0
  41. package/dist/splatSortWorker.js +137 -0
  42. package/dist/splatSorter.d.ts +112 -0
  43. package/dist/splatSorter.js +231 -0
  44. package/dist/splatView.d.ts +52 -0
  45. package/dist/splatView.js +115 -0
  46. package/package.json +56 -0
  47. package/src/fixtures/README.md +36 -0
  48. package/src/fixtures/cloud.sog +0 -0
  49. package/src/fixtures/cloud.texels.json +27 -0
  50. package/src/fixtures/cloud.truth.json +582 -0
  51. package/src/half.ts +92 -0
  52. package/src/index.ts +55 -0
  53. package/src/shaders/generated/splat.wgsl.ts +98 -0
  54. package/src/shaders/splat.ts +344 -0
  55. package/src/splat.ts +75 -0
  56. package/src/splatBudget.ts +48 -0
  57. package/src/splatCapture.ts +154 -0
  58. package/src/splatCull.ts +91 -0
  59. package/src/splatData.ts +398 -0
  60. package/src/splatGl.ts +262 -0
  61. package/src/splatGpu.ts +259 -0
  62. package/src/splatLayout.ts +86 -0
  63. package/src/splatMatrix.ts +81 -0
  64. package/src/splatPass.ts +324 -0
  65. package/src/splatPly.ts +283 -0
  66. package/src/splatSog.ts +375 -0
  67. package/src/splatSogDecoder.ts +33 -0
  68. package/src/splatSort.ts +296 -0
  69. package/src/splatSortWorker.ts +155 -0
  70. package/src/splatSorter.ts +285 -0
  71. package/src/splatView.ts +147 -0
@@ -0,0 +1,296 @@
1
+ /** A counting sort over a sixteen-bit depth key: no comparisons, one pass, far to near, budgeted. */
2
+
3
+ /** How many buckets the depth key has. A sixteen-bit key, so 65,536 of them. */
4
+ export const SPLAT_SORT_BUCKETS = 65536;
5
+
6
+ /**
7
+ * How many buckets the screen-space size key has.
8
+ *
9
+ * The key is the top sixteen bits of a positive float's own bit pattern, which reaches 32,640 for
10
+ * an infinite ratio — so this is the next power of two above that rather than a number with a
11
+ * meaning of its own. See `sortSplatsByDepth` for why those bits are a usable key at all.
12
+ */
13
+ export const SPLAT_SIZE_BUCKETS = 32768;
14
+
15
+ /**
16
+ * One sort, as the worker protocol and as the function's own parameter list.
17
+ *
18
+ * **One object rather than eleven arguments, and the worker is the reason.** `createSplatSortWorker`
19
+ * posts this and hands `event.data` straight to the sort, so the message shape and the call shape
20
+ * cannot disagree — which is the failure the 2026-08-17 rule is about, in the one place here where
21
+ * a decision genuinely does cross a thread boundary.
22
+ */
23
+ export interface SplatSortRequest {
24
+ /** Three per splat, in the capture's own space. */
25
+ readonly positions: Float32Array;
26
+ /** One per splat: `SplatData.extents`, the largest sigma. Only read when there is a budget. */
27
+ readonly extents: Float32Array;
28
+ readonly count: number;
29
+ /**
30
+ * The camera's forward **in the capture's own space**, unit length.
31
+ *
32
+ * Not the world-space forward: a batch with a model matrix is looked at from a different
33
+ * direction in its own frame, and `resolveSplatView` is what converts one to the other. One
34
+ * sorter therefore serves one batch and two batches need no merged order.
35
+ */
36
+ readonly dirX: number;
37
+ readonly dirY: number;
38
+ readonly dirZ: number;
39
+ /**
40
+ * The camera's position in the capture's own space.
41
+ *
42
+ * **Invisible in the order and load-bearing in the budget.** Shifting every splat by the same
43
+ * amount cannot reorder them, so depth alone never needed this; the size key is `extent` over
44
+ * the *distance*, and a distance needs a point to measure from.
45
+ */
46
+ readonly originX: number;
47
+ readonly originY: number;
48
+ readonly originZ: number;
49
+ /** The most splats to keep. Zero, or anything at or above `count`, keeps all of them. */
50
+ readonly budget: number;
51
+ /** The buffer to fill and hand back, so the pair can ping-pong rather than allocate. */
52
+ readonly out: Uint32Array;
53
+ }
54
+
55
+ /**
56
+ * What a sort answers with: the filled buffer, and how many entries of it are real.
57
+ *
58
+ * **The count is not the request's count once a budget bites**, and it comes back rather than
59
+ * being recomputed, because only the sort knows how its ration of the straddling size bucket
60
+ * fell out. A caller that assumed the capture's own count would walk off the end of a valid order
61
+ * into whatever the buffer held before — stale indices drawn as splats, which reads as a corrupt
62
+ * capture rather than as an off-by-one.
63
+ */
64
+ export interface SplatSortResult {
65
+ /** The buffer that was handed in, or the same storage after a worker transferred it twice. */
66
+ readonly order: Uint32Array;
67
+ readonly count: number;
68
+ }
69
+
70
+ /**
71
+ * How a sort actually happens.
72
+ *
73
+ * **A capability the caller may replace, which is `AGENTS.md`'s rule about platform APIs and not a
74
+ * convenience.** The shipped default is a worker; a consumer under a strict CSP, in a test, or on
75
+ * a runtime with no `Worker` at all supplies its own and nothing here has to know.
76
+ */
77
+ export type SplatSortFn = (request: SplatSortRequest) => Promise<SplatSortResult>;
78
+
79
+ /**
80
+ * The arrays a sort reuses, allocated once by the caller.
81
+ *
82
+ * **A sort allocates nothing**, which is the whole reason this is a parameter rather than a local:
83
+ * the two histograms are 384 KB between them, and a capture re-sorts whenever the view turns. Doing
84
+ * that in a worker does not make an allocation free — it makes it somebody else's garbage.
85
+ */
86
+ export interface SplatSortScratch {
87
+ /** The depth bucket of each splat, carried from the counting pass to the scatter. */
88
+ readonly keys: Uint16Array;
89
+ /** The depth histogram, then the prefix sum over it. */
90
+ readonly counts: Uint32Array;
91
+ /** The size bucket of each splat, overwritten by the counting pass with whether it survived. */
92
+ readonly sizes: Uint16Array;
93
+ /** The size histogram, walked from the top to find the budget's threshold. */
94
+ readonly sizeCounts: Uint32Array;
95
+ /**
96
+ * Two views of one four-byte buffer, for reading a float's bits without allocating a view.
97
+ *
98
+ * A pair rather than a `DataView` because this is read once per splat per sort and the typed
99
+ * pair is one store and one load. It is in the scratch rather than at module scope for the same
100
+ * reason everything else here is: the sort is stringified into a worker and closes over nothing.
101
+ */
102
+ readonly bits: Float32Array;
103
+ readonly bitsAsUint: Uint32Array;
104
+ }
105
+
106
+ export function createSplatSortScratch(count: number): SplatSortScratch {
107
+ const bits = new ArrayBuffer(4);
108
+ return {
109
+ keys: new Uint16Array(Math.max(1, count)),
110
+ counts: new Uint32Array(SPLAT_SORT_BUCKETS),
111
+ sizes: new Uint16Array(Math.max(1, count)),
112
+ sizeCounts: new Uint32Array(SPLAT_SIZE_BUCKETS),
113
+ bits: new Float32Array(bits),
114
+ bitsAsUint: new Uint32Array(bits),
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Order the splats worth drawing far to near, writing indices into `request.out`.
120
+ *
121
+ * Returns how many were written, which is `count` unless a budget cut it.
122
+ *
123
+ * **Far to near, and the direction is the one thing a test can catch that a screenshot cannot.**
124
+ * A near-to-far order looks *plausible* — the cloud is still a cloud — and is wrong at every
125
+ * silhouette, because `over` compositing is not commutative. `dir` is the camera's forward in the
126
+ * capture's own space, so a larger projection along it is farther away and this sorts descending.
127
+ *
128
+ * **A counting sort rather than a comparison sort**, which is what makes it affordable at a
129
+ * million splats: one pass for the range, one to bucket, one prefix sum, one to scatter, all
130
+ * linear. `Array.prototype.sort` on a million indices is tens of milliseconds and allocates.
131
+ *
132
+ * **Sixteen bits is 65,536 distinct depths across the capture's whole extent.** At a capture ten
133
+ * metres deep that is 0.15 mm a bucket, far below what any ordering error could show. What it
134
+ * gives up is exactness: two splats inside one bucket keep their input order rather than their
135
+ * true order, which is a tie broken arbitrarily and invisible. What would make it wrong is a
136
+ * capture whose depth range is dominated by one distant outlier, which squeezes everything else
137
+ * into a handful of buckets — the same failure a histogram always has, and the reason the range
138
+ * is measured rather than assumed.
139
+ *
140
+ * **A budget selects by screen-space size, and taking a prefix of the order instead would be
141
+ * wrong in a way that looks deliberate.** The order is far to near, so its front is the far end:
142
+ * `splatBudget.test.ts` measures that, rather than leaving it to be recalled. A prefix therefore
143
+ * keeps the distant half of a capture and throws away everything close to the camera — the splats
144
+ * covering the most pixels, and most of what a viewer is looking at. A suffix inverts the mistake
145
+ * and drops the backdrop. Neither is a budget; both are a capture with a piece missing.
146
+ *
147
+ * So the splats kept are the ones largest **on screen**, which is the extent over the distance,
148
+ * and the ones dropped are by construction smaller than every one kept. What that gives up is
149
+ * stability: as the camera closes on a capture, splats cross the threshold and appear, which is
150
+ * a pop bounded by the size of the smallest splat still being drawn — around a pixel at any
151
+ * budget worth setting, and unmissable at a budget of a few thousand. What would make it wrong is
152
+ * a capture of very flat splats seen edge-on, where the largest sigma over-estimates the pixels
153
+ * covered; `SplatData.extents` carries that same caveat, because it is the same approximation.
154
+ */
155
+ export function sortSplatsByDepth(request: SplatSortRequest, scratch: SplatSortScratch): number {
156
+ const { positions, extents, count, dirX, dirY, dirZ, originX, originY, originZ, out } = request;
157
+ if (count <= 0) return 0;
158
+ const { keys, counts, sizes, sizeCounts, bits, bitsAsUint } = scratch;
159
+ /*
160
+ * **The bucket counts come from the scratch, not from the module constants, and that is what
161
+ * lets this function be stringified into a worker.** `createSplatSortWorker` ships
162
+ * `sortSplatsByDepth.toString()` so there is one implementation of the ordering rather than two
163
+ * — and a stringified function loses its scope, so any module-scope binding it referenced would
164
+ * be an undefined identifier the moment a bundler renamed or dropped it. Reading `.length`
165
+ * closes over nothing. `splatSort.test.ts` asserts the source is free of such references.
166
+ */
167
+ const buckets = counts.length;
168
+ /* Normalised to zero, so every test below is against one number rather than against two. */
169
+ const budget = request.budget > 0 && request.budget < count ? request.budget : 0;
170
+
171
+ let min = Infinity;
172
+ let max = -Infinity;
173
+ if (budget > 0) sizeCounts.fill(0);
174
+ for (let index = 0; index < count; index++) {
175
+ const p = index * 3;
176
+ const depth =
177
+ ((positions[p] ?? 0) - originX) * dirX +
178
+ ((positions[p + 1] ?? 0) - originY) * dirY +
179
+ ((positions[p + 2] ?? 0) - originZ) * dirZ;
180
+ if (depth < min) min = depth;
181
+ if (depth > max) max = depth;
182
+
183
+ if (budget > 0) {
184
+ /*
185
+ * The screen-space radius is the focal length times `extent / depth`, and the focal length
186
+ * is the same for every splat in a frame — so it drops out of a ranking and this is the
187
+ * whole key.
188
+ *
189
+ * **The key is the top sixteen bits of the ratio's own float bits**, which for a positive
190
+ * float is exactly monotone: IEEE 754 lays out sign, then exponent, then mantissa, so a
191
+ * larger positive float has a larger bit pattern, and taking the high half keeps the sign,
192
+ * all eight exponent bits and seven of the mantissa. That is a logarithmic quantisation —
193
+ * 128 buckets an octave, so a threshold lands within half a percent of the ideal size cut —
194
+ * at the cost of one store and one load rather than a `Math.log2` per splat per sort. What
195
+ * would make it wrong is a negative key, which cannot happen: the guard below sends
196
+ * everything that is not a positive ratio to bucket zero.
197
+ *
198
+ * A splat at or behind the camera has no screen-space size, so `depth <= 0` fails
199
+ * `ratio > 0` and lands in bucket zero, where the budget drops it first. So does a `NaN`,
200
+ * because every comparison against one is false — which is the honest answer for a splat
201
+ * whose size cannot be computed.
202
+ */
203
+ const ratio = (extents[index] ?? 0) / depth;
204
+ let key = 0;
205
+ if (ratio > 0) {
206
+ bits[0] = ratio;
207
+ key = (bitsAsUint[0] ?? 0) >>> 16;
208
+ }
209
+ sizes[index] = key;
210
+ sizeCounts[key] = (sizeCounts[key] ?? 0) + 1;
211
+ }
212
+ }
213
+
214
+ /*
215
+ * The threshold, and the quota that trims the bucket straddling it.
216
+ *
217
+ * Walking down from the largest size, `above` is everything strictly bigger than `threshold`
218
+ * and fits inside the budget by construction. The bucket that would overflow it is *included*
219
+ * and then rationed: `quota` splats from it are admitted in index order and the rest are not.
220
+ * Without that ration a capture whose splats are all one size — which is most synthetic ones,
221
+ * and any capture from a fixed-scale exporter — would put every splat in one bucket and the
222
+ * threshold alone would answer either everything or nothing. Nothing is a blank screen on
223
+ * exactly the device the budget exists for.
224
+ */
225
+ let threshold = 0;
226
+ let quota = 0;
227
+ if (budget > 0) {
228
+ let above = 0;
229
+ for (let bucket = sizeCounts.length - 1; bucket >= 0; bucket--) {
230
+ const howMany = sizeCounts[bucket] ?? 0;
231
+ threshold = bucket;
232
+ if (above + howMany > budget) break;
233
+ above += howMany;
234
+ }
235
+ quota = budget - above;
236
+ }
237
+
238
+ /*
239
+ * A capture with no depth range at all — one splat, or a plane exactly side-on. Every key would
240
+ * be a division by zero, so the scale is zero and every splat lands in bucket 0 in input order.
241
+ * That is a legal answer: with no range there is no ordering to get wrong.
242
+ *
243
+ * The range spans every splat rather than only the ones inside the budget, because it is
244
+ * measured before the threshold is known. What that costs is depth resolution when a budget
245
+ * discards a distant tail; at 65,536 buckets there is a great deal to spare.
246
+ */
247
+ const range = max - min;
248
+ const scale = range > 0 ? (buckets - 1) / range : 0;
249
+
250
+ counts.fill(0);
251
+ let admitted = 0;
252
+ let remaining = quota;
253
+ for (let index = 0; index < count; index++) {
254
+ if (budget > 0) {
255
+ const size = sizes[index] ?? 0;
256
+ const keep = size > threshold || (size === threshold && remaining > 0);
257
+ if (keep && size === threshold) remaining--;
258
+ /* Overwritten with the verdict, so the scatter admits exactly this set without re-running
259
+ the ration. The size itself is not wanted again. */
260
+ sizes[index] = keep ? 1 : 0;
261
+ if (!keep) continue;
262
+ }
263
+ const p = index * 3;
264
+ const depth =
265
+ ((positions[p] ?? 0) - originX) * dirX +
266
+ ((positions[p + 1] ?? 0) - originY) * dirY +
267
+ ((positions[p + 2] ?? 0) - originZ) * dirZ;
268
+ /*
269
+ * Inverted, so that an *ascending* bucket walk comes out far to near. Doing it here rather
270
+ * than by walking the histogram backwards keeps the scatter a plain forward loop, and a
271
+ * backwards scatter is where a stable sort quietly stops being stable.
272
+ */
273
+ const bucket = buckets - 1 - Math.round((depth - min) * scale);
274
+ keys[index] = bucket;
275
+ counts[bucket] = (counts[bucket] ?? 0) + 1;
276
+ admitted++;
277
+ }
278
+
279
+ /* Prefix sum in place: each bucket becomes where its first splat goes. */
280
+ let running = 0;
281
+ for (let bucket = 0; bucket < buckets; bucket++) {
282
+ const howMany = counts[bucket] ?? 0;
283
+ counts[bucket] = running;
284
+ running += howMany;
285
+ }
286
+
287
+ for (let index = 0; index < count; index++) {
288
+ if (budget > 0 && sizes[index] === 0) continue;
289
+ const bucket = keys[index] ?? 0;
290
+ const at = counts[bucket] ?? 0;
291
+ out[at] = index;
292
+ counts[bucket] = at + 1;
293
+ }
294
+
295
+ return admitted;
296
+ }
@@ -0,0 +1,155 @@
1
+ /** The shipped sort capability: a worker built from a Blob, falling back to the main thread aloud. */
2
+
3
+ import { sortOnMainThread } from './splatSorter.ts';
4
+ import { SPLAT_SIZE_BUCKETS, SPLAT_SORT_BUCKETS } from './splatSort.ts';
5
+ import type { SplatSortFn, SplatSortRequest, SplatSortResult } from './splatSort.ts';
6
+
7
+ /**
8
+ * The sort, as source, for a worker that has no module graph to import from.
9
+ *
10
+ * **Stringified rather than a second file, and that is the no-build-step contract.** This engine
11
+ * ships TypeScript source and consumers bundle it; `new Worker(new URL('./x.ts', import.meta.url))`
12
+ * asks every consumer's bundler to know about a worker entry point, which is a build step by
13
+ * another name. A `Blob` needs nothing of anybody.
14
+ *
15
+ * **What it costs is a copy of the sort that no test can reach**, so the two could drift — which is
16
+ * exactly what the 2026-08-17 rule is about. It does not drift here because it is not written
17
+ * twice: `sortSplatsByDepth.toString()` is interpolated below, so the worker runs the same
18
+ * function the main thread does, and a change to that function changes both. What would make it
19
+ * wrong is that function closing over a module-scope binding, since a stringified closure loses
20
+ * its scope — which is why it takes its scratch as a parameter and its bucket counts as part of it.
21
+ *
22
+ * **The capture is cached in the worker and re-sent only when it changes.** Positions are twelve
23
+ * bytes a splat and extents four, so posting them with every sort would structured-clone sixteen
24
+ * megabytes for a million-splat capture — several times a second while a viewer turns, on the
25
+ * device this whole arrangement exists for. The sender omits them when the capture is the one the
26
+ * worker already holds, and `null` is what says so.
27
+ */
28
+ function workerSource(sortSource: string): string {
29
+ return `
30
+ const SPLAT_SORT_BUCKETS = ${SPLAT_SORT_BUCKETS};
31
+ const SPLAT_SIZE_BUCKETS = ${SPLAT_SIZE_BUCKETS};
32
+ const sortSplatsByDepth = ${sortSource};
33
+ let scratch = null;
34
+ let positions = null;
35
+ let extents = null;
36
+ self.onmessage = (event) => {
37
+ const request = event.data;
38
+ if (request.positions !== null) positions = request.positions;
39
+ if (request.extents !== null) extents = request.extents;
40
+ const count = request.count;
41
+ if (scratch === null || scratch.keys.length < count) {
42
+ const bits = new ArrayBuffer(4);
43
+ scratch = {
44
+ keys: new Uint16Array(Math.max(1, count)),
45
+ counts: new Uint32Array(SPLAT_SORT_BUCKETS),
46
+ sizes: new Uint16Array(Math.max(1, count)),
47
+ sizeCounts: new Uint32Array(SPLAT_SIZE_BUCKETS),
48
+ bits: new Float32Array(bits),
49
+ bitsAsUint: new Uint32Array(bits),
50
+ };
51
+ }
52
+ request.positions = positions;
53
+ request.extents = extents;
54
+ const kept = sortSplatsByDepth(request, scratch);
55
+ /* Transferred back, so the pair ping-pongs rather than allocating a result a frame. */
56
+ self.postMessage({ order: request.out, count: kept }, [request.out.buffer]);
57
+ };
58
+ `;
59
+ }
60
+
61
+ /**
62
+ * Build the default sort capability.
63
+ *
64
+ * **Falls back to the main thread aloud, once.** A strict CSP without `worker-src blob:` refuses
65
+ * the construction, and so does any runtime with no `Worker` at all. A capture that stutters is
66
+ * better than one that does not draw — and a *silent* fallback is what `capabilityClamp` exists to
67
+ * prevent, because the symptom is then a frame rate nobody can attribute.
68
+ *
69
+ * The sort's source is passed in rather than imported here so this module has no opinion about
70
+ * where the function lives; `createDefaultSplatSort` supplies it.
71
+ */
72
+ export function createSplatSortWorker(sortSource: string): SplatSortFn {
73
+ let worker: Worker | null = null;
74
+ let warned = false;
75
+
76
+ const fallback = (reason: string): SplatSortFn => {
77
+ if (!warned) {
78
+ warned = true;
79
+ console.warn(
80
+ `splats: sorting on the main thread — ${reason}. A capture will stutter while the view ` +
81
+ 'turns. Supply your own `sort` capability to put it somewhere else.',
82
+ );
83
+ }
84
+ return sortOnMainThread;
85
+ };
86
+
87
+ try {
88
+ if (
89
+ typeof Worker === 'undefined' ||
90
+ typeof Blob === 'undefined' ||
91
+ typeof URL === 'undefined'
92
+ ) {
93
+ return fallback('this runtime has no Worker');
94
+ }
95
+ const url = URL.createObjectURL(
96
+ new Blob([workerSource(sortSource)], { type: 'text/javascript' }),
97
+ );
98
+ worker = new Worker(url);
99
+ /* The blob is referenced by the live worker; the URL is only the handle. */
100
+ URL.revokeObjectURL(url);
101
+ } catch (error) {
102
+ return fallback(String((error as Error).message ?? error));
103
+ }
104
+
105
+ const live = worker;
106
+ /* Identity, not a copy: `SplatData` owns these arrays for its lifetime, so the same reference
107
+ arriving twice is exactly the statement that the worker's copy is still current. */
108
+ let sentPositions: Float32Array | null = null;
109
+ let sentExtents: Float32Array | null = null;
110
+
111
+ return (request: SplatSortRequest): Promise<SplatSortResult> =>
112
+ new Promise<SplatSortResult>((resolve, reject) => {
113
+ const onMessage = (event: MessageEvent): void => {
114
+ live.removeEventListener('message', onMessage);
115
+ live.removeEventListener('error', onError);
116
+ resolve(event.data as SplatSortResult);
117
+ };
118
+ const onError = (event: ErrorEvent): void => {
119
+ live.removeEventListener('message', onMessage);
120
+ live.removeEventListener('error', onError);
121
+ /* The worker never learned this capture, so the next attempt must send it again. */
122
+ sentPositions = null;
123
+ sentExtents = null;
124
+ reject(new Error(`splats: the sort worker failed — ${event.message}`));
125
+ };
126
+ live.addEventListener('message', onMessage);
127
+ live.addEventListener('error', onError);
128
+
129
+ const positions = request.positions === sentPositions ? null : request.positions;
130
+ const extents = request.extents === sentExtents ? null : request.extents;
131
+ sentPositions = request.positions;
132
+ sentExtents = request.extents;
133
+ /*
134
+ * `out` is transferred and the capture is copied at most once. Copying rather than
135
+ * transferring the capture is the price of not owning it: the positions belong to the
136
+ * `SplatData` and the main thread reads them for a fallback sort.
137
+ */
138
+ live.postMessage(
139
+ {
140
+ positions,
141
+ extents,
142
+ count: request.count,
143
+ dirX: request.dirX,
144
+ dirY: request.dirY,
145
+ dirZ: request.dirZ,
146
+ originX: request.originX,
147
+ originY: request.originY,
148
+ originZ: request.originZ,
149
+ budget: request.budget,
150
+ out: request.out,
151
+ },
152
+ [request.out.buffer],
153
+ );
154
+ });
155
+ }