@tinypoly/gpu-renderer 0.0.0 → 0.0.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/src/bvh.ts CHANGED
@@ -35,47 +35,6 @@ export interface PackedBvh {
35
35
  maxDepth: number;
36
36
  }
37
37
 
38
- interface Range {
39
- start: number;
40
- count: number;
41
- node: number;
42
- depth: number;
43
- }
44
-
45
- /** Axis aligned bounds under construction, in doubles so the sweep does not accumulate rounding. */
46
- class Bounds {
47
- min = [Infinity, Infinity, Infinity];
48
- max = [-Infinity, -Infinity, -Infinity];
49
-
50
- reset() {
51
- this.min = [Infinity, Infinity, Infinity];
52
- this.max = [-Infinity, -Infinity, -Infinity];
53
- }
54
-
55
- addPoint(x: number, y: number, z: number) {
56
- const point = [x, y, z];
57
-
58
- for (let axis = 0; axis < 3; axis++) {
59
- if (point[axis] < this.min[axis]) this.min[axis] = point[axis];
60
- if (point[axis] > this.max[axis]) this.max[axis] = point[axis];
61
- }
62
- }
63
-
64
- addBounds(other: Bounds) {
65
- for (let axis = 0; axis < 3; axis++) {
66
- if (other.min[axis] < this.min[axis]) this.min[axis] = other.min[axis];
67
- if (other.max[axis] > this.max[axis]) this.max[axis] = other.max[axis];
68
- }
69
- }
70
-
71
- /** Surface area, the cost the split heuristic weighs each side by. Empty bounds cost nothing. */
72
- area(): number {
73
- const x = this.max[0] - this.min[0], y = this.max[1] - this.min[1], z = this.max[2] - this.min[2];
74
-
75
- return x < 0 || y < 0 || z < 0 ? 0 : 2 * (x * y + y * z + z * x);
76
- }
77
- }
78
-
79
38
  /** Per-triangle bounds and centroids, the only inputs the split needs. */
80
39
  function triangleBounds(positions: Float32Array, triangleCount: number) {
81
40
  const min = new Float32Array(triangleCount * 3);
@@ -123,41 +82,81 @@ export function buildBvh(positions: Float32Array, triangleCount: number): Packed
123
82
  ints = new Int32Array(nodes);
124
83
  };
125
84
 
126
- const stack: Range[] = [{ start: 0, count: triangleCount, node: 0, depth: 0 }];
127
- const bounds = new Bounds();
85
+ // The pending ranges, four integers each: start, count, node and depth. Typed, so the loop allocates nothing.
86
+ let stack = new Int32Array(4 * 2 * MAX_BVH_DEPTH + 8);
87
+ let top = 1;
88
+ const bins = new Bins();
128
89
  let nodeCount = 1;
129
90
  let maxDepth = 0;
130
91
 
131
- while (stack.length) {
132
- const range = stack.pop() as Range;
92
+ stack[0] = 0;
93
+ stack[1] = triangleCount;
94
+ stack[2] = 0;
95
+ stack[3] = 0;
133
96
 
134
- maxDepth = Math.max(maxDepth, range.depth);
135
- bounds.reset();
97
+ while (top) {
98
+ top--;
136
99
 
137
- for (let i = range.start; i < range.start + range.count; i++) {
138
- const triangle = order[i] * 3;
100
+ const start = stack[top * 4], count = stack[top * 4 + 1], node = stack[top * 4 + 2], depth = stack[top * 4 + 3];
101
+ const end = start + count;
139
102
 
140
- bounds.addPoint(min[triangle], min[triangle + 1], min[triangle + 2]);
141
- bounds.addPoint(max[triangle], max[triangle + 1], max[triangle + 2]);
142
- }
103
+ if (depth > maxDepth) maxDepth = depth;
143
104
 
144
- const word = range.node * NODE_WORDS;
105
+ // The node's bounds and its centroids' bounds, in one pass. Doubles, so the corners are exactly the floats read.
106
+ let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
107
+ let cMinX = Infinity, cMinY = Infinity, cMinZ = Infinity, cMaxX = -Infinity, cMaxY = -Infinity, cMaxZ = -Infinity;
145
108
 
146
- floats[word] = bounds.min[0];
147
- floats[word + 1] = bounds.min[1];
148
- floats[word + 2] = bounds.min[2];
149
- floats[word + 4] = bounds.max[0];
150
- floats[word + 5] = bounds.max[1];
151
- floats[word + 6] = bounds.max[2];
109
+ for (let i = start; i < end; i++) {
110
+ const triangle = order[i] * 3;
111
+ let value = min[triangle];
112
+
113
+ if (value < minX) minX = value;
114
+ value = min[triangle + 1];
115
+ if (value < minY) minY = value;
116
+ value = min[triangle + 2];
117
+ if (value < minZ) minZ = value;
118
+ value = max[triangle];
119
+ if (value > maxX) maxX = value;
120
+ value = max[triangle + 1];
121
+ if (value > maxY) maxY = value;
122
+ value = max[triangle + 2];
123
+ if (value > maxZ) maxZ = value;
124
+ value = centroid[triangle];
125
+ if (value < cMinX) cMinX = value;
126
+ if (value > cMaxX) cMaxX = value;
127
+ value = centroid[triangle + 1];
128
+ if (value < cMinY) cMinY = value;
129
+ if (value > cMaxY) cMaxY = value;
130
+ value = centroid[triangle + 2];
131
+ if (value < cMinZ) cMinZ = value;
132
+ if (value > cMaxZ) cMaxZ = value;
133
+ }
152
134
 
153
- const split = range.count > MAX_LEAF_SIZE && range.depth + 1 < MAX_BVH_DEPTH
154
- ? partition(order, centroid, min, max, range, bounds.area())
155
- : -1;
135
+ const word = node * NODE_WORDS;
136
+
137
+ floats[word] = minX;
138
+ floats[word + 1] = minY;
139
+ floats[word + 2] = minZ;
140
+ floats[word + 4] = maxX;
141
+ floats[word + 5] = maxY;
142
+ floats[word + 6] = maxZ;
143
+
144
+ let split = -1;
145
+
146
+ if (count > MAX_LEAF_SIZE && depth + 1 < MAX_BVH_DEPTH) {
147
+ bins.centroidMin[0] = cMinX;
148
+ bins.centroidMin[1] = cMinY;
149
+ bins.centroidMin[2] = cMinZ;
150
+ bins.centroidMax[0] = cMaxX;
151
+ bins.centroidMax[1] = cMaxY;
152
+ bins.centroidMax[2] = cMaxZ;
153
+ split = partition(order, centroid, min, max, start, end, area(minX, minY, minZ, maxX, maxY, maxZ), bins);
154
+ }
156
155
 
157
156
  if (split < 0) {
158
157
  // A leaf points into `order` instead of into the node array, which the sign of the first link tells apart.
159
- ints[word + 3] = -1 - range.start;
160
- ints[word + 7] = range.count;
158
+ ints[word + 3] = -1 - start;
159
+ ints[word + 7] = count;
161
160
  continue;
162
161
  }
163
162
 
@@ -168,92 +167,157 @@ export function buildBvh(positions: Float32Array, triangleCount: number): Packed
168
167
 
169
168
  ints[word + 3] = left;
170
169
  ints[word + 7] = right;
171
- stack.push({ start: range.start, count: split - range.start, node: left, depth: range.depth + 1 });
172
- stack.push({ start: split, count: range.start + range.count - split, node: right, depth: range.depth + 1 });
170
+
171
+ if ((top + 2) * 4 > stack.length) {
172
+ const grown = new Int32Array(stack.length * 2);
173
+
174
+ grown.set(stack);
175
+ stack = grown;
176
+ }
177
+
178
+ // Left first, then right, so the right side is built first, as the order the tree was always built in.
179
+ stack.set([start, split - start, left, depth + 1, split, end - split, right, depth + 1], top * 4);
180
+ top += 2;
173
181
  }
174
182
 
175
183
  return { nodes: nodes.slice(0, nodeCount * NODE_WORDS * 4), nodeCount, order, maxDepth };
176
184
  }
177
185
 
186
+ /** Surface area, the cost the split heuristic weighs each side by. Empty bounds cost nothing. */
187
+ function area(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number): number {
188
+ const x = maxX - minX, y = maxY - minY, z = maxZ - minZ;
189
+
190
+ return x < 0 || y < 0 || z < 0 ? 0 : 2 * (x * y + y * z + z * x);
191
+ }
192
+
193
+ /** Scratch space for one split, kept across every node so binning allocates nothing. */
194
+ class Bins {
195
+ readonly centroidMin = new Float64Array(3);
196
+ readonly centroidMax = new Float64Array(3);
197
+ /** Six doubles per bin and axis: the minimum corner, then the maximum. */
198
+ readonly bounds = new Float64Array(3 * BIN_COUNT * 6);
199
+ readonly counts = new Int32Array(3 * BIN_COUNT);
200
+ readonly rightArea = new Float64Array(BIN_COUNT);
201
+ readonly rightCount = new Int32Array(BIN_COUNT);
202
+ readonly scale = new Float64Array(3);
203
+
204
+ reset() {
205
+ const bounds = this.bounds;
206
+
207
+ for (let i = 0; i < bounds.length; i += 6) {
208
+ bounds[i] = bounds[i + 1] = bounds[i + 2] = Infinity;
209
+ bounds[i + 3] = bounds[i + 4] = bounds[i + 5] = -Infinity;
210
+ }
211
+
212
+ this.counts.fill(0);
213
+ }
214
+ }
215
+
178
216
  /**
179
217
  * Reorders `order` in place so the range splits in two, and returns where the right side starts, or -1 when no split
180
- * beats keeping the range as a leaf. Bins the centroids on all three axes and sweeps the surface area heuristic.
218
+ * beats keeping the range as a leaf. Bins the centroids on all three axes in one pass and sweeps the surface area
219
+ * heuristic on each.
181
220
  */
182
221
  function partition(order: Uint32Array, centroid: Float64Array, min: Float32Array, max: Float32Array,
183
- range: Range, parentArea: number): number {
222
+ start: number, end: number, parentArea: number, bins: Bins): number {
184
223
  if (parentArea <= 0) return -1;
185
224
 
186
- const end = range.start + range.count;
187
- const centroidBounds = new Bounds();
225
+ const { centroidMin, centroidMax, bounds, counts, rightArea, rightCount, scale } = bins;
226
+ let binned = false;
188
227
 
189
- for (let i = range.start; i < end; i++) {
190
- const triangle = order[i] * 3;
228
+ for (let axis = 0; axis < 3; axis++) {
229
+ const extent = centroidMax[axis] - centroidMin[axis];
191
230
 
192
- centroidBounds.addPoint(centroid[triangle], centroid[triangle + 1], centroid[triangle + 2]);
231
+ // Every centroid sits at the same place on this axis: no split here separates anything.
232
+ scale[axis] = extent > 0 ? BIN_COUNT / extent : 0;
233
+ binned ||= extent > 0;
193
234
  }
194
235
 
195
- // Splitting costs a traversal step, so a split only wins when it beats shading the whole range as one leaf.
196
- let bestCost = range.count, bestAxis = -1, bestBin = -1, bestScale = 0;
236
+ if (!binned) return -1;
197
237
 
198
- for (let axis = 0; axis < 3; axis++) {
199
- const extent = centroidBounds.max[axis] - centroidBounds.min[axis];
238
+ bins.reset();
200
239
 
201
- // Every centroid sits at the same place on this axis: no split here separates anything.
202
- if (extent <= 0) continue;
240
+ for (let i = start; i < end; i++) {
241
+ const triangle = order[i] * 3;
242
+ const x0 = min[triangle], y0 = min[triangle + 1], z0 = min[triangle + 2];
243
+ const x1 = max[triangle], y1 = max[triangle + 1], z1 = max[triangle + 2];
203
244
 
204
- const scale = BIN_COUNT / extent;
205
- const binBounds = Array.from({ length: BIN_COUNT }, () => new Bounds());
206
- const binCounts = new Int32Array(BIN_COUNT);
245
+ for (let axis = 0; axis < 3; axis++) {
246
+ if (scale[axis] === 0) continue;
207
247
 
208
- for (let i = range.start; i < end; i++) {
209
- const triangle = order[i] * 3;
210
- const bin = binOf(centroid[triangle + axis], centroidBounds.min[axis], scale);
248
+ const bin = axis * BIN_COUNT + binOf(centroid[triangle + axis], centroidMin[axis], scale[axis]);
249
+ const at = bin * 6;
250
+
251
+ counts[bin]++;
211
252
 
212
- binCounts[bin]++;
213
- binBounds[bin].addPoint(min[triangle], min[triangle + 1], min[triangle + 2]);
214
- binBounds[bin].addPoint(max[triangle], max[triangle + 1], max[triangle + 2]);
253
+ if (x0 < bounds[at]) bounds[at] = x0;
254
+ if (y0 < bounds[at + 1]) bounds[at + 1] = y0;
255
+ if (z0 < bounds[at + 2]) bounds[at + 2] = z0;
256
+ if (x1 > bounds[at + 3]) bounds[at + 3] = x1;
257
+ if (y1 > bounds[at + 4]) bounds[at + 4] = y1;
258
+ if (z1 > bounds[at + 5]) bounds[at + 5] = z1;
215
259
  }
260
+ }
261
+
262
+ // Splitting costs a traversal step, so a split only wins when it beats shading the whole range as one leaf.
263
+ let bestCost = end - start, bestAxis = -1, bestBin = -1;
264
+
265
+ for (let axis = 0; axis < 3; axis++) {
266
+ if (scale[axis] === 0) continue;
216
267
 
217
268
  // Sweeping once from each side turns the heuristic into two passes instead of one per candidate split.
218
- const rightArea = new Float64Array(BIN_COUNT);
219
- const rightCount = new Int32Array(BIN_COUNT);
220
- const accumulated = new Bounds();
269
+ let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
221
270
  let running = 0;
222
271
 
223
272
  for (let bin = BIN_COUNT - 1; bin > 0; bin--) {
224
- accumulated.addBounds(binBounds[bin]);
225
- running += binCounts[bin];
226
- rightArea[bin] = accumulated.area();
273
+ const at = (axis * BIN_COUNT + bin) * 6;
274
+
275
+ if (bounds[at] < minX) minX = bounds[at];
276
+ if (bounds[at + 1] < minY) minY = bounds[at + 1];
277
+ if (bounds[at + 2] < minZ) minZ = bounds[at + 2];
278
+ if (bounds[at + 3] > maxX) maxX = bounds[at + 3];
279
+ if (bounds[at + 4] > maxY) maxY = bounds[at + 4];
280
+ if (bounds[at + 5] > maxZ) maxZ = bounds[at + 5];
281
+ running += counts[axis * BIN_COUNT + bin];
282
+ rightArea[bin] = area(minX, minY, minZ, maxX, maxY, maxZ);
227
283
  rightCount[bin] = running;
228
284
  }
229
285
 
230
- accumulated.reset();
286
+ minX = minY = minZ = Infinity;
287
+ maxX = maxY = maxZ = -Infinity;
231
288
  running = 0;
232
289
 
233
290
  for (let bin = 0; bin < BIN_COUNT - 1; bin++) {
234
- accumulated.addBounds(binBounds[bin]);
235
- running += binCounts[bin];
291
+ const at = (axis * BIN_COUNT + bin) * 6;
292
+
293
+ if (bounds[at] < minX) minX = bounds[at];
294
+ if (bounds[at + 1] < minY) minY = bounds[at + 1];
295
+ if (bounds[at + 2] < minZ) minZ = bounds[at + 2];
296
+ if (bounds[at + 3] > maxX) maxX = bounds[at + 3];
297
+ if (bounds[at + 4] > maxY) maxY = bounds[at + 4];
298
+ if (bounds[at + 5] > maxZ) maxZ = bounds[at + 5];
299
+ running += counts[axis * BIN_COUNT + bin];
236
300
 
237
301
  if (!running || !rightCount[bin + 1]) continue;
238
302
 
239
303
  const cost = TRAVERSAL_COST
240
- + (accumulated.area() * running + rightArea[bin + 1] * rightCount[bin + 1]) / parentArea;
304
+ + (area(minX, minY, minZ, maxX, maxY, maxZ) * running + rightArea[bin + 1] * rightCount[bin + 1]) / parentArea;
241
305
 
242
306
  if (cost < bestCost) {
243
307
  bestCost = cost;
244
308
  bestAxis = axis;
245
309
  bestBin = bin;
246
- bestScale = scale;
247
310
  }
248
311
  }
249
312
  }
250
313
 
251
314
  if (bestAxis < 0) return -1;
252
315
 
253
- const pivot = centroidBounds.min[bestAxis];
254
- let left = range.start;
316
+ const pivot = centroidMin[bestAxis];
317
+ const bestScale = scale[bestAxis];
318
+ let left = start;
255
319
 
256
- for (let i = range.start; i < end; i++) {
320
+ for (let i = start; i < end; i++) {
257
321
  if (binOf(centroid[order[i] * 3 + bestAxis], pivot, bestScale) <= bestBin) {
258
322
  const swap = order[i];
259
323
 
@@ -264,7 +328,7 @@ function partition(order: Uint32Array, centroid: Float64Array, min: Float32Array
264
328
  }
265
329
 
266
330
  // The sweep only proposes splits both sides occupy, so this is a guard against rounding, not an expected outcome.
267
- return left === range.start || left === end ? -1 : left;
331
+ return left === start || left === end ? -1 : left;
268
332
  }
269
333
 
270
334
  function binOf(value: number, origin: number, scale: number): number {
@@ -0,0 +1,178 @@
1
+ import { buildBvh, type PackedBvh } from "./bvh.js";
2
+
3
+ /** What the page sends the worker: its own copy of the positions, transferred. */
4
+ export interface BvhRequest {
5
+ id: number;
6
+ positions: Float32Array;
7
+ triangleCount: number;
8
+ }
9
+
10
+ /** The hierarchy back, its buffers transferred, or why it could not be built. */
11
+ export type BvhResponse =
12
+ | { id: number; nodes: ArrayBuffer; nodeCount: number; order: Uint32Array; maxDepth: number }
13
+ | { id: number; error: string };
14
+
15
+ /**
16
+ * What the renderer needs from a worker: the shape of a Web `Worker`. Any transport that carries structured clones and
17
+ * transfer lists fits, such as a `worker_threads` thread behind an adapter.
18
+ */
19
+ export interface BvhWorker {
20
+ postMessage(message: BvhRequest, transfer?: Transferable[]): void;
21
+ terminate(): void;
22
+ onmessage: ((event: { data: BvhResponse }) => void) | null;
23
+ onerror: ((event: { message?: string; preventDefault?(): void }) => void) | null;
24
+ }
25
+
26
+ export type BvhWorkerFactory = () => BvhWorker;
27
+
28
+ /**
29
+ * Below this many triangles the hierarchy is built on the page: it takes a few milliseconds, less than copying the
30
+ * positions to a worker and the hierarchy back.
31
+ */
32
+ export const BVH_WORKER_MIN_TRIANGLES = 50_000;
33
+
34
+ /** The worker this package ships, next to this module. Replace it when the bundler cannot follow the URL. */
35
+ export function createDefaultBvhWorker(): BvhWorker {
36
+ if (typeof Worker === "undefined")
37
+ throw new Error("Web Workers are unavailable here: pass `createBvhWorker`, or `bvhWorker: false`.");
38
+
39
+ return new Worker(new URL("./bvhWorker.js", import.meta.url), { type: "module" }) as unknown as BvhWorker;
40
+ }
41
+
42
+ /** The worker's side of the exchange, apart from the worker itself so it can be run anywhere. */
43
+ export function respondToBvhRequest(request: BvhRequest): { response: BvhResponse; transfer: Transferable[] } {
44
+ try {
45
+ const bvh = buildBvh(request.positions, request.triangleCount);
46
+
47
+ return {
48
+ response: { id: request.id, ...bvh },
49
+ transfer: [bvh.nodes, bvh.order.buffer],
50
+ };
51
+ } catch(error) {
52
+ const message = error instanceof Error ? error.message : String(error);
53
+
54
+ return { response: { id: request.id, error: message }, transfer: [] };
55
+ }
56
+ }
57
+
58
+ interface Pending {
59
+ positions: Float32Array;
60
+ triangleCount: number;
61
+ resolve: (bvh: PackedBvh) => void;
62
+ reject: (error: Error) => void;
63
+ }
64
+
65
+ /**
66
+ * Builds hierarchies in a worker, so a large scene does not hold the page while it is split. The worker is started on
67
+ * the first build that needs it and kept for the next ones. If it cannot be started or fails to load, every build from
68
+ * then on happens on the page, with the same result: the worker only moves the work, it never changes it.
69
+ */
70
+ export class BvhBuilder {
71
+ private worker: BvhWorker | null = null;
72
+ private failed = false;
73
+ private disposed = false;
74
+ private next = 0;
75
+ private readonly pending = new Map<number, Pending>();
76
+
77
+ constructor(
78
+ private readonly enabled: boolean,
79
+ private readonly createWorker: BvhWorkerFactory = createDefaultBvhWorker,
80
+ private readonly minTriangles = BVH_WORKER_MIN_TRIANGLES) {}
81
+
82
+ /** Whether builds go to the worker: it is enabled and has not failed. */
83
+ get usesWorker(): boolean {
84
+ return this.enabled && !this.failed && !this.disposed;
85
+ }
86
+
87
+ build(positions: Float32Array, triangleCount: number): Promise<PackedBvh> {
88
+ if (this.disposed) return Promise.reject(abortError());
89
+
90
+ const worker = this.usesWorker && triangleCount >= this.minTriangles ? this.connect() : null;
91
+
92
+ if (!worker) return Promise.resolve(buildBvh(positions, triangleCount));
93
+
94
+ const id = this.next++;
95
+ // The page still uploads the positions, so the worker gets a copy it can own.
96
+ const copy = positions.slice(0, triangleCount * 9);
97
+
98
+ return new Promise<PackedBvh>((resolve, reject) => {
99
+ this.pending.set(id, { positions, triangleCount, resolve, reject });
100
+ worker.postMessage({ id, positions: copy, triangleCount }, [copy.buffer]);
101
+ });
102
+ }
103
+
104
+ /** Stops the worker. Builds still waiting for it reject with an `AbortError`. */
105
+ dispose() {
106
+ this.disposed = true;
107
+ this.worker?.terminate();
108
+ this.worker = null;
109
+
110
+ for (const pending of this.pending.values()) pending.reject(abortError());
111
+
112
+ this.pending.clear();
113
+ }
114
+
115
+ private connect(): BvhWorker | null {
116
+ if (this.worker) return this.worker;
117
+
118
+ let worker: BvhWorker;
119
+
120
+ try {
121
+ worker = this.createWorker();
122
+ } catch(error) {
123
+ this.fail(error instanceof Error ? error.message : String(error));
124
+
125
+ return null;
126
+ }
127
+
128
+ worker.onmessage = event => this.receive(event.data);
129
+
130
+ worker.onerror = event => {
131
+ event.preventDefault?.();
132
+ this.fail(event.message || "the worker failed to load");
133
+ };
134
+
135
+ this.worker = worker;
136
+
137
+ return worker;
138
+ }
139
+
140
+ private receive(response: BvhResponse) {
141
+ const pending = this.pending.get(response.id);
142
+
143
+ if (!pending) return;
144
+
145
+ this.pending.delete(response.id);
146
+
147
+ if ("error" in response) pending.reject(new Error(`GPU renderer: building the BVH failed: ${response.error}`));
148
+ else pending.resolve({
149
+ nodes: response.nodes, nodeCount: response.nodeCount, order: response.order, maxDepth: response.maxDepth,
150
+ });
151
+ }
152
+
153
+ /** The worker is gone for good: what it was building, and everything after, is built on the page instead. */
154
+ private fail(reason: string) {
155
+ if (!this.failed)
156
+ console.warn(`GPU renderer: the BVH worker is unavailable (${reason}); building on the main thread instead.`);
157
+
158
+ this.failed = true;
159
+ this.worker?.terminate();
160
+ this.worker = null;
161
+
162
+ const waiting = [...this.pending.values()];
163
+
164
+ this.pending.clear();
165
+
166
+ for (const pending of waiting) {
167
+ try {
168
+ pending.resolve(buildBvh(pending.positions, pending.triangleCount));
169
+ } catch(error) {
170
+ pending.reject(error instanceof Error ? error : new Error(String(error)));
171
+ }
172
+ }
173
+ }
174
+ }
175
+
176
+ function abortError(): Error {
177
+ return Object.assign(new Error("The renderer was disposed."), { name: "AbortError" });
178
+ }
@@ -0,0 +1,13 @@
1
+ // The worker `createDefaultBvhWorker` starts: it builds one hierarchy per message, off the page's thread.
2
+ import { respondToBvhRequest, type BvhRequest, type BvhResponse } from "./bvhBuilder.js";
3
+
4
+ const scope = self as unknown as {
5
+ onmessage: ((event: MessageEvent<BvhRequest>) => void) | null;
6
+ postMessage(message: BvhResponse, transfer: Transferable[]): void;
7
+ };
8
+
9
+ scope.onmessage = event => {
10
+ const { response, transfer } = respondToBvhRequest(event.data);
11
+
12
+ scope.postMessage(response, transfer);
13
+ };