gaussian-splat-lite 1.3.2 → 1.4.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.
@@ -0,0 +1,620 @@
1
+ // Candidate search adapted from MIT-licensed code. See THIRD_PARTY_LICENSES.md.
2
+ import { HALF, halfBits } from "./math.js";
3
+ import { Progress } from "./progress.js";
4
+ import { IndexArray, PagedArray, Permutation } from "./storage.js";
5
+ export const NONE = 0xffffffff;
6
+ // Files over which the tables of searches spread: worker threads read their
7
+ // pages at once, and the system reads one file little faster from many.
8
+ const STRIPES = 8;
9
+ // The share of slots that cells may take. Fuller tables mean longer probes
10
+ // but fewer pages to read and write back once they no longer fit the cache.
11
+ const LOAD = 0.75;
12
+
13
+ // An entry holds Similarities.get()'s 13 terms: the center, covariance and
14
+ // log determinant as floats and the color as halves, which it is exactly.
15
+ // Then come its node, the next entry of its cell's list and, as a double,
16
+ // its merge weight, so that a search reads nothing else.
17
+ const STRIDE = 16;
18
+ const COLOR = 10;
19
+ const NODE = 12;
20
+ const LINK = 13;
21
+ const WEIGHT = 14;
22
+ const pair = new Uint32Array(2);
23
+ const double = new Float64Array(pair.buffer);
24
+ // Entries are sorted by the Morton code of their cell, 4 bits per axis and
25
+ // radix pass. Cells beyond 30 bits per axis share codes.
26
+ const BITS = 30;
27
+ const SPREAD = Uint16Array.from({ length: 16 }, (_, v) => {
28
+ let spread = 0;
29
+ for (let bit = 0; bit < 4; bit++) spread |= ((v >> bit) & 1) << (bit * 3);
30
+ return spread;
31
+ });
32
+
33
+ // The difference of two cells' coordinates on the axis that orders them in
34
+ // Morton order, with x varying fastest: negative when the first comes first.
35
+ function morton(a, b) {
36
+ const below = (x, y) => x < y && x < (x ^ y);
37
+ let high = a[0] ^ b[0];
38
+ let difference = a[0] - b[0];
39
+ for (let d = 1; d < 3; d++) {
40
+ const bits = a[d] ^ b[d];
41
+ if (!below(bits, high)) {
42
+ high = bits;
43
+ difference = a[d] - b[d];
44
+ }
45
+ }
46
+ return difference;
47
+ }
48
+
49
+ // A set of cell hashes, cleared in constant time.
50
+ export class Cells {
51
+ constructor(bits) {
52
+ this.mask = (1 << bits) - 1;
53
+ this.hashes = new Uint32Array(1 << bits);
54
+ this.stamps = new Uint32Array(1 << bits);
55
+ this.stamp = 1;
56
+ }
57
+ clear() {
58
+ this.stamp++;
59
+ }
60
+ add(hash) {
61
+ let slot = hash & this.mask;
62
+ while (this.stamps[slot] === this.stamp && this.hashes[slot] !== hash)
63
+ slot = (slot + 1) & this.mask;
64
+ this.stamps[slot] = this.stamp;
65
+ this.hashes[slot] = hash;
66
+ }
67
+ has(hash) {
68
+ for (
69
+ let slot = hash & this.mask;
70
+ this.stamps[slot] === this.stamp;
71
+ slot = (slot + 1) & this.mask
72
+ )
73
+ if (this.hashes[slot] === hash) return true;
74
+ return false;
75
+ }
76
+ }
77
+
78
+ // A grid table that workers can read.
79
+ function newTable(workspace) {
80
+ const table = new PagedArray(workspace, Float32Array).share(STRIPES);
81
+ table.wordView = true;
82
+ return table;
83
+ }
84
+
85
+ // Copies through `scratch`: fetching the target page may evict the source's.
86
+ const scratch = new Uint32Array(STRIDE);
87
+ function copy(source, from, target, to) {
88
+ const words = source.at(from * STRIDE).words;
89
+ const offset = (from * STRIDE) & source.mask;
90
+ for (let w = 0; w < STRIDE; w++) scratch[w] = words[offset + w];
91
+ const out = target.at(to * STRIDE, true).words;
92
+ const at = (to * STRIDE) & target.mask;
93
+ for (let w = 0; w < STRIDE; w++) out[at + w] = scratch[w];
94
+ }
95
+
96
+ // A cell's hash XORs one term per axis. Above three bits for the cell in its
97
+ // block of 2 cells per axis, a term mixes the low and high 32 bits of the
98
+ // block's coordinate: the cells of a block take consecutive slots, so the
99
+ // cells around a query lie on few cache lines.
100
+ const LOW = [73856093, 19349663, 83492791];
101
+ const HIGH = [1640531535, 1597334677, 3812015801];
102
+ function term(value, d) {
103
+ const block = Math.floor(value / 2);
104
+ return (
105
+ ((Math.imul(block, LOW[d]) ^
106
+ Math.imul(Math.floor(block / 4294967296), HIGH[d])) <<
107
+ 3) |
108
+ ((value - block * 2) << d)
109
+ );
110
+ }
111
+ export function cellHash(x, y, z) {
112
+ return (term(x, 0) ^ term(y, 1) ^ term(z, 2)) >>> 0;
113
+ }
114
+
115
+ // Finds candidates in a grid's tables: the Grid itself on the main thread,
116
+ // and workers through SharedColumns. A removed entry has its bit set in
117
+ // `gone`, so that removing one leaves the pages of entries unchanged.
118
+ export class Lookup {
119
+ constructor(entries, slots, gone, capacity, step) {
120
+ this.entries = entries;
121
+ this.slots = slots;
122
+ this.gone = gone;
123
+ this.capacity = capacity;
124
+ this.step = step;
125
+ // Exact, as the step is a power of two, and faster than dividing.
126
+ this.inverse = 1 / step;
127
+ this.terms = new Int32Array(9);
128
+ }
129
+
130
+ // The slot of cell (x, y, z), or the free slot for it. A slot is a cell
131
+ // hash and its newest entry + 1, which is 0 in free slots.
132
+ slot(hash, x, y, z) {
133
+ const mask = this.capacity - 1;
134
+ for (let slot = hash & mask; ; slot = (slot + 1) & mask) {
135
+ const words = this.slots.page(slot * 2);
136
+ const at = this.slots.offset(slot * 2);
137
+ const head = words[at + 1];
138
+ if (!head) return slot;
139
+ if (words[at] !== hash) continue;
140
+ const floats = this.entries.page((head - 1) * STRIDE);
141
+ const offset = this.entries.offset((head - 1) * STRIDE);
142
+ if (
143
+ Math.floor(floats[offset] * this.inverse) === x &&
144
+ Math.floor(floats[offset + 1] * this.inverse) === y &&
145
+ Math.floor(floats[offset + 2] * this.inverse) === z
146
+ )
147
+ return slot;
148
+ }
149
+ }
150
+
151
+ // Copies an entry's similarity terms and then its weight into `out` and
152
+ // returns its node.
153
+ read(entry, out) {
154
+ const page = this.entries.at(entry * STRIDE);
155
+ const offset = this.entries.offset(entry * STRIDE);
156
+ const { values, words } = page;
157
+ for (let d = 0; d < 3; d++) out[d] = values[offset + d];
158
+ out[3] = HALF[words[offset + COLOR] & 0xffff];
159
+ out[4] = HALF[words[offset + COLOR] >>> 16];
160
+ out[5] = HALF[words[offset + COLOR + 1]];
161
+ for (let d = 0; d < 7; d++) out[6 + d] = values[offset + 3 + d];
162
+ pair[0] = words[offset + WEIGHT];
163
+ pair[1] = words[offset + WEIGHT + 1];
164
+ out[13] = double[0];
165
+ return words[offset + NODE];
166
+ }
167
+
168
+ // Whether `entry` is removed.
169
+ isRemoved(entry) {
170
+ return (this.gone.get(entry >>> 5) >>> (entry & 31)) & 1;
171
+ }
172
+
173
+ // The most similar splat other than `index` in the 27 cells around `query`,
174
+ // with its score in `metric`. Lists and cells are visited in reverse, and >=
175
+ // keeps the last best score: the first best of a forward scan over lists in
176
+ // insertion order. With `cells`, only the entries from `from` on in those
177
+ // cells count.
178
+ find(query, index, from = 0, cells = undefined) {
179
+ const qx = query[0];
180
+ const qy = query[1];
181
+ const qz = query[2];
182
+ const red = query[3];
183
+ const green = query[4];
184
+ const blue = query[5];
185
+ const cxx = query[6];
186
+ const cyy = query[7];
187
+ const czz = query[8];
188
+ const cxy = query[9];
189
+ const cxz = query[10];
190
+ const cyz = query[11];
191
+ const logDet = query[12];
192
+ const gx = Math.floor(qx * this.inverse);
193
+ const gy = Math.floor(qy * this.inverse);
194
+ const gz = Math.floor(qz * this.inverse);
195
+ const terms = this.terms;
196
+ for (let d = -1; d <= 1; d++) {
197
+ terms[1 + d] = term(gx + d, 0);
198
+ terms[4 + d] = term(gy + d, 1);
199
+ terms[7 + d] = term(gz + d, 2);
200
+ }
201
+ const entries = this.entries;
202
+ const gone = this.gone;
203
+ // The word of `gone` last read.
204
+ let word = -1;
205
+ let bits = 0;
206
+ let best = NONE;
207
+ let metric = Number.NEGATIVE_INFINITY;
208
+ let home = NONE;
209
+ let near = NONE;
210
+ for (let dz = 1; dz >= -1; dz--) {
211
+ for (let dy = 1; dy >= -1; dy--) {
212
+ for (let dx = 1; dx >= -1; dx--) {
213
+ const hash = (terms[1 + dx] ^ terms[4 + dy] ^ terms[7 + dz]) >>> 0;
214
+ if (cells && !cells.has(hash)) continue;
215
+ const slot = this.slot(hash, gx + dx, gy + dy, gz + dz);
216
+ if (!dx && !dy && !dz) home = slot;
217
+ const head = this.slots.get(slot * 2 + 1);
218
+ let entry = head ? head - 1 : NONE;
219
+ // Lists run from the newest entry down.
220
+ while (entry !== NONE && entry >= from) {
221
+ // Before the entry's page, which reading `gone` could evict.
222
+ if (entry >>> 5 !== word) {
223
+ word = entry >>> 5;
224
+ bits = gone.get(word);
225
+ }
226
+ const page = entries.at(entry * STRIDE);
227
+ const b = page.values;
228
+ const w = page.words;
229
+ const o = entries.offset(entry * STRIDE);
230
+ if (w[o + NODE] !== index && !((bits >>> (entry & 31)) & 1)) {
231
+ const xx = (cxx + b[o + 3]) * 0.5;
232
+ const yy = (cyy + b[o + 4]) * 0.5;
233
+ const zz = (czz + b[o + 5]) * 0.5;
234
+ const xy = (cxy + b[o + 6]) * 0.5;
235
+ const xz = (cxz + b[o + 7]) * 0.5;
236
+ const yz = (cyz + b[o + 8]) * 0.5;
237
+ const det =
238
+ xx * (yy * zz - yz * yz) -
239
+ xy * (xy * zz - yz * xz) +
240
+ xz * (xy * yz - yy * xz);
241
+ const diag = Math.max(Math.abs(xx), Math.abs(yy), Math.abs(zz));
242
+ let distance = 0;
243
+ if (Math.abs(det) >= 1e-9 * Math.max(diag * diag * diag, 1e-30)) {
244
+ const x = b[o] - qx;
245
+ const y = b[o + 1] - qy;
246
+ const z = b[o + 2] - qz;
247
+ const quad =
248
+ ((yy * zz - yz * yz) * x * x +
249
+ (xx * zz - xz * xz) * y * y +
250
+ (xx * yy - xy * xy) * z * z +
251
+ 2 *
252
+ ((xz * yz - xy * zz) * x * y +
253
+ (xy * yz - xz * yy) * x * z +
254
+ (xy * xz - xx * yz) * y * z)) /
255
+ det;
256
+ distance =
257
+ 0.125 * quad + 0.5 * Math.log(det) - logDet - b[o + 9];
258
+ }
259
+ const color = w[o + COLOR];
260
+ const score =
261
+ -distance -
262
+ (red - HALF[color & 0xffff]) ** 2 -
263
+ (green - HALF[color >>> 16]) ** 2 -
264
+ (blue - HALF[w[o + COLOR + 1]]) ** 2;
265
+ const value = Number.isNaN(score)
266
+ ? Number.NEGATIVE_INFINITY
267
+ : score;
268
+ if (best === NONE || value >= metric) {
269
+ best = entry;
270
+ metric = value;
271
+ near = slot;
272
+ }
273
+ }
274
+ entry = w[o + LINK];
275
+ }
276
+ }
277
+ }
278
+ }
279
+ this.metric = metric;
280
+ // The slots of the query's cell, if visited, and of the best's cell.
281
+ this.home = home;
282
+ this.near = near;
283
+ return best;
284
+ }
285
+ }
286
+
287
+ // The candidates of one LOD level. Each splat's similarity terms are computed
288
+ // once and its entry is stored in spatial order, so the cells around a query
289
+ // lie on few pages. A hash maps each cell to the newest entry of its list,
290
+ // keyed by the cell's hash and checked against that entry's center. Removed
291
+ // entries stay in the lists until compact() drops them.
292
+ export class Grid extends Lookup {
293
+ constructor(similarities, step, bounds, pool) {
294
+ const workspace = similarities.splats.workspace;
295
+ super(
296
+ newTable(workspace),
297
+ new IndexArray(workspace).share(STRIPES),
298
+ new IndexArray(workspace).share(STRIPES),
299
+ 16,
300
+ step,
301
+ );
302
+ this.similarities = similarities;
303
+ this.workspace = workspace;
304
+ this.pool = pool;
305
+ this.length = 0;
306
+ // Bumped when slots move.
307
+ this.layout = 0;
308
+ // Entries below `sorted` are in key order; inserted ones follow.
309
+ this.sorted = 0;
310
+ this.removed = 0;
311
+ this.count = 0;
312
+ // The slot, cell and cell hash of the last linked entry; -1 for none.
313
+ this.last = -1;
314
+ this.lastX = 0;
315
+ this.lastY = 0;
316
+ this.lastZ = 0;
317
+ this.lastHash = 0;
318
+ // Node -> entry, kept up to date by link().
319
+ this.entryOf = new IndexArray(this.workspace);
320
+ this.tuple = new Float64Array(14);
321
+ // Sort keys address blocks of `scale` cells per axis within the bounds:
322
+ // single cells unless the bounds exceed 2^30 cells.
323
+ this.origin = bounds.min.map((value) => Math.floor(value / step));
324
+ const extent = Math.max(
325
+ ...bounds.max.map(
326
+ (value, d) => Math.floor(value / step) - this.origin[d] + 1,
327
+ ),
328
+ );
329
+ const bits = Math.max(1, Math.ceil(Math.log2(extent)));
330
+ this.scale = 2 ** Math.max(0, bits - BITS);
331
+ this.limit = 2 ** Math.min(bits, BITS) - 1;
332
+ this.passes = Math.ceil(Math.min(bits, BITS) / 4);
333
+ this.a = new Int32Array(3);
334
+ this.b = new Int32Array(3);
335
+ }
336
+
337
+ // The coordinates of the block of an entry's cell, into `out`.
338
+ block(table, entry, out) {
339
+ const values = table.page(entry * STRIDE);
340
+ const offset = (entry * STRIDE) & table.mask;
341
+ for (let d = 0; d < 3; d++)
342
+ out[d] = Math.min(
343
+ this.limit,
344
+ Math.max(
345
+ 0,
346
+ Math.floor(
347
+ (Math.floor(values[offset + d] * this.inverse) - this.origin[d]) /
348
+ this.scale,
349
+ ),
350
+ ),
351
+ );
352
+ return out;
353
+ }
354
+
355
+ // Adds the active splats that are not removed, in heap order.
356
+ build(active, removed, label) {
357
+ const progress = label && new Progress(`${label} grid`, active.length);
358
+ for (let i = 0; i < active.length; i++) {
359
+ const node = active.index(i);
360
+ if (!removed.get(node)) this.add(node);
361
+ if ((i & 4095) === 0) progress?.update(i);
362
+ }
363
+ progress?.finish();
364
+ this.entries = this.sort(this.entries, this.length, label);
365
+ this.sorted = this.length;
366
+ this.relink(label);
367
+ }
368
+
369
+ // Adds a merged splat during the level, with its terms if known, and
370
+ // returns the hash of its cell.
371
+ insert(node, weight, terms, slot = NONE) {
372
+ this.add(node, weight, terms);
373
+ this.link(this.length - 1, slot);
374
+ return this.lastHash;
375
+ }
376
+
377
+ // Without terms, reads the splat's terms and weight.
378
+ add(node, weight, terms) {
379
+ const tuple = terms ?? this.similarities.get(node, this.tuple);
380
+ const entry = this.length++;
381
+ const { values, words } = this.entries.at(entry * STRIDE, true);
382
+ const offset = (entry * STRIDE) & this.entries.mask;
383
+ for (let d = 0; d < 3; d++) values[offset + d] = tuple[d];
384
+ for (let d = 0; d < 7; d++) values[offset + 3 + d] = tuple[6 + d];
385
+ words[offset + COLOR] = halfBits(tuple[3]) | (halfBits(tuple[4]) << 16);
386
+ words[offset + COLOR + 1] = halfBits(tuple[5]);
387
+ words[offset + NODE] = node;
388
+ words[offset + LINK] = NONE;
389
+ double[0] = terms ? weight : tuple[13];
390
+ words[offset + WEIGHT] = pair[0];
391
+ words[offset + WEIGHT + 1] = pair[1];
392
+ }
393
+
394
+ // Returns the first `length` entries of `table` stably sorted by block,
395
+ // disposing `table` if they moved. Sorts each entry's block and position,
396
+ // four words, and then moves the entries once.
397
+ sort(table, length, label) {
398
+ const passes = this.passes;
399
+ const progress =
400
+ label && new Progress(`${label} spatial sort`, length * (passes + 2));
401
+ const digit = (words, o, pass) =>
402
+ SPREAD[(words[o] >>> (pass * 4)) & 15] |
403
+ (SPREAD[(words[o + 1] >>> (pass * 4)) & 15] << 1) |
404
+ (SPREAD[(words[o + 2] >>> (pass * 4)) & 15] << 2);
405
+ const counts = Array.from({ length: passes }, () => new Float64Array(4096));
406
+ const block = this.a;
407
+ let keys = new IndexArray(this.workspace);
408
+ for (let entry = 0; entry < length; entry++) {
409
+ this.block(table, entry, block);
410
+ const words = keys.page(entry * 4, true);
411
+ const o = (entry * 4) & keys.mask;
412
+ words[o] = block[0];
413
+ words[o + 1] = block[1];
414
+ words[o + 2] = block[2];
415
+ words[o + 3] = entry;
416
+ for (let pass = 0; pass < passes; pass++)
417
+ counts[pass][digit(words, o, pass)]++;
418
+ if ((entry & 4095) === 0) progress?.update(entry);
419
+ }
420
+ let moved = false;
421
+ for (let pass = 0; pass < passes; pass++) {
422
+ const cursor = counts[pass];
423
+ if (cursor.includes(length)) continue;
424
+ for (let b = 0, sum = 0; b < 4096; b++) {
425
+ const count = cursor[b];
426
+ cursor[b] = sum;
427
+ sum += count;
428
+ }
429
+ const sorted = new IndexArray(this.workspace);
430
+ for (let i = 0; i < length; i++) {
431
+ const words = keys.page(i * 4);
432
+ const o = (i * 4) & keys.mask;
433
+ const x = words[o];
434
+ const y = words[o + 1];
435
+ const z = words[o + 2];
436
+ const entry = words[o + 3];
437
+ const t = cursor[digit(words, o, pass)]++ * 4;
438
+ const out = sorted.page(t, true);
439
+ const at = t & sorted.mask;
440
+ out[at] = x;
441
+ out[at + 1] = y;
442
+ out[at + 2] = z;
443
+ out[at + 3] = entry;
444
+ if ((i & 4095) === 0) progress?.update(length * (pass + 1) + i);
445
+ }
446
+ keys.dispose();
447
+ keys = sorted;
448
+ moved = true;
449
+ }
450
+ if (!moved) {
451
+ keys.dispose();
452
+ progress?.finish();
453
+ return table;
454
+ }
455
+ const indices = new IndexArray(this.workspace);
456
+ for (let i = 0; i < length; i++) {
457
+ indices.push(keys.get(i * 4 + 3));
458
+ if ((i & 4095) === 0) progress?.update(length * (passes + 1) + i);
459
+ }
460
+ keys.dispose();
461
+ progress?.finish();
462
+ const permutation = new Permutation(
463
+ this.workspace,
464
+ indices,
465
+ length,
466
+ label && `${label} spatial order`,
467
+ );
468
+ indices.dispose();
469
+ const sorted = permutation.apply(
470
+ table,
471
+ STRIDE,
472
+ this.pool,
473
+ true,
474
+ label && `${label} spatial reorder`,
475
+ );
476
+ permutation.dispose();
477
+ return sorted;
478
+ }
479
+
480
+ // Drops removed entries and merges the inserted ones, sorted, into the
481
+ // rest. Entries with equal keys keep their order.
482
+ compact() {
483
+ const old = this.entries;
484
+ let inserted = newTable(this.workspace);
485
+ let added = 0;
486
+ for (let entry = this.sorted; entry < this.length; entry++)
487
+ if (!this.isRemoved(entry)) copy(old, entry, inserted, added++);
488
+ inserted = this.sort(inserted, added);
489
+ this.entries = newTable(this.workspace);
490
+ let length = 0;
491
+ for (let i = 0, j = 0; ; ) {
492
+ while (i < this.sorted && this.isRemoved(i)) i++;
493
+ if (i === this.sorted && j === added) break;
494
+ if (
495
+ j === added ||
496
+ (i < this.sorted &&
497
+ morton(this.block(old, i, this.a), this.block(inserted, j, this.b)) <=
498
+ 0)
499
+ )
500
+ copy(old, i++, this.entries, length++);
501
+ else copy(inserted, j++, this.entries, length++);
502
+ }
503
+ old.dispose();
504
+ inserted.dispose();
505
+ this.gone.dispose();
506
+ this.gone = new IndexArray(this.workspace).share(STRIPES);
507
+ this.length = this.sorted = length;
508
+ this.removed = 0;
509
+ this.relink();
510
+ }
511
+
512
+ // Rebuilds the lists in entry order. Entries of a cell are in insertion
513
+ // order, so each list runs from the newest entry to the oldest.
514
+ relink(label) {
515
+ const progress = label && new Progress(`${label} indexing`, this.length);
516
+ this.slots.dispose();
517
+ this.slots = new IndexArray(this.workspace).share(STRIPES);
518
+ // Compaction keeps at most the cells there were.
519
+ this.capacity = 16;
520
+ while (this.capacity * LOAD <= this.count) this.capacity *= 2;
521
+ this.count = 0;
522
+ this.last = -1;
523
+ this.layout++;
524
+ for (let entry = 0; entry < this.length; entry++) {
525
+ this.link(entry);
526
+ if ((entry & 4095) === 0) progress?.update(entry);
527
+ }
528
+ progress?.finish();
529
+ }
530
+
531
+ // Prepends `entry` to the list of its cell.
532
+ link(entry, hint = NONE) {
533
+ const page = this.entries.at(entry * STRIDE);
534
+ const offset = (entry * STRIDE) & this.entries.mask;
535
+ const node = page.words[offset + NODE];
536
+ const x = Math.floor(page.values[offset] * this.inverse);
537
+ const y = Math.floor(page.values[offset + 1] * this.inverse);
538
+ const z = Math.floor(page.values[offset + 2] * this.inverse);
539
+ this.entryOf.set(node, entry);
540
+ // Entries in spatial order often share the last entry's cell.
541
+ let slot = this.last;
542
+ if (hint !== NONE) {
543
+ slot = hint;
544
+ this.lastHash = this.slots.get(hint * 2);
545
+ } else if (
546
+ slot < 0 ||
547
+ x !== this.lastX ||
548
+ y !== this.lastY ||
549
+ z !== this.lastZ
550
+ ) {
551
+ if (
552
+ !Number.isSafeInteger(x) ||
553
+ !Number.isSafeInteger(y) ||
554
+ !Number.isSafeInteger(z)
555
+ )
556
+ throw new Error(
557
+ "Splat coordinates exceed the LOD grid's integer range",
558
+ );
559
+ this.lastHash = (term(x, 0) ^ term(y, 1) ^ term(z, 2)) >>> 0;
560
+ slot = this.slot(this.lastHash, x, y, z);
561
+ }
562
+ const head = this.slots.get(slot * 2 + 1);
563
+ this.entries.at(entry * STRIDE, true).words[offset + LINK] = head
564
+ ? head - 1
565
+ : NONE;
566
+ this.slots.set(slot * 2 + 1, entry + 1);
567
+ this.last = slot;
568
+ this.lastX = x;
569
+ this.lastY = y;
570
+ this.lastZ = z;
571
+ if (!head) {
572
+ this.slots.set(slot * 2, this.lastHash);
573
+ if (++this.count >= this.capacity * LOAD) this.grow();
574
+ }
575
+ }
576
+
577
+ grow() {
578
+ this.last = -1;
579
+ this.layout++;
580
+ const old = this.slots;
581
+ const capacity = this.capacity;
582
+ this.capacity *= 2;
583
+ this.slots = new IndexArray(this.workspace).share(STRIPES);
584
+ const mask = this.capacity - 1;
585
+ for (let i = 0; i < capacity; i++) {
586
+ const head = old.get(i * 2 + 1);
587
+ if (!head) continue;
588
+ const hash = old.get(i * 2);
589
+ let slot = hash & mask;
590
+ while (this.slots.get(slot * 2 + 1)) slot = (slot + 1) & mask;
591
+ this.slots.set(slot * 2, hash);
592
+ this.slots.set(slot * 2 + 1, head);
593
+ }
594
+ old.dispose();
595
+ }
596
+
597
+ remove(entry) {
598
+ const word = entry >>> 5;
599
+ this.gone.set(word, this.gone.get(word) | (1 << (entry & 31)));
600
+ this.removed++;
601
+ }
602
+
603
+ // The entry of `node`, which must be in the grid.
604
+ entry(node) {
605
+ return this.entryOf.get(node);
606
+ }
607
+
608
+ // Compacts the grid once half of it is removed. Entries keep their
609
+ // positions until the next call.
610
+ tidy() {
611
+ if (this.removed * 2 > this.length) this.compact();
612
+ }
613
+
614
+ dispose() {
615
+ this.entries.dispose();
616
+ this.slots.dispose();
617
+ this.gone.dispose();
618
+ this.entryOf.dispose();
619
+ }
620
+ }