@vitessce/spatial-accelerated 3.8.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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +52925 -0
  3. package/dist-tsc/SpatialWrapper.d.ts +2 -0
  4. package/dist-tsc/SpatialWrapper.d.ts.map +1 -0
  5. package/dist-tsc/SpatialWrapper.js +45 -0
  6. package/dist-tsc/VolumeDataManager.d.ts +170 -0
  7. package/dist-tsc/VolumeDataManager.d.ts.map +1 -0
  8. package/dist-tsc/VolumeDataManager.js +1273 -0
  9. package/dist-tsc/VolumeDataManager.test.d.ts +2 -0
  10. package/dist-tsc/VolumeDataManager.test.d.ts.map +1 -0
  11. package/dist-tsc/VolumeDataManager.test.js +164 -0
  12. package/dist-tsc/VolumeRenderManager.d.ts +265 -0
  13. package/dist-tsc/VolumeRenderManager.d.ts.map +1 -0
  14. package/dist-tsc/VolumeRenderManager.js +396 -0
  15. package/dist-tsc/VolumeShader.d.ts +262 -0
  16. package/dist-tsc/VolumeShader.d.ts.map +1 -0
  17. package/dist-tsc/VolumeShader.js +73 -0
  18. package/dist-tsc/VolumeView.d.ts +2 -0
  19. package/dist-tsc/VolumeView.d.ts.map +1 -0
  20. package/dist-tsc/VolumeView.js +521 -0
  21. package/dist-tsc/index.d.ts +2 -0
  22. package/dist-tsc/index.d.ts.map +1 -0
  23. package/dist-tsc/index.js +1 -0
  24. package/dist-tsc/shaders/GaussianFragmentShader.d.ts +2 -0
  25. package/dist-tsc/shaders/GaussianFragmentShader.d.ts.map +1 -0
  26. package/dist-tsc/shaders/GaussianFragmentShader.js +242 -0
  27. package/dist-tsc/shaders/GaussianVertexShader.d.ts +2 -0
  28. package/dist-tsc/shaders/GaussianVertexShader.d.ts.map +1 -0
  29. package/dist-tsc/shaders/GaussianVertexShader.js +51 -0
  30. package/dist-tsc/shaders/VolumeFragmentShader.d.ts +2 -0
  31. package/dist-tsc/shaders/VolumeFragmentShader.d.ts.map +1 -0
  32. package/dist-tsc/shaders/VolumeFragmentShader.js +1817 -0
  33. package/dist-tsc/shaders/VolumeVertexShader.d.ts +2 -0
  34. package/dist-tsc/shaders/VolumeVertexShader.d.ts.map +1 -0
  35. package/dist-tsc/shaders/VolumeVertexShader.js +110 -0
  36. package/package.json +63 -0
  37. package/src/SpatialWrapper.js +60 -0
  38. package/src/VolumeDataManager.js +1515 -0
  39. package/src/VolumeDataManager.test.js +180 -0
  40. package/src/VolumeRenderManager.js +589 -0
  41. package/src/VolumeShader.js +74 -0
  42. package/src/VolumeView.js +651 -0
  43. package/src/index.js +1 -0
  44. package/src/shaders/GaussianFragmentShader.js +243 -0
  45. package/src/shaders/GaussianVertexShader.js +52 -0
  46. package/src/shaders/VolumeFragmentShader.js +1818 -0
  47. package/src/shaders/VolumeVertexShader.js +111 -0
@@ -0,0 +1,1515 @@
1
+ /* eslint-disable no-bitwise */
2
+ /* eslint-disable no-underscore-dangle */
3
+ /* eslint-disable max-len */
4
+ /* eslint-disable no-unused-vars */
5
+ /* eslint-disable camelcase */
6
+ import {
7
+ Data3DTexture,
8
+ RedFormat,
9
+ RedIntegerFormat,
10
+ UnsignedByteType,
11
+ UnsignedIntType,
12
+ LinearFilter,
13
+ NearestFilter,
14
+ } from 'three';
15
+ import { isEqual } from 'lodash-es';
16
+ import { log, atLeastLogLevel, LogLevel } from '@vitessce/globals';
17
+
18
+ // NOTES:
19
+ // 2048 x 2048 x 32 = 4096 bricks, around 134 MB
20
+ // 2048 x 2048 x 64 = 8192 bricks, around 268 MB
21
+ // 2048 x 2048 x 128 = 16384 bricks, around 537 MB **
22
+ // 2048 x 2048 x 256 = 32768 bricks, around 1.074 GB
23
+ // 2048 x 2048 x 512 = 65536 bricks, around 2.148 GB
24
+ // 2048 x 2048 x 1024 = 131072 bricks, around 4.295 GB
25
+
26
+ // Default chunk sizes
27
+ // const CHUNK_SIZE = 32;
28
+ // const CHUNK_TOTAL_SIZE = 32768; // 32*32*32 = 32768
29
+ const BRICK_SIZE = 32;
30
+ const BRICK_CACHE_SIZE_X = 64;
31
+ const BRICK_CACHE_SIZE_Y = 64;
32
+ const BRICK_CACHE_SIZE_Z = 4;
33
+ const TOTAL_NUM_BRICKS = BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y * BRICK_CACHE_SIZE_Z;
34
+
35
+ const BRICK_CACHE_SIZE_VOXELS_X = BRICK_CACHE_SIZE_X * BRICK_SIZE;
36
+ const BRICK_CACHE_SIZE_VOXELS_Y = BRICK_CACHE_SIZE_Y * BRICK_SIZE;
37
+ const BRICK_CACHE_SIZE_VOXELS_Z = BRICK_CACHE_SIZE_Z * BRICK_SIZE;
38
+
39
+ const PAGE_TABLE_ADDRESS_SIZE = 'uint32';
40
+ const BRICK_CACHE_ADDRESS_SIZE = 'uint16';
41
+
42
+ const MAX_RESOLUTION_LEVELS = 10; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
43
+ const MAX_CHANNELS = 7; // [0, 1, 2, 3, 4, 5, 6]
44
+
45
+ // const manualChannelSelection = 0;
46
+
47
+ // Add a constant for initialization status
48
+ const INIT_STATUS = {
49
+ NOT_STARTED: 'not_started',
50
+ IN_PROGRESS: 'in_progress',
51
+ COMPLETE: 'complete',
52
+ FAILED: 'failed',
53
+ };
54
+
55
+ function logWithColor(message) {
56
+ if (atLeastLogLevel(LogLevel.DEBUG)) {
57
+ console.warn(`%cDM: ${message}`, 'background: blue; color: white; padding: 2px; border-radius: 3px;');
58
+ }
59
+ }
60
+
61
+
62
+ /* Begin extracted functions */
63
+ export function _resolutionStatsToShapes(multiResolutionStats) {
64
+ return multiResolutionStats.map((stats) => {
65
+ const { dims } = stats;
66
+ const shape = [
67
+ // Other spatial-accelerated code assumes TCZYX dimension order
68
+ dims.t, // TODO: standardize lowercase/uppercase dim names at store-level
69
+ dims.c, // TODO: handle case when dimension(s) are missing
70
+ dims.z,
71
+ dims.y,
72
+ dims.x,
73
+ ];
74
+ return shape;
75
+ });
76
+ }
77
+ export function _resolutionStatsToBrickLayout(multiResolutionStats) {
78
+ return multiResolutionStats.map((stats) => {
79
+ const shape = [
80
+ stats.depth, // z
81
+ stats.height, // y
82
+ stats.width, // x
83
+ ];
84
+ // TODO: abstract chunkSize to always be 32x32x32 at store-level
85
+ // const chunkSize = array0.chunks || [BRICK_SIZE, BRICK_SIZE, BRICK_SIZE];
86
+ return [
87
+ Math.ceil((shape[0] || 1) / BRICK_SIZE),
88
+ Math.ceil((shape[1] || 1) / BRICK_SIZE),
89
+ Math.ceil((shape[2] || 1) / BRICK_SIZE),
90
+ ];
91
+ });
92
+ }
93
+
94
+ /**
95
+ *
96
+ * @param {[number, number, number][]} zarrStoreBrickLayout
97
+ * @param {number} channelsZarrMappingsLength
98
+ */
99
+ export function _initMRMCPT(zarrStoreBrickLayout, channelsZarrMappingsLength) {
100
+ log.debug('_initMRMCPT', zarrStoreBrickLayout, channelsZarrMappingsLength);
101
+ // Page table
102
+ const PT = {
103
+ channelOffsets: [
104
+ [0, 0, 1],
105
+ [0, 1, 0],
106
+ [0, 1, 1],
107
+ [1, 0, 0],
108
+ [1, 0, 1],
109
+ [1, 1, 0],
110
+ [1, 1, 1],
111
+ ],
112
+ anchors: [],
113
+ offsets: [],
114
+ xExtent: 0, // includes the offset inclusive
115
+ yExtent: 0, // includes the offset inclusive
116
+ zExtent: 0, // includes the offset inclusive
117
+ z0Extent: 0, // l0 z extent
118
+ zTotal: 0, // original z extent plus the l0 z extent times the channel count
119
+ };
120
+
121
+ // Calculate PT extents first
122
+ PT.xExtent = 1;
123
+ PT.yExtent = 1;
124
+ PT.zExtent = 1;
125
+ const l0z = zarrStoreBrickLayout[0][0]; // Get z extent from highest resolution
126
+ PT.z0Extent = l0z;
127
+
128
+ // log.debug('PT', PT);
129
+
130
+ // log.debug('PT anchors', PT.anchors);
131
+
132
+ // log.debug('PT anchors with 0', PT.anchors);
133
+
134
+ PT.lowestDataRes = zarrStoreBrickLayout.length - 1;
135
+ // log.debug('lowestDataRes', PT.lowestDataRes);
136
+
137
+ // Sum up the extents from all resolution levels excluding the l0
138
+ for (let i = zarrStoreBrickLayout.length - 1; i > 0; i--) {
139
+ PT.anchors.push([
140
+ PT.xExtent,
141
+ PT.yExtent,
142
+ PT.zExtent,
143
+ ]);
144
+ PT.xExtent += zarrStoreBrickLayout[i][2];
145
+ PT.yExtent += zarrStoreBrickLayout[i][1];
146
+ PT.zExtent += zarrStoreBrickLayout[i][0];
147
+ }
148
+
149
+ PT.anchors.push([0, 0, PT.zExtent]);
150
+ PT.anchors.reverse();
151
+
152
+ // log.debug('PT anchors', PT.anchors);
153
+
154
+ // Calculate total Z extent including channel offsets
155
+ // PT.zTotal = PT.zExtent + (this.zarrStore.channelCount * l0z);
156
+ PT.zTotal = PT.zExtent + channelsZarrMappingsLength * l0z;
157
+ // log.debug('number of PT channels', channelsZarrMappingsLength);
158
+
159
+ // log.debug('Page Table Extents:', {
160
+ // x: PT.xExtent,
161
+ // y: PT.yExtent,
162
+ // z: PT.zExtent,
163
+ // z0: PT.z0Extent,
164
+ // zTotal: PT.zTotal,
165
+ // });
166
+
167
+ // Initialize the BrickCache as a 3D texture (uint8)
168
+ const brickCacheData = new Uint8Array(
169
+ BRICK_CACHE_SIZE_VOXELS_X
170
+ * BRICK_CACHE_SIZE_VOXELS_Y
171
+ * BRICK_CACHE_SIZE_VOXELS_Z,
172
+ );
173
+ brickCacheData.fill(0);
174
+
175
+ // Initialize the PageTable data using calculated extents
176
+ const pageTableData = new Uint32Array(
177
+ PT.xExtent * PT.yExtent * PT.zTotal,
178
+ );
179
+ pageTableData.fill(0);
180
+
181
+ const bcTHREE = new Data3DTexture(
182
+ brickCacheData,
183
+ BRICK_CACHE_SIZE_VOXELS_X,
184
+ BRICK_CACHE_SIZE_VOXELS_Y,
185
+ BRICK_CACHE_SIZE_VOXELS_Z,
186
+ );
187
+ bcTHREE.format = RedFormat;
188
+ bcTHREE.type = UnsignedByteType;
189
+ bcTHREE.internalFormat = 'R8';
190
+ // bcTHREE.minFilter = NearestFilter;
191
+ // bcTHREE.magFilter = NearestFilter;
192
+ bcTHREE.minFilter = LinearFilter;
193
+ bcTHREE.magFilter = LinearFilter;
194
+ bcTHREE.generateMipmaps = false;
195
+ bcTHREE.needsUpdate = true;
196
+
197
+ const ptTHREE = new Data3DTexture(
198
+ pageTableData,
199
+ PT.xExtent,
200
+ PT.yExtent,
201
+ PT.zTotal,
202
+ );
203
+ ptTHREE.format = RedIntegerFormat;
204
+ ptTHREE.type = UnsignedIntType;
205
+ ptTHREE.internalFormat = 'R32UI';
206
+ ptTHREE.minFilter = NearestFilter;
207
+ ptTHREE.magFilter = NearestFilter;
208
+ ptTHREE.generateMipmaps = false;
209
+ ptTHREE.needsUpdate = true;
210
+
211
+ log.debug('_initMRMCPT', PT, ptTHREE, bcTHREE);
212
+
213
+ return {
214
+ PT,
215
+ ptTHREE,
216
+ bcTHREE,
217
+ };
218
+ }
219
+
220
+
221
+ /* ------------------------------------------------------------- *
222
+ * 3. Pack PT entry (flags | min | max | bcX | bcY | bcZ) *
223
+ * ------------------------------------------------------------- */
224
+ export function _packPT(min, max, bcX, bcY, bcZ) {
225
+ // Scale down to 7-bit range (0-127) by dividing by 2
226
+ const clamp7 = v => Math.max(0, Math.min(127, Math.floor(v / 2)));
227
+ // log.debug('Scaled min/max:', clamp7(min), clamp7(max));
228
+ return (
229
+ (1 << 31) // bit‑31 = resident
230
+ | (1 << 30) // bit‑30 = init‑done
231
+ | (clamp7(min) << 23) // 7 bits
232
+ | (clamp7(max) << 16) // 7 bits
233
+ | ((bcX & 0x3F) << 10) // 6 bits
234
+ | ((bcY & 0x3F) << 4) // 6 bits
235
+ | (bcZ & 0x0F) // 4 bits
236
+ ) >>> 0; // keep unsigned
237
+ }
238
+
239
+ // Pack PT entry indicating "not resident but init" with min/max preserved
240
+ // Note: this is not used anywhere?
241
+ /*
242
+ export function _packPTNotResident(min, max, bcX, bcY, bcZ) {
243
+ // Scale down to 7-bit range (0-127) by dividing by 2
244
+ const clamp7 = v => Math.max(0, Math.min(127, Math.floor(v / 2)));
245
+
246
+ return (
247
+ (0 << 31) // bit-31 = NOT resident
248
+ | (1 << 30) // bit-30 = init-done
249
+ | (clamp7(min) << 23) // 7 bits
250
+ | (clamp7(max) << 16) // 7 bits
251
+ | ((bcX & 0x3F) << 10) // 6 bits
252
+ | ((bcY & 0x3F) << 4) // 6 bits
253
+ | (bcZ & 0x0F) // 4 bits
254
+ ) >>> 0; // keep unsigned
255
+ }
256
+ */
257
+
258
+ export function _ptToZarr(ptx, pty, ptz, ptInfo) {
259
+ const { PT_zExtent, PT_z0Extent, PT_anchors } = ptInfo;
260
+ let channel = -1;
261
+ let resolution = -1;
262
+ let x = -1;
263
+ let y = -1;
264
+ let z = -1;
265
+ // log.debug('pt', ptx, pty, ptz);
266
+ // log.debug('pt', ptx, pty, ptz);
267
+ // log.debug('ptz', this.PT.z, this.PT.l0z);
268
+ if (ptz >= PT_zExtent) {
269
+ // log.debug('ptz >= this.PT.zExtent');
270
+ resolution = 0;
271
+ x = ptx;
272
+ y = pty;
273
+ z = (ptz - PT_zExtent) % PT_z0Extent;
274
+ channel = Math.floor((ptz - PT_zExtent) / PT_z0Extent);
275
+ } else {
276
+ // log.debug('ptz < this.PT.zExtent');
277
+ // log.debug('this.PT.anchors', this.PT.anchors);
278
+ for (let i = 1; i < PT_anchors.length; i++) {
279
+ if (ptx < PT_anchors[i][0] && pty < PT_anchors[i][1] && ptz < PT_anchors[i][2]) {
280
+ // all PT coordinates are less than the anchor -> one res lower
281
+ // log.debug('all PT coordinates are less than the anchor -> one res lower');
282
+ } else {
283
+ // log.debug('not all PT coordinates are less than the anchor ', this.PT.anchors[i]);
284
+ resolution = i;
285
+ const channelMask = [0, 0, 0];
286
+ // log.debug('ptx', ptx);
287
+ // log.debug('pty', pty);
288
+ // log.debug('ptz', ptz);
289
+ // log.debug('this.PT.anchors[i]', this.PT.anchors[i]);
290
+ if (ptx >= PT_anchors[i][0]) { channelMask[0] = 1; }
291
+ if (pty >= PT_anchors[i][1]) { channelMask[1] = 1; }
292
+ if (ptz >= PT_anchors[i][2]) { channelMask[2] = 1; }
293
+ const binaryChannel = (channelMask[0] << 2) | (channelMask[1] << 1) | channelMask[2];
294
+ channel = Math.max(1, Math.min(7, binaryChannel)) - 1;
295
+ // log.debug('channel', channel);
296
+ const thisOffset = channelMask.map((v, j) => v * PT_anchors[i][j]);
297
+ // log.debug('thisOffset', thisOffset);
298
+ x = ptx - thisOffset[0];
299
+ y = pty - thisOffset[1];
300
+ z = ptz - thisOffset[2];
301
+ // log.debug('x', x);
302
+ // log.debug('y', y);
303
+ // log.debug('z', z);
304
+ break;
305
+ }
306
+ }
307
+ }
308
+ // log.debug(channel, resolution, x, y, z);
309
+
310
+ return {
311
+ channel,
312
+ resolution,
313
+ x,
314
+ y,
315
+ z,
316
+ };
317
+ }
318
+
319
+ export function _requestBufferToRequestObjects(buffer, k) {
320
+ const counts = new Map();
321
+ for (let i = 0; i < buffer.length; i += 4) {
322
+ const r = buffer[i];
323
+ const g = buffer[i + 1];
324
+ const b = buffer[i + 2];
325
+ const a = buffer[i + 3];
326
+ if ((r | g | b | a) === 0) {
327
+ // eslint-disable-next-line no-continue
328
+ continue;
329
+ }
330
+ const packed = ((r << 24) | (g << 16) | (b << 8) | a) >>> 0;
331
+ counts.set(packed, (counts.get(packed) || 0) + 1);
332
+ }
333
+ // log.debug('counts', counts);
334
+ // log.debug('this.k', this.k);
335
+ // Get the top K (potentially fewer than K) page table requests.
336
+ const requests = [...counts.entries()]
337
+ .sort((a, b) => b[1] - a[1])
338
+ .slice(0, k)
339
+ .map(([packed]) => ({
340
+ x: (packed >> 22) & 0x3FF,
341
+ y: (packed >> 12) & 0x3FF,
342
+ z: packed & 0xFFF,
343
+ }));
344
+ return { requests, origRequestCount: counts.size };
345
+ }
346
+
347
+ /* End extracted functions */
348
+
349
+ export class VolumeDataManager {
350
+ constructor(glParam) {
351
+ logWithColor('CLASS INITIALIZING');
352
+ // this.store = new zarrita.FetchStore(url); // TODO: use this.images instead
353
+
354
+ const gl = glParam.getContext?.() || glParam;
355
+ const renderer = glParam;
356
+
357
+ // this.images = images;
358
+ // this.imageLayerScopes = imageLayerScopes;
359
+
360
+ // Handle both WebGLRenderer and WebGL context
361
+ if (gl.domElement && gl.getContext) {
362
+ // This is likely a THREE.WebGLRenderer
363
+ this.gl = gl.getContext();
364
+ } else if (gl.isWebGLRenderer) {
365
+ // Explicit THREE.WebGLRenderer check
366
+ this.gl = gl.getContext();
367
+ } else {
368
+ // Assume it's already a WebGL context
369
+ this.gl = gl;
370
+ }
371
+
372
+ // const maxFragmentUniformVectors = this.gl.getParameter(this.gl.MAX_FRAGMENT_UNIFORM_VECTORS);
373
+ // log.debug('maxFragmentUniformVectors', maxFragmentUniformVectors);
374
+ // 1024
375
+
376
+ this.renderer = renderer;
377
+
378
+ // Create a mock context if we couldn't get a real one
379
+ if (!this.gl || typeof this.gl.getParameter !== 'function') {
380
+ log.debug('Unable to get WebGL context, using mock context');
381
+ this.gl = {
382
+ getParameter: (param) => {
383
+ // Return reasonable defaults
384
+ const defaults = {
385
+ MAX_TEXTURE_SIZE: 4096,
386
+ MAX_3D_TEXTURE_SIZE: 256,
387
+ MAX_RENDERBUFFER_SIZE: 4096,
388
+ MAX_UNIFORM_BUFFER_BINDINGS: 16,
389
+ };
390
+ return defaults[param] || 0;
391
+ },
392
+ MAX_TEXTURE_SIZE: 'MAX_TEXTURE_SIZE',
393
+ MAX_3D_TEXTURE_SIZE: 'MAX_3D_TEXTURE_SIZE',
394
+ MAX_RENDERBUFFER_SIZE: 'MAX_RENDERBUFFER_SIZE',
395
+ MAX_UNIFORM_BUFFER_BINDINGS: 'MAX_UNIFORM_BUFFER_BINDINGS',
396
+ };
397
+ }
398
+
399
+ log.debug('GL CONSTANTS');
400
+ log.debug(this.gl);
401
+ log.debug(this.gl.TEXTURE0);
402
+ log.debug(this.gl.textures);
403
+ log.debug('RENDERER');
404
+ log.debug(this.renderer);
405
+
406
+ this.deviceLimits = {
407
+ maxTextureSize: this.gl.getParameter(this.gl.MAX_TEXTURE_SIZE),
408
+ max3DTextureSize: this.gl.getParameter(this.gl.MAX_3D_TEXTURE_SIZE),
409
+ maxRenderbufferSize: this.gl.getParameter(this.gl.MAX_RENDERBUFFER_SIZE),
410
+ maxUniformBufferBindings: this.gl.getParameter(this.gl.MAX_UNIFORM_BUFFER_BINDINGS),
411
+ };
412
+
413
+ this.zarrStore = {
414
+ resolutions: null, // 6 (the number of resolutions aka. pyramid levels in the file)
415
+ chunkSize: [], // [32, 32, 32]
416
+ shapes: [], // [[795, 1024, 1024], ..., [64, 64, 64], [32, 32, 32]]
417
+ arrays: [], // [array0, array1, array2, array3, array4, array5]
418
+ dtype: '', // 'uint8'
419
+ physicalSizeTotal: [], // [795 x 0.0688, 1024 x 0.03417, 1024 x 0.03417]
420
+ physicalSizeVoxel: [], // [0.0688, 0.03417, 0.03417]
421
+ brickLayout: [], // [[25, 32, 32],[13, 16, 16], ..., [2,2,2],[1,1,1]]
422
+ // store: '', // ref to this.store
423
+ // group: '', // ref to this.group
424
+ channelCount: 1, // MAX 7 TODO: get from zarr metadata
425
+ scales: [], // downsample ratios, [x,y,z] per resolution level
426
+ lowestDataRes: 0, // lowest resolution level with data
427
+ };
428
+
429
+ this.ptTHREE = null;
430
+ this.bcTHREE = null;
431
+
432
+ this.channels = {
433
+ maxChannels: 7, // lower when dataset has fewer, dictates page table size
434
+ zarrMappings: [], // stores the zarr channel index for every one of the up to 7 channels
435
+ colorMappings: [], // stores the PT slot for every color
436
+ downsampleMin: [], // stores the downsample min for every one of the up to 7 channels
437
+ downsampleMax: [], // stores the downsample max for every one of the up to 7 channels
438
+ };
439
+
440
+ this.PT = {
441
+ channelOffsets: [
442
+ [0, 0, 1],
443
+ [0, 1, 0],
444
+ [0, 1, 1],
445
+ [1, 0, 0],
446
+ [1, 0, 1],
447
+ [1, 1, 0],
448
+ [1, 1, 1],
449
+ ],
450
+ anchors: [],
451
+ offsets: [],
452
+ xExtent: 0, // includes the offset inclusive
453
+ yExtent: 0, // includes the offset inclusive
454
+ zExtent: 0, // includes the offset inclusive
455
+ z0Extent: 0, // l0 z extent
456
+ zTotal: 0, // original z extent plus the l0 z extent times the channel count
457
+ };
458
+
459
+ // this.minimumMin = 255;
460
+ // this.maximumMin = 0;
461
+ // this.minimumMax = 255;
462
+ // this.maximumMax = 0;
463
+
464
+ // this.bricksAllocated = 0;
465
+ this.bricksEverLoaded = new Set();
466
+
467
+ // Properties for volume rendering
468
+ // this.originalScale = [1, 1, 1]; // Original dimensions
469
+ // this.physicalScale = [{ size: 1 }, { size: 1 }, { size: 2.1676 }]; // Physical size scaling
470
+ // this.physicalScale = [{ size: 1 }, { size: 1 }, { size: 1 }]; // Physical size scaling
471
+ // this.testArray = [];
472
+ // this.ptArray = [];
473
+ // this.ptManOffsets = [];
474
+
475
+ // top k page table addresses
476
+ // this.requestsStack = [];
477
+ this.isBusy = false;
478
+
479
+ // brick cache structure for internal calculations
480
+ this.BCTimeStamps = new Array(TOTAL_NUM_BRICKS).fill(0);
481
+ this.BCMinMax = new Array(TOTAL_NUM_BRICKS).fill([0, 0]);
482
+ this.BCFull = false;
483
+ this.BCUnusedIndex = 0;
484
+
485
+ // brick cache to page table mapping
486
+ this.bc2pt = new Array(TOTAL_NUM_BRICKS).fill(null);
487
+
488
+ // top k unused brick cache addresses
489
+ this.LRUStack = [];
490
+ // this.LRUReady = false;
491
+ this.triggerUsage = true;
492
+ this.triggerRequest = false;
493
+ this.timeStamp = 0;
494
+ this.k = 40;
495
+ this.noNewRequests = false;
496
+
497
+ this.needsBailout = false; // Flag to indicate if we need to bail out of processing requests
498
+
499
+ // Add initialization status
500
+ this.initStatus = INIT_STATUS.NOT_STARTED;
501
+ this.initError = null;
502
+ logWithColor('VolumeDataManager constructor complete');
503
+ }
504
+
505
+ initImages(images, imageLayerScopes) {
506
+ logWithColor('INIT IMAGES');
507
+ this.images = images;
508
+ this.imageLayerScopes = imageLayerScopes;
509
+ }
510
+
511
+ /**
512
+ * Initialize the VolumeDataManager with Zarr store details and device limits
513
+ * This should be called ONCE at website initialization
514
+ * TODO(mark): merge this with the constructor?
515
+ * @returns {Promise<Object>} Object with Zarr store details and device limits
516
+ */
517
+ async init(config) {
518
+ logWithColor('INIT()');
519
+ // Prevent multiple initializations
520
+ if (this.initStatus !== INIT_STATUS.NOT_STARTED) {
521
+ log.debug('VolumeDataManager init() was called more than once!');
522
+
523
+ if (this.initStatus === INIT_STATUS.COMPLETE) {
524
+ // Return existing initialized data
525
+ return {
526
+ success: true,
527
+ deviceLimits: this.deviceLimits,
528
+ zarrStore: this.zarrStore,
529
+ // physicalScale: this.physicalScale,
530
+ physicalSizeTotal: this.zarrStore.physicalSizeTotal,
531
+ physicalSizeVoxel: this.zarrStore.physicalSizeVoxel,
532
+ error: null,
533
+ };
534
+ }
535
+
536
+ if (this.initStatus === INIT_STATUS.FAILED) {
537
+ return {
538
+ success: false,
539
+ error: this.initError || 'Unknown initialization error',
540
+ };
541
+ }
542
+
543
+ // Still in progress, return a pending state
544
+ return {
545
+ success: false,
546
+ pending: true,
547
+ error: 'Initialization in progress',
548
+ };
549
+ }
550
+
551
+ this.initStatus = INIT_STATUS.IN_PROGRESS;
552
+ logWithColor('INIT() IN PROGRESS');
553
+ try {
554
+ // Query Zarr store details
555
+
556
+ // Get all resolution levels
557
+ const imageWrapper = this.images?.[this.imageLayerScopes?.[0]]?.image?.instance;
558
+
559
+ // TODO(mark): Do not access vivLoader.metadata directly. Generalize to non-NGFF metadata.
560
+ this.ngffMetadata = imageWrapper.vivLoader.metadata;
561
+ log.debug('ngffMetadata', this.ngffMetadata);
562
+
563
+ if (!imageWrapper || imageWrapper.getType() !== 'ome-zarr') {
564
+ throw new Error('Invalid imageWrapper or not an OME-Zarr image');
565
+ }
566
+
567
+ const multiResolutionStats = imageWrapper.getMultiResolutionStats();
568
+ const shapes = _resolutionStatsToShapes(multiResolutionStats);
569
+ const resolutions = multiResolutionStats.length;
570
+
571
+ this.zarrStore.resolutions = resolutions;
572
+
573
+ // TODO: filter to only those resolutions below the 16k x 16x x 4k limit?
574
+ const vivData = imageWrapper.getData();
575
+
576
+ if (!Array.isArray(vivData) || vivData.length < 1) {
577
+ throw new Error('Not a multiresolution loader');
578
+ }
579
+
580
+ if (!isEqual(vivData[0].labels, ['t', 'c', 'z', 'y', 'x'])) {
581
+ // TODO: relax this and remove the error.
582
+ throw new Error('Expected OME-Zarr data with dimensions [t, c, z, y, x]');
583
+ }
584
+
585
+ log.debug('vivData', vivData);
586
+
587
+ // TODO: add a better getter to ImageWrapper to avoid accessing the private _data field here?
588
+ const arrays = vivData.map(resolutionData => resolutionData._data);
589
+ const scales = new Array(resolutions).fill(null);
590
+
591
+ // Try to load each resolution level in order
592
+ /*
593
+ const loadPromises = [];
594
+ for (let i = 0; i < resolutions; i++) {
595
+ loadPromises.push(this.tryLoadResolution(i, arrays));
596
+ }
597
+
598
+ const results = await Promise.all(loadPromises);
599
+
600
+ log.debug('results', results);
601
+
602
+ // Verify all resolutions were loaded successfully
603
+ const failedResolutions = results.filter(r => !r.success);
604
+ if (failedResolutions.length > 0) {
605
+ throw new Error(`Failed to load ${failedResolutions.length} resolution levels: ${failedResolutions.map(r => r.level).join(', ')}`);
606
+ }
607
+
608
+ console.info(`Successfully loaded ${resolutions} resolution levels.`);
609
+ */
610
+
611
+ // console.info('shapes', shapes);
612
+ // console.info('arrays', arrays);
613
+
614
+ // Get the first array for metadata
615
+ if (arrays.length > 0) {
616
+ const array0 = arrays[0];
617
+
618
+ // Update store information
619
+ this.zarrStore = {
620
+ resolutions,
621
+ chunkSize: array0.chunks,
622
+ shapes,
623
+ arrays,
624
+ dtype: array0.dtype,
625
+ physicalSizeTotal: [], // Will be populated if metadata exists
626
+ physicalSizeVoxel: [], // Will be populated if metadata exists
627
+ brickLayout: [], // Calculate from shapes and chunk sizes
628
+ // store: this.store,
629
+ // group: this.group,
630
+ channelCount: shapes[0][1],
631
+ scales,
632
+ };
633
+
634
+ // initializing channel mappings and slots as empty
635
+ this.channels.colorMappings = new Array(Math.min(this.zarrStore.channelCount, 7)).fill(-1);
636
+ this.channels.zarrMappings = new Array(Math.min(this.zarrStore.channelCount, 7)).fill(undefined);
637
+ this.channels.downsampleMin = new Array(Math.min(this.zarrStore.channelCount, 7)).fill(undefined);
638
+ this.channels.downsampleMax = new Array(Math.min(this.zarrStore.channelCount, 7)).fill(undefined);
639
+
640
+ // log.debug('zarrMappings in init', this.channels.zarrMappings);
641
+ // log.debug('downsampleMin in init', this.channels.downsampleMin);
642
+ // log.debug('downsampleMax in init', this.channels.downsampleMax);
643
+
644
+ // Extract physical size information if available
645
+ if (array0.meta && array0.meta.physicalSizes) {
646
+ const { x, y, z } = array0.meta.physicalSizes;
647
+
648
+ // Get size data or use default value of 1
649
+ const zSize = z?.size || 1;
650
+ const ySize = y?.size || 1;
651
+ const xSize = x?.size || 1;
652
+
653
+ this.zarrStore.physicalSizeVoxel = [zSize, ySize, xSize];
654
+
655
+ // log.debug('avail physicalSizeVoxel', this.zarrStore.physicalSizeVoxel);
656
+
657
+ // Calculate total physical size
658
+ if (array0.shape && array0.shape.length >= 5) {
659
+ this.zarrStore.physicalSizeTotal = [
660
+ (array0.shape[2] || 1) * zSize,
661
+ (array0.shape[3] || 1) * ySize,
662
+ (array0.shape[4] || 1) * xSize,
663
+ ];
664
+ }
665
+ } else {
666
+ // log.debug('no physicalSizeVoxel');
667
+ this.zarrStore.physicalSizeVoxel = [1, 1, 1];
668
+ this.zarrStore.physicalSizeTotal = [
669
+ array0.shape[2] || 1,
670
+ array0.shape[3] || 1,
671
+ array0.shape[4] || 1,
672
+ ];
673
+ // log.debug('default physicalSizeVoxel', this.zarrStore.physicalSizeVoxel);
674
+ // log.debug('default physicalSizeTotal', this.zarrStore.physicalSizeTotal);
675
+ }
676
+
677
+ // log.debug('group.attrs', this.group.attrs);
678
+ const { multiscales } = this.ngffMetadata;
679
+
680
+ if (!multiscales) {
681
+ throw new Error('Expected multiscales metadata in group.attrs');
682
+ }
683
+
684
+ // Check the per-resolution coordinate transformations
685
+ if (multiscales?.[0]?.datasets?.[0]?.coordinateTransformations) {
686
+ for (let i = 0; i < resolutions; i++) {
687
+ if (multiscales?.[0]?.datasets?.[i]?.coordinateTransformations?.[0]?.scale) {
688
+ const { scale } = multiscales[0].datasets[i].coordinateTransformations[0];
689
+ scales[i] = [scale[4], scale[3], scale[2]];
690
+ // log.debug('scale', i, scales[i]);
691
+ }
692
+ }
693
+
694
+ // TODO: if the scales are not powers of two, throw an error?
695
+ } else {
696
+ log.error('no coordinateTransformations available, assuming downsampling ratio of 2 per dimension');
697
+ for (let i = 0; i < resolutions; i++) {
698
+ // 2 to the power of i
699
+ const scale = 2 ** i;
700
+ scales[i] = [scale, scale, scale];
701
+ }
702
+ }
703
+
704
+ // log.debug('scales', scales);
705
+ this.zarrStore.scales = scales;
706
+
707
+ // Use the top-level coordinateTransformations scale if available
708
+ const { coordinateTransformations } = this.ngffMetadata;
709
+ if (coordinateTransformations?.[0]?.scale) {
710
+ const { scale } = coordinateTransformations[0];
711
+ // Assuming scale is in format [..., zscale, yscale, xscale]
712
+ const scaleLength = scale.length;
713
+
714
+ if (scaleLength >= 3) {
715
+ const zScale = scale[scaleLength - 3];
716
+ const yScale = scale[scaleLength - 2];
717
+ const xScale = scale[scaleLength - 1];
718
+
719
+ // Update physicalScale
720
+ // this.physicalScale = [
721
+ // { size: xScale },
722
+ // { size: yScale },
723
+ // { size: zScale },
724
+ // ];
725
+
726
+ // log.debug('physicalScale', this.physicalScale);
727
+
728
+ // Update zarrStore's physicalSizeVoxel
729
+ this.zarrStore.physicalSizeVoxel = [zScale, yScale, xScale];
730
+
731
+ // Calculate total physical size if shape exists
732
+ if (array0.shape && array0.shape.length >= 5) {
733
+ this.zarrStore.physicalSizeTotal = [
734
+ (array0.shape[2] || 1) * zScale,
735
+ (array0.shape[3] || 1) * yScale,
736
+ (array0.shape[4] || 1) * xScale,
737
+ ];
738
+ }
739
+
740
+ // log.debug('Using scale from coordinateTransformations:', this.physicalScale);
741
+ }
742
+ }
743
+
744
+
745
+ // Calculate brick layout for each resolution
746
+ this.zarrStore.brickLayout = _resolutionStatsToBrickLayout(
747
+ imageWrapper.getMultiResolutionStats(),
748
+ );
749
+
750
+ // log.debug('zarrStore', this.zarrStore);
751
+
752
+ // init channel mappings
753
+ // log.debug('initializing channel mappings');
754
+ log.debug('config', config);
755
+ // for each key in config, add to channel mappings
756
+ const { omero } = this.ngffMetadata || {};
757
+
758
+ if (!omero) {
759
+ // TODO(mark): do not use omero metadata directly, use the ImageWrapper channel methods.
760
+ throw new Error('Expected omero metadata in ngffMetadata');
761
+ }
762
+
763
+ log.debug('omero', omero);
764
+
765
+ Object.keys(config).forEach((key, i) => {
766
+ const configChannel = config[key].spatialTargetC;
767
+ this.channels.zarrMappings[i] = configChannel;
768
+ this.channels.colorMappings[i] = i;
769
+ this.channels.downsampleMin[i] = omero?.channels?.[configChannel]?.window?.min || 0;
770
+ this.channels.downsampleMax[i] = omero?.channels?.[configChannel]?.window?.max || 65535;
771
+ });
772
+ log.debug('zarrMappings after init', this.channels.zarrMappings);
773
+ log.debug('colorMappings after init', this.channels.colorMappings);
774
+ log.debug('downsampleMin after init', this.channels.downsampleMin);
775
+ log.debug('downsampleMax after init', this.channels.downsampleMax);
776
+
777
+ // Initialize MRMCPT textures after we have all the necessary information
778
+ this.initMRMCPT();
779
+ // this.populateMRMCPT();
780
+ // this.testTexture();
781
+ }
782
+
783
+ this.initStatus = INIT_STATUS.COMPLETE;
784
+ logWithColor('INIT() COMPLETE');
785
+
786
+ // log.debug(this.zarrStore);
787
+
788
+ /*
789
+ if (this.ngffMetadata.coordinateTransformations
790
+ && this.ngffMetadata.coordinateTransformations[0]
791
+ && this.ngffMetadata.coordinateTransformations[0].scale) {
792
+ // log.debug('scale', this.ngffMetadata.coordinateTransformations[0].scale);
793
+ }
794
+ */
795
+
796
+ // Return data that can be displayed in the HUD
797
+ return {
798
+ success: true,
799
+ deviceLimits: this.deviceLimits,
800
+ zarrStore: this.zarrStore,
801
+ // physicalScale: this.physicalScale,
802
+ physicalSizeTotal: this.zarrStore.physicalSizeTotal,
803
+ physicalSizeVoxel: this.zarrStore.physicalSizeVoxel,
804
+ error: null,
805
+ };
806
+ } catch (error) {
807
+ logWithColor('INIT() FAILED');
808
+ log.error('Error initializing VolumeDataManager:', error);
809
+ this.initStatus = INIT_STATUS.FAILED;
810
+ this.initError = error.message || 'Unknown error';
811
+
812
+ return {
813
+ success: false,
814
+ error: this.initError,
815
+ };
816
+ }
817
+ }
818
+
819
+ /**
820
+ * Initialize the BrickCache and PageTable
821
+ * MRMCPT: multi-resolution multi-channel page table
822
+ *
823
+ * Depends on:
824
+ * - zarrStore.brickLayout
825
+ * - zarrMappings.length (zarrMappings: the zarr channel index for every one of the up to 7 channels)
826
+ * -
827
+ */
828
+ initMRMCPT() {
829
+ logWithColor('initMRMCPT');
830
+
831
+ // log.debug('initMRMCPT', this.zarrStore.shapes[0]);
832
+ // log.debug('initMRMCPT', this.zarrStore.channelCount);
833
+ // log.debug('initMRMCPT', this.channels.zarrMappings);
834
+ // log.debug('initMRMCPT', this.channels.colorMappings);
835
+
836
+ const { PT, ptTHREE, bcTHREE } = _initMRMCPT(
837
+ this.zarrStore.brickLayout,
838
+ this.channels.zarrMappings.length,
839
+ );
840
+
841
+ this.PT = PT;
842
+ this.ptTHREE = ptTHREE;
843
+ this.bcTHREE = bcTHREE;
844
+
845
+ // log.debug('gl', this.gl);
846
+ // log.debug('renderer', this.renderer);
847
+
848
+ logWithColor('initMRMCPT() COMPLETE');
849
+ }
850
+
851
+ /*
852
+ testTexture() {
853
+ log.debug('testTexture pt', this.ptTHREE);
854
+ log.debug('testTexture bc', this.bcTHREE);
855
+ }
856
+ */
857
+
858
+ async initTexture() {
859
+ const requests = [
860
+ { x: 0, y: 0, z: 1 },
861
+ ];
862
+ await this.handleBrickRequests(requests);
863
+ }
864
+
865
+ updateChannels(channelProps) {
866
+ logWithColor('updateChannels');
867
+ log.debug('channelProps', channelProps);
868
+
869
+ // log.error('TODO: init channel mappings first ');
870
+ log.debug('this.channels.zarrMappings', this.channels.zarrMappings);
871
+ log.debug('this.channels.colorMappings', this.channels.colorMappings);
872
+ log.debug('this.channels.downsampleMin', this.channels.downsampleMin);
873
+ log.debug('this.channels.downsampleMax', this.channels.downsampleMax);
874
+
875
+ if (this.channels.zarrMappings.length === 0) {
876
+ log.debug('channels not initialized yet');
877
+ return;
878
+ }
879
+
880
+ // Early exit check: compare requested mappings with current mappings
881
+ const requestedZarrChannels = Object.values(channelProps)
882
+ .map(channelData => channelData.spatialTargetC)
883
+ .filter(targetC => targetC !== undefined);
884
+
885
+ const currentZarrChannels = this.channels.zarrMappings
886
+ .filter(mapping => mapping !== undefined);
887
+
888
+ // Create sorted arrays for comparison
889
+ const requestedSorted = [...new Set(requestedZarrChannels)].sort((a, b) => a - b);
890
+ const currentSorted = [...new Set(currentZarrChannels)].sort((a, b) => a - b);
891
+
892
+ // Early exit if mappings haven't changed
893
+ if (requestedSorted.length === currentSorted.length
894
+ && requestedSorted.every((val, index) => val === currentSorted[index])) {
895
+ log.debug('Channel mappings unchanged, skipping update');
896
+ // return;
897
+ }
898
+
899
+ log.debug('Channel mappings changed:', {
900
+ current: currentSorted,
901
+ requested: requestedSorted,
902
+ });
903
+
904
+ // Instead of for...of loop, use Object.entries() with forEach
905
+ Object.entries(channelProps).forEach(([uiChannelKey, channelData]) => {
906
+ const targetZarrChannel = channelData.spatialTargetC;
907
+
908
+ log.debug(`UI channel "${uiChannelKey}" wants zarr channel ${targetZarrChannel}`);
909
+
910
+ // Check if this zarr channel is already mapped
911
+ const existingSlotIndex = this.channels.zarrMappings.indexOf(targetZarrChannel);
912
+
913
+ if (existingSlotIndex === -1) {
914
+ // Need to allocate a new slot for this zarr channel
915
+ const nextFreeSlot = this.channels.zarrMappings.findIndex(slot => slot === undefined);
916
+ if (nextFreeSlot !== -1) {
917
+ this.channels.zarrMappings[nextFreeSlot] = targetZarrChannel;
918
+ log.debug('channelData', channelData);
919
+ log.debug('this.ngffMetadata?.omero?.channels', this.ngffMetadata?.omero?.channels);
920
+ log.debug('targetZarrChannel', targetZarrChannel);
921
+ this.channels.downsampleMin[nextFreeSlot] = this.ngffMetadata?.omero?.channels?.[targetZarrChannel]?.window?.min || 0;
922
+ this.channels.downsampleMax[nextFreeSlot] = this.ngffMetadata?.omero?.channels?.[targetZarrChannel]?.window?.max || 65535;
923
+ log.debug(`Mapped zarr channel ${targetZarrChannel} to slot ${nextFreeSlot}`);
924
+ log.debug('channels', this.channels);
925
+ } else {
926
+ log.debug('No free slots found, looking for unused mapped channels');
927
+
928
+ // Find zarr channels that are currently mapped but no longer requested
929
+ const currentlyMapped = this.channels.zarrMappings.filter(mapping => mapping !== undefined);
930
+ const stillRequested = requestedZarrChannels; // We calculated this earlier
931
+ const unusedMappedChannels = currentlyMapped.filter(mappedChannel => !stillRequested.includes(mappedChannel));
932
+
933
+ log.debug('Currently mapped:', currentlyMapped);
934
+ log.debug('Still requested:', stillRequested);
935
+ log.debug('Unused mapped channels:', unusedMappedChannels);
936
+
937
+ if (unusedMappedChannels.length > 0) {
938
+ // Find the first slot that maps to an unused zarr channel
939
+ const slotToReuse = this.channels.zarrMappings.findIndex(mapping => unusedMappedChannels.includes(mapping));
940
+
941
+ if (slotToReuse !== -1) {
942
+ const oldZarrChannel = this.channels.zarrMappings[slotToReuse];
943
+ this.channels.zarrMappings[slotToReuse] = targetZarrChannel;
944
+ this.channels.downsampleMin[slotToReuse] = this.ngffMetadata?.omero?.channels?.[targetZarrChannel]?.window?.min || 0;
945
+ this.channels.downsampleMax[slotToReuse] = this.ngffMetadata?.omero?.channels?.[targetZarrChannel]?.window?.max || 65535;
946
+ log.debug(`Reused slot ${slotToReuse}: ${oldZarrChannel} -> ${targetZarrChannel}`);
947
+
948
+ this._purgeChannel(slotToReuse);
949
+ } else {
950
+ log.error('Could not find slot to reuse - this should not happen');
951
+ }
952
+ } else {
953
+ log.error('All slots are full and all mapped channels are still in use');
954
+ }
955
+ }
956
+ } else {
957
+ log.debug(`Zarr channel ${targetZarrChannel} already mapped to slot ${existingSlotIndex}`);
958
+ }
959
+ });
960
+
961
+ const newColorMappings = Object.values(channelProps).map((ch) => {
962
+ const slot = this.channels.zarrMappings.indexOf(ch.spatialTargetC);
963
+ return slot !== -1 ? slot : -1;
964
+ });
965
+
966
+ while (newColorMappings.length < 7) {
967
+ newColorMappings.push(-1);
968
+ }
969
+
970
+ log.debug('newColorMappings', newColorMappings);
971
+ this.channels.colorMappings = newColorMappings;
972
+
973
+ log.debug('updatedChannels', this.channels);
974
+ }
975
+
976
+ /**
977
+ * Try to load a resolution level
978
+ * @param {number} resolutionIndex - The resolution level to load
979
+ * @param {Array} arrays - Array to store the loaded arrays
980
+ * @returns {Promise} Promise resolving when the resolution is loaded or rejected
981
+ */
982
+ /*
983
+ async tryLoadResolution(resolutionIndex, arrays) {
984
+ logWithColor('tryLoadResolution');
985
+ log.debug(resolutionIndex, arrays);
986
+ try {
987
+ const array = await zarrita.open(this.group.resolve(String(resolutionIndex)));
988
+ // Create new arrays to avoid modifying parameters directly
989
+ const newArrays = [...arrays];
990
+ newArrays[resolutionIndex] = array;
991
+ // Update the original arrays
992
+ Object.assign(arrays, newArrays);
993
+ logWithColor('tryLoadResolution() COMPLETE');
994
+ return { success: true, level: resolutionIndex };
995
+ } catch (err) {
996
+ log.error(`Failed to load resolution ${resolutionIndex}:`, err);
997
+ return { success: false, level: resolutionIndex, error: err.message };
998
+ }
999
+ }
1000
+ */
1001
+
1002
+ /**
1003
+ * Get physical dimensions
1004
+ * @returns {Array} Physical dimensions [X, Y, Z]
1005
+ */
1006
+ getPhysicalDimensionsXYZ() {
1007
+ log.debug('getPhysicalDimensionsXYZ');
1008
+ log.debug('this.zarrStore.physicalSizeTotal', this.zarrStore.physicalSizeTotal);
1009
+ const out = [this.zarrStore.physicalSizeTotal[2],
1010
+ this.zarrStore.physicalSizeTotal[1],
1011
+ this.zarrStore.physicalSizeTotal[0]];
1012
+ log.debug('out', out);
1013
+ return out;
1014
+ }
1015
+
1016
+ /**
1017
+ * Get the maximum resolution
1018
+ * @returns {number} Maximum resolution
1019
+ */
1020
+ getMaxResolutionXYZ() {
1021
+ log.debug('getMaxResolutionXYZ');
1022
+ log.debug('this.zarrStore.shapes', this.zarrStore.shapes);
1023
+ const out = [this.zarrStore.shapes[0][4],
1024
+ this.zarrStore.shapes[0][3],
1025
+ this.zarrStore.shapes[0][2]];
1026
+ log.debug('out', out);
1027
+ return out;
1028
+ }
1029
+
1030
+ getOriginalScaleXYZ() {
1031
+ logWithColor('getOriginalScaleXYZ');
1032
+ log.debug('this.zarrStore.physicalSizeVoxel', this.zarrStore.physicalSizeVoxel);
1033
+ const out = [this.zarrStore.physicalSizeVoxel[2],
1034
+ this.zarrStore.physicalSizeVoxel[1],
1035
+ this.zarrStore.physicalSizeVoxel[0]];
1036
+ log.debug('out', out);
1037
+ return out;
1038
+ }
1039
+
1040
+ getNormalizedScaleXYZ() {
1041
+ log.debug('getNormalizedScaleXYZ');
1042
+ const out = [
1043
+ 1.0,
1044
+ this.zarrStore.physicalSizeVoxel[1] / this.zarrStore.physicalSizeVoxel[2],
1045
+ this.zarrStore.physicalSizeVoxel[0] / this.zarrStore.physicalSizeVoxel[0],
1046
+ ];
1047
+ log.debug('out', out);
1048
+ return out;
1049
+ }
1050
+
1051
+ getBoxDimensionsXYZ() {
1052
+ log.debug('getBoxDimensionsXYZ');
1053
+ log.debug('this.zarrStore.shapes', this.zarrStore.shapes);
1054
+ const out = [1,
1055
+ this.zarrStore.shapes[0][3] / this.zarrStore.shapes[0][4],
1056
+ this.zarrStore.shapes[0][2] / this.zarrStore.shapes[0][4],
1057
+ ];
1058
+ log.debug('out', out);
1059
+ return out;
1060
+ }
1061
+
1062
+ /**
1063
+ * Load a specific Zarr chunk based on [t,c,z,y,x] coordinates
1064
+ * @param {number} t - Time point (default 0)
1065
+ * @param {number} c - Channel (default 0)
1066
+ * @param {number} z - Z coordinate
1067
+ * @param {number} y - Y coordinate
1068
+ * @param {number} x - X coordinate
1069
+ * @param {number} resolution - Resolution level
1070
+ * @returns {Promise<Uint8Array>} 32x32x32 chunk data
1071
+ */
1072
+ async loadZarrChunk(t = 0, c = 0, z, y, x, resolution) {
1073
+ // logWithColor('loadZarrChunk');
1074
+ if (!this.zarrStore || !this.zarrStore.arrays[resolution]) {
1075
+ throw new Error('Zarr store or resolution not initialized');
1076
+ }
1077
+
1078
+ // log.debug('loadZarrChunk', t, c, z, y, x, resolution);
1079
+
1080
+ const array = this.zarrStore.arrays[resolution];
1081
+
1082
+ // TODO: Use zarrita's get(arr, slice) method (rather than arr.getChunk),
1083
+ // which will enable reading from arrays with other chunk shapes
1084
+ // (i.e., not strictly 32x32x32 chunks).
1085
+ // To do so, will need to determine the slice indices corresponding
1086
+ // to each "assumed 32x32x32 chunk key".
1087
+ // Reference: https://zarrita.dev/slicing.html
1088
+
1089
+ const chunkEntry = await array.getChunk([t, c, z, y, x]);
1090
+
1091
+ // log.debug('chunkEntry', chunkEntry);
1092
+
1093
+ if (!chunkEntry) {
1094
+ throw new Error(`No chunk found at coordinates [${t},${c},${z},${y},${x}]`);
1095
+ }
1096
+
1097
+ // Ensure the chunk is the expected size
1098
+ if (chunkEntry.data.length !== BRICK_SIZE * BRICK_SIZE * BRICK_SIZE) {
1099
+ throw new Error(`Unexpected chunk size: ${chunkEntry.data.length}`);
1100
+ }
1101
+
1102
+ return chunkEntry.data;
1103
+ }
1104
+
1105
+ async processRequestData(buffer) {
1106
+ if (this.isBusy) {
1107
+ log.debug('processRequestData: already busy, skipping');
1108
+ return;
1109
+ }
1110
+ this.isBusy = true;
1111
+ this.triggerRequest = false;
1112
+
1113
+ const { requests, origRequestCount } = _requestBufferToRequestObjects(buffer, this.k);
1114
+
1115
+ if (requests.length === 0) {
1116
+ this.noNewRequests = true;
1117
+ }
1118
+ log.debug(`processRequestData: handling ${requests.length} requests of ${origRequestCount}`);
1119
+ await this.handleBrickRequests(requests);
1120
+
1121
+ // We want processRequestData to alternate with processUsageData.
1122
+ this.triggerUsage = true;
1123
+ this.isBusy = false;
1124
+ }
1125
+
1126
+
1127
+ async processUsageData(buffer) {
1128
+ if (this.isBusy) {
1129
+ log.debug('processUsageData: already busy, skipping');
1130
+ this.needsBailout = true; // Set a flag to indicate we need to bail out of processing requests
1131
+ return;
1132
+ }
1133
+ this.isBusy = true;
1134
+ this.triggerUsage = false;
1135
+
1136
+ const now = ++this.timeStamp;
1137
+ const usedBricks = new Set();
1138
+
1139
+ // Process buffer to identify used bricks
1140
+ for (let i = 0; i < buffer.length; i += 4) {
1141
+ const x = buffer[i];
1142
+ const y = buffer[i + 1];
1143
+ const z = buffer[i + 2];
1144
+
1145
+ // Skip empty entries
1146
+ if ((x | y | z) === 0) {
1147
+ // eslint-disable-next-line no-continue
1148
+ continue;
1149
+ }
1150
+
1151
+ // Calculate brick index
1152
+ const bcIndex = z * BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y + y * BRICK_CACHE_SIZE_X + x;
1153
+
1154
+ // Only add valid indices
1155
+ if (bcIndex < this.BCTimeStamps.length) {
1156
+ usedBricks.add(bcIndex);
1157
+ }
1158
+ }
1159
+ // Update timestamps for all used bricks at once
1160
+ Array.from(usedBricks).forEach((bcIndex) => {
1161
+ this.BCTimeStamps[bcIndex] = now;
1162
+ });
1163
+
1164
+ // Check if brick cache is full based on allocation index
1165
+ // this.BCFull = this.BCUnusedIndex >= this.BCTimeStamps.length;
1166
+ // If cache is full or we want to always maintain the LRU list
1167
+ // if (this.BCFull || this.BCUnusedIndex >= this.BCTimeStamps.length - this.k) { // Always update LRU list
1168
+
1169
+ // do NOT overwrite BCFull; it is latched in _allocateBCSlots()
1170
+ if (this.BCFull) this._buildLRU(); // The brick cache is full, so update the list of least recently used bricks, as it will be needed for eviction during the next _allocateBCSlots
1171
+
1172
+ // log.debug('this.BCTimeStamps', this.BCTimeStamps);
1173
+ // log.debug('this.timeStamp', this.timeStamp);
1174
+
1175
+ // We want processRequestData to alternate with processUsageData.
1176
+ this.triggerRequest = true;
1177
+ this.isBusy = false;
1178
+ }
1179
+
1180
+ // Helper method to update PT entries for evicted bricks
1181
+ _evictBrick(bcIndex) {
1182
+ const pt = this.bc2pt[bcIndex];
1183
+ if (!pt) return; // brick was never resident
1184
+
1185
+ const [min, max] = this.BCMinMax[bcIndex] || [0, 0];
1186
+ const ptVal = (
1187
+ (0 << 31) // not-resident
1188
+ | (1 << 30) // init-done
1189
+ | (Math.min(127, min >> 1) << 23)
1190
+ | (Math.min(127, max >> 1) << 16)
1191
+ ) >>> 0;
1192
+
1193
+ this._updatePTEntry(pt.x, pt.y, pt.z, ptVal); // ← PT coord!
1194
+ this.bc2pt[bcIndex] = null; // slot now free
1195
+ }
1196
+
1197
+ _purgeChannel(ptChannelIndex) {
1198
+ log.debug('purging channel', ptChannelIndex);
1199
+ log.debug('corresponding zarr channel', this.channels.zarrMappings[ptChannelIndex]);
1200
+
1201
+ if (!this.ptTHREE) {
1202
+ log.error('pagetable texture not initialized');
1203
+ return;
1204
+ }
1205
+
1206
+ this.channels.downsampleMin[ptChannelIndex] = undefined;
1207
+ this.channels.downsampleMax[ptChannelIndex] = undefined;
1208
+ this.channels.zarrMappings[ptChannelIndex] = undefined;
1209
+
1210
+ const channelMask = this.PT.channelOffsets[ptChannelIndex];
1211
+
1212
+ log.debug('channelMask', channelMask);
1213
+ log.error('TODO: not tested yet');
1214
+
1215
+ const { gl } = this;
1216
+ const texPT = this.renderer.properties.get(this.ptTHREE).__webglTexture;
1217
+ gl.activeTexture(gl.TEXTURE0);
1218
+ gl.bindTexture(gl.TEXTURE_3D, texPT);
1219
+
1220
+ for (let r = 0; r < this.zarrStore.resolutions; r++) {
1221
+ const anchor = [
1222
+ this.PT.anchors[r][2] * channelMask[2],
1223
+ this.PT.anchors[r][1] * channelMask[1],
1224
+ this.PT.anchors[r][0] * channelMask[0],
1225
+ ];
1226
+ log.debug('anchor', anchor);
1227
+ const extents = this.zarrStore.brickLayout[r];
1228
+ const size = extents[0] * extents[1] * extents[2];
1229
+ log.debug('extents', extents);
1230
+ log.debug('size', size);
1231
+ // texsub 3D
1232
+ // replace with a block of zeros
1233
+ gl.texSubImage3D(
1234
+ gl.TEXTURE_3D, 0,
1235
+ anchor[0], anchor[1], anchor[2],
1236
+ extents[0], extents[1], extents[2],
1237
+ gl.RED_INTEGER, gl.UNSIGNED_INT,
1238
+ new Uint32Array(size),
1239
+ );
1240
+ }
1241
+ gl.bindTexture(gl.TEXTURE_3D, null);
1242
+ }
1243
+
1244
+ // Update a PT entry
1245
+ _updatePTEntry(ptX, ptY, ptZ, ptVal) {
1246
+ if (!this.ptTHREE) return;
1247
+
1248
+ const { gl } = this;
1249
+ const texPT = this.renderer.properties.get(this.ptTHREE).__webglTexture;
1250
+ gl.activeTexture(gl.TEXTURE0);
1251
+ gl.bindTexture(gl.TEXTURE_3D, texPT);
1252
+ gl.texSubImage3D(
1253
+ gl.TEXTURE_3D, 0,
1254
+ ptX, ptY, ptZ,
1255
+ 1, 1, 1,
1256
+ gl.RED_INTEGER, gl.UNSIGNED_INT,
1257
+ new Uint32Array([ptVal]),
1258
+ );
1259
+ gl.bindTexture(gl.TEXTURE_3D, null);
1260
+ }
1261
+
1262
+ /* ------------------------------------------------------------- *
1263
+ * 2. Allocate the next n free bricks in the brick cache *
1264
+ * ------------------------------------------------------------- */
1265
+ /**
1266
+ *
1267
+ * @param {number} n The number of slots to allocate
1268
+ * @returns {{ bcIndex, x, y, z }[]} Array of brick cache coordinates for the allocated slots.
1269
+ */
1270
+ _allocateBCSlots(n) {
1271
+ let slots = [];
1272
+ const total = BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y * BRICK_CACHE_SIZE_Z;
1273
+
1274
+ if (!this.BCFull && this.BCUnusedIndex + n > total) {
1275
+ // The brick cache was not already full,
1276
+ // but allocating N more bricks will put us over the limit.
1277
+ this.BCFull = true;
1278
+ log.debug('BRICK CACHE FULL');
1279
+ }
1280
+ // Do the allocation.
1281
+ if (!this.BCFull) {
1282
+ for (let i = 0; i < n; ++i) {
1283
+ const bcIndex = (this.BCUnusedIndex + i) % total; // wrap for now
1284
+ const z = Math.floor(bcIndex / (BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y));
1285
+ const rem = bcIndex - z * BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y;
1286
+ const y = Math.floor(rem / BRICK_CACHE_SIZE_X);
1287
+ const x = rem % BRICK_CACHE_SIZE_X;
1288
+
1289
+ slots.push({ bcIndex, x, y, z });
1290
+ }
1291
+ this.BCUnusedIndex += n;
1292
+ } else { // ---- BC already full ----
1293
+ // Before proceeding to choose old slots from the LRU stack,
1294
+ // we need to ensure that the LRU stack is at least as long as N
1295
+ // (since N is the number of new brick slots to allocate).
1296
+ if (this.LRUStack.length < n) this._buildLRU(); // top-up list
1297
+ // Use the LRU stack to evict the least-recently-used bricks
1298
+ // and return their coordinates for the new bricks to use.
1299
+ slots = this.LRUStack.splice(0, n).map((bcIndex) => {
1300
+ this._evictBrick(bcIndex);
1301
+ const z = Math.floor(bcIndex / (BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y));
1302
+ const rem = bcIndex - z * BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y;
1303
+ const y = Math.floor(rem / BRICK_CACHE_SIZE_X);
1304
+ const x = rem % BRICK_CACHE_SIZE_X;
1305
+ return { bcIndex, x, y, z };
1306
+ });
1307
+ }
1308
+ return slots;
1309
+ }
1310
+
1311
+ /* ------------------------------------------------------------- *
1312
+ * 4. Upload one brick + PT entry *
1313
+ * ------------------------------------------------------------- */
1314
+ async _uploadBrick(ptCoord, bcSlot) {
1315
+ // log.debug('uploading brick', ptCoord, bcSlot);
1316
+
1317
+ if (ptCoord.x >= this.PT.xExtent
1318
+ || ptCoord.y >= this.PT.yExtent
1319
+ || ptCoord.z >= this.PT.zTotal
1320
+ || ptCoord.x < 0
1321
+ || ptCoord.y < 0
1322
+ || ptCoord.z < 0
1323
+ ) {
1324
+ log.error('this.PT', this.PT);
1325
+ log.error('ptCoord out of bounds', ptCoord);
1326
+ return;
1327
+ }
1328
+
1329
+ // log.debug('ptCoord', ptCoord);
1330
+ // log.debug('this.PT', this.PT);
1331
+
1332
+
1333
+ /* 4.1 fetch chunk from Zarr */
1334
+ const { channel, resolution, x, y, z } = _ptToZarr(
1335
+ ptCoord.x, ptCoord.y, ptCoord.z,
1336
+ { PT_zExtent: this.PT.zExtent, PT_z0Extent: this.PT.z0Extent, PT_anchors: this.PT.anchors },
1337
+ );
1338
+
1339
+ const zarrChannel = this.channels.zarrMappings[channel];
1340
+
1341
+ if (zarrChannel === undefined || zarrChannel === -1) {
1342
+ log.error('zarrChannel is undefined or -1', zarrChannel);
1343
+ return;
1344
+ }
1345
+
1346
+ log.debug('starting to load zarr chunk', { resolution, z, y, x, zarrChannel });
1347
+ let chunk = await this.loadZarrChunk(0, zarrChannel, z, y, x, resolution);
1348
+ // log.debug('chunk', chunk);
1349
+
1350
+ // TODO(mark): is there a better way than iterating over all of the chunk values?
1351
+ if (chunk instanceof Uint16Array) {
1352
+ log.debug('chunk is Uint16Array, converting to Uint8Array');
1353
+ if (this.channels.downsampleMin[channel] === undefined) {
1354
+ // get the channel ID from this.channels.zarrMappings
1355
+ const channelId = this.channels.zarrMappings[channel];
1356
+ log.debug('channelId was not found in this.channels.downsampleMin[channel]', channelId);
1357
+ // get the downsample min and max for the channel from omero
1358
+ this.channels.downsampleMin[channel] = this.ngffMetadata?.omero?.channels?.[channelId]?.window?.min || 0;
1359
+ this.channels.downsampleMax[channel] = this.ngffMetadata?.omero?.channels?.[channelId]?.window?.max || 65535;
1360
+
1361
+ log.debug('this.channels.downsampleMin[channel]', this.channels.downsampleMin[channel]);
1362
+ log.debug('this.channels.downsampleMax[channel]', this.channels.downsampleMax[channel]);
1363
+ }
1364
+ // TODO(mark): to reduce memory usage, create the Uint8Array as a view of the Uint16Array
1365
+ // and use bitwise operations. Then scale the values.
1366
+ const uint8Chunk = new Uint8Array(chunk.length);
1367
+ for (let i = 0; i < chunk.length; i++) {
1368
+ // Scale from 0-65535 to 0-255
1369
+ uint8Chunk[i] = Math.floor((chunk[i] - this.channels.downsampleMin[channel]) / (this.channels.downsampleMax[channel] - this.channels.downsampleMin[channel]) * 255);
1370
+ }
1371
+ chunk = uint8Chunk;
1372
+ }
1373
+
1374
+ if (!(chunk instanceof Uint8Array)) {
1375
+ throw new Error(`Unsupported chunk type: ${chunk.constructor.name}. Expected Uint8Array.`);
1376
+ }
1377
+
1378
+ let min = 255;
1379
+ let max = 0;
1380
+
1381
+ for (let i = 0; i < chunk.length; ++i) {
1382
+ const v = chunk[i];
1383
+ if (v < min) min = v;
1384
+ if (v > max) max = v;
1385
+ }
1386
+
1387
+ /* 4.3 brick‑cache upload */
1388
+ const { gl } = this;
1389
+ const texBC = this.renderer.properties.get(this.bcTHREE).__webglTexture;
1390
+ gl.activeTexture(gl.TEXTURE2);
1391
+ gl.bindTexture(gl.TEXTURE_3D, texBC);
1392
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
1393
+ gl.texSubImage3D(
1394
+ gl.TEXTURE_3D, 0,
1395
+ bcSlot.x * BRICK_SIZE,
1396
+ bcSlot.y * BRICK_SIZE,
1397
+ bcSlot.z * BRICK_SIZE,
1398
+ BRICK_SIZE, BRICK_SIZE, BRICK_SIZE,
1399
+ gl.RED, gl.UNSIGNED_BYTE, chunk,
1400
+ );
1401
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
1402
+ gl.bindTexture(gl.TEXTURE_3D, null);
1403
+ // log.debug('uploaded brick');
1404
+
1405
+ const error = gl.getError();
1406
+ if (error !== gl.NO_ERROR) {
1407
+ log.error('WebGL error during brick upload:', error, chunk);
1408
+ }
1409
+
1410
+ /* 4.4 PT entry upload */
1411
+ if (channel >= this.channels.zarrMappings.length) {
1412
+ log.debug('channel is out of bounds', channel);
1413
+ min = 255;
1414
+ max = 255;
1415
+ }
1416
+
1417
+ const ptVal = _packPT(min, max, bcSlot.x, bcSlot.y, bcSlot.z);
1418
+ const texPT = this.renderer.properties.get(this.ptTHREE).__webglTexture;
1419
+ gl.activeTexture(gl.TEXTURE0);
1420
+ gl.bindTexture(gl.TEXTURE_3D, texPT);
1421
+ gl.texSubImage3D(
1422
+ gl.TEXTURE_3D, 0,
1423
+ ptCoord.x, ptCoord.y, ptCoord.z,
1424
+ 1, 1, 1,
1425
+ gl.RED_INTEGER, gl.UNSIGNED_INT,
1426
+ new Uint32Array([ptVal]),
1427
+ );
1428
+ gl.bindTexture(gl.TEXTURE_3D, null);
1429
+ // log.debug('uploaded PT entry');
1430
+ // log.debug('ptVal', ptVal);
1431
+ // log.debug('min', min);
1432
+ // log.debug('max', max);
1433
+ // log.debug('bcSlot', bcSlot);
1434
+ // log.debug('channel', channel);
1435
+ // log.debug('resolution', resolution);
1436
+ // log.debug('x', x);
1437
+ // log.debug('y', y);
1438
+ // log.debug('z', z);
1439
+ // log.debug('ptCoord', ptCoord);
1440
+
1441
+ const error2 = gl.getError();
1442
+ if (error2 !== gl.NO_ERROR) {
1443
+ log.error('WebGL error during pagetable upload:', error2, chunk);
1444
+ }
1445
+
1446
+ /* 4.5 bookkeeping */
1447
+ // const now = ++this.timeStamp;
1448
+ this.BCTimeStamps[bcSlot.bcIndex] = this.timeStamp;
1449
+ this.BCMinMax[bcSlot.bcIndex] = [min, max];
1450
+ this.bc2pt[bcSlot.bcIndex] = ptCoord;
1451
+ // log.debug('bookkeeping');
1452
+ }
1453
+
1454
+ /* ------------------------------------------------------------- *
1455
+ * 5. Public: handle a batch of PT requests (array of {x,y,z}) *
1456
+ * ------------------------------------------------------------- */
1457
+ async handleBrickRequests(ptRequests) {
1458
+ // log.debug('handleBrickRequests');
1459
+ if (ptRequests.length === 0) return;
1460
+ // log.debug('ptRequests', ptRequests);
1461
+ // log.debug('this.BCFull', this.BCFull);
1462
+ // log.debug('this.BCUnusedIndex', this.BCUnusedIndex);
1463
+ // log.debug('this.BCTimeStamps', this.BCTimeStamps);
1464
+ // log.debug('this.LRUStack', this.LRUStack);
1465
+
1466
+ /* <= k requests, allocate same number of bricks */
1467
+ const slots = this._allocateBCSlots(ptRequests.length);
1468
+ // log.debug('slots', slots);
1469
+
1470
+ /* upload each (sequentially or Promise.all if you prefer IO overlap) */
1471
+ log.debug('handleBrickRequests: starting for loop');
1472
+ for (let i = 0; i < ptRequests.length; ++i) {
1473
+ log.debug('uploading brick', ptRequests[i], slots[i]);
1474
+ // eslint-disable-next-line no-await-in-loop
1475
+ await this._uploadBrick(ptRequests[i], slots[i]);
1476
+ // this.bricksAllocated++;
1477
+ const rlength = this.bricksEverLoaded.size;
1478
+ this.bricksEverLoaded.add(`${ptRequests[i].x},${ptRequests[i].y},${ptRequests[i].z}`);
1479
+ if (rlength === this.bricksEverLoaded.size) {
1480
+ log.debug('DUPLICATE BRICK LOADED', ptRequests[i]);
1481
+ }
1482
+
1483
+ if (this.needsBailout) {
1484
+ log.debug('Bailing out of handleBrickRequests early due to needsBailout flag');
1485
+ this.needsBailout = false; // Reset the flag
1486
+ break; // Exit the loop early
1487
+
1488
+ // TODO(mark): do something with the allocated slots that were not used?
1489
+ }
1490
+ }
1491
+ // log.debug('this.bricksAllocated', this.bricksAllocated);
1492
+ log.debug('this.bricksEverLoaded', this.bricksEverLoaded);
1493
+ // log.debug('uploaded bricks');
1494
+
1495
+ /* let Three.js know textures changed */
1496
+ // this.bcTHREE.needsUpdate = true;
1497
+ // this.ptTHREE.needsUpdate = true;
1498
+ }
1499
+
1500
+ /* --------------------------------------------------------- *
1501
+ * Rebuild the LRUStack with the k least-recently-used bricks *
1502
+ * --------------------------------------------------------- */
1503
+ _buildLRU() {
1504
+ // Update this.LRUStack based on BCTimeStamps.
1505
+ // BCTimeStamps is updated during both initial brick upload and processUsageData.
1506
+ // However it is not until _buildLRU is called that the timestamps are sorted
1507
+ // to update this.LRUStack.
1508
+ const brickIndicesWithTimes = this.BCTimeStamps
1509
+ .map((time, index) => ({ index, time }));
1510
+ this.LRUStack = brickIndicesWithTimes
1511
+ .sort((a, b) => a.time - b.time)
1512
+ .slice(0, this.k)
1513
+ .map(item => item.index);
1514
+ }
1515
+ }