@aics/vole-core 3.12.4 → 3.13.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 (35) hide show
  1. package/README.md +17 -12
  2. package/es/View3d.js +7 -2
  3. package/es/VolumeRenderSettings.js +11 -0
  4. package/es/loaders/TiffLoader.js +3 -1
  5. package/es/types/NaiveSurfaceNets.d.ts +1 -1
  6. package/es/types/RayMarchedAtlasVolume.d.ts +1 -1
  7. package/es/types/ThreeJsPanel.d.ts +2 -2
  8. package/es/types/TrackballControls.d.ts +1 -1
  9. package/es/types/VolumeDrawable.d.ts +1 -1
  10. package/es/types/VolumeRenderImpl.d.ts +1 -1
  11. package/es/types/index.d.ts +1 -1
  12. package/es/types/workers/VolumeLoaderContext.d.ts +9 -13
  13. package/es/types/workers/types.d.ts +25 -16
  14. package/es/workers/VolumeLoadWorker.js +54 -32
  15. package/es/workers/VolumeLoaderContext.js +52 -51
  16. package/es/workers/types.js +17 -7
  17. package/package.json +13 -13
  18. package/es/test/ChunkPrefetchIterator.test.js +0 -208
  19. package/es/test/RequestQueue.test.js +0 -442
  20. package/es/test/SubscribableRequestQueue.test.js +0 -244
  21. package/es/test/VolumeCache.test.js +0 -118
  22. package/es/test/VolumeRenderSettings.test.js +0 -71
  23. package/es/test/lut.test.js +0 -671
  24. package/es/test/num_utils.test.js +0 -140
  25. package/es/test/volume.test.js +0 -98
  26. package/es/test/zarr_utils.test.js +0 -358
  27. package/es/types/test/ChunkPrefetchIterator.test.d.ts +0 -1
  28. package/es/types/test/RequestQueue.test.d.ts +0 -1
  29. package/es/types/test/SubscribableRequestQueue.test.d.ts +0 -1
  30. package/es/types/test/VolumeCache.test.d.ts +0 -1
  31. package/es/types/test/VolumeRenderSettings.test.d.ts +0 -1
  32. package/es/types/test/lut.test.d.ts +0 -1
  33. package/es/types/test/num_utils.test.d.ts +0 -1
  34. package/es/types/test/volume.test.d.ts +0 -1
  35. package/es/types/test/zarr_utils.test.d.ts +0 -1
@@ -21,11 +21,11 @@ const throttle = throttledQueue(1, 16);
21
21
  class SharedLoadWorkerHandle {
22
22
  pendingRequests = [];
23
23
  workerOpen = true;
24
- throttleChannelData = false;
25
- onChannelData = undefined;
26
- onUpdateMetadata = undefined;
24
+ onEvent = undefined;
27
25
  constructor() {
28
- this.worker = new Worker(new URL("./VolumeLoadWorker", import.meta.url));
26
+ this.worker = new Worker(new URL("./VolumeLoadWorker", import.meta.url), {
27
+ type: "module"
28
+ });
29
29
  this.worker.onmessage = this.receiveMessage.bind(this);
30
30
  }
31
31
 
@@ -53,7 +53,11 @@ class SharedLoadWorkerHandle {
53
53
  * Send a message of type `T` to the worker.
54
54
  * Returns a `Promise` that resolves with the worker's response, or rejects with an error message.
55
55
  */
56
- sendMessage(type, payload) {
56
+ // overload 1: message is a global action and does not require a loader ID
57
+
58
+ // overload 2: message is a loader-specific action and requires a loader ID
59
+
60
+ sendMessage(type, payload, loaderId) {
57
61
  let msgId = -1;
58
62
  const promise = new Promise((resolve, reject) => {
59
63
  msgId = this.registerMessagePromise({
@@ -65,7 +69,8 @@ class SharedLoadWorkerHandle {
65
69
  const msg = {
66
70
  msgId,
67
71
  type,
68
- payload
72
+ payload,
73
+ loaderId
69
74
  };
70
75
  this.worker.postMessage(msg);
71
76
  return promise;
@@ -76,17 +81,7 @@ class SharedLoadWorkerHandle {
76
81
  data
77
82
  }) {
78
83
  if (data.responseResult === WorkerResponseResult.EVENT) {
79
- if (data.eventType === WorkerEventType.CHANNEL_LOAD) {
80
- if (this.onChannelData) {
81
- if (this.throttleChannelData) {
82
- throttle(() => this.onChannelData ? this.onChannelData(data) : {});
83
- } else {
84
- this.onChannelData ? this.onChannelData(data) : {};
85
- }
86
- }
87
- } else if (data.eventType === WorkerEventType.METADATA_UPDATE) {
88
- this.onUpdateMetadata?.(data);
89
- }
84
+ this.onEvent?.(data);
90
85
  } else {
91
86
  const prom = this.pendingRequests[data.msgId];
92
87
  if (prom === undefined) {
@@ -103,16 +98,13 @@ class SharedLoadWorkerHandle {
103
98
  this.pendingRequests[data.msgId] = undefined;
104
99
  }
105
100
  }
106
- setThrottleChannelData(throttle) {
107
- this.throttleChannelData = throttle;
108
- }
109
101
  }
110
102
 
111
103
  /**
112
104
  * A context in which volume loaders can be run, which allows loading to run on a WebWorker (where it won't block
113
105
  * rendering or UI updates) and loaders to share a single `VolumeCache` and `RequestQueue`.
114
106
  *
115
- * ### To use:
107
+ * # To use:
116
108
  * 1. Create a `VolumeLoaderContext` with the desired cache and queue configuration.
117
109
  * 2. Before creating a loader, await `onOpen` to ensure the worker is ready.
118
110
  * 3. Create a loader with `createLoader`. This accepts nearly the same arguments as `createVolumeLoader`, but without
@@ -123,10 +115,11 @@ class SharedLoadWorkerHandle {
123
115
  * running on the worker.
124
116
  */
125
117
  class VolumeLoaderContext {
126
- activeLoader = undefined;
127
- activeLoaderId = -1;
118
+ throttleChannelData = false;
128
119
  constructor(maxCacheSize, maxActiveRequests, maxLowPriorityRequests) {
129
120
  this.workerHandle = new SharedLoadWorkerHandle();
121
+ this.workerHandle.onEvent = this.handleEvent.bind(this);
122
+ this.loaders = new Map();
130
123
  this.openPromise = this.workerHandle.sendMessage(WorkerMsgType.INIT, {
131
124
  maxCacheSize,
132
125
  maxActiveRequests,
@@ -145,7 +138,20 @@ class VolumeLoaderContext {
145
138
  /** Close this context, its worker, and any active loaders. */
146
139
  close() {
147
140
  this.workerHandle.close();
148
- this.activeLoader?.close();
141
+ }
142
+ handleEvent(e) {
143
+ const loader = this.loaders.get(e.loaderId);
144
+ if (loader) {
145
+ if (e.eventType === WorkerEventType.CHANNEL_LOAD) {
146
+ if (this.throttleChannelData) {
147
+ throttle(() => loader.onChannelData(e));
148
+ } else {
149
+ loader.onChannelData(e);
150
+ }
151
+ } else if (e.eventType === WorkerEventType.METADATA_UPDATE) {
152
+ loader.onUpdateMetadata(e);
153
+ }
154
+ }
149
155
  }
150
156
 
151
157
  /**
@@ -165,23 +171,19 @@ class VolumeLoaderContext {
165
171
  }
166
172
  return new RawArrayLoader(options.rawArrayOptions.data, options.rawArrayOptions.metadata);
167
173
  }
168
- const success = await this.workerHandle.sendMessage(WorkerMsgType.CREATE_LOADER, {
174
+ const loaderId = await this.workerHandle.sendMessage(WorkerMsgType.CREATE_LOADER, {
169
175
  path,
170
176
  options
171
177
  });
172
- if (!success) {
178
+ if (loaderId === undefined) {
173
179
  throw new Error("Failed to create loader");
174
180
  }
175
- this.activeLoader?.close();
176
- this.activeLoaderId += 1;
177
- this.activeLoader = new WorkerLoader(this.activeLoaderId, this.workerHandle);
178
- return this.activeLoader;
181
+ const loader = new WorkerLoader(loaderId, this.workerHandle);
182
+ this.loaders.set(loaderId, loader);
183
+ return loader;
179
184
  }
180
185
  setThrottleChannelData(throttle) {
181
- this.workerHandle.setThrottleChannelData(throttle);
182
- }
183
- getActiveLoader() {
184
- return this.activeLoader;
186
+ this.throttleChannelData = throttle;
185
187
  }
186
188
  }
187
189
 
@@ -191,7 +193,6 @@ class VolumeLoaderContext {
191
193
  * Created with `VolumeLoaderContext.createLoader`. See its documentation for more.
192
194
  */
193
195
  class WorkerLoader extends ThreadableVolumeLoader {
194
- isOpen = true;
195
196
  currentLoadId = -1;
196
197
  currentLoadCallback = undefined;
197
198
  currentMetadataUpdateCallback = undefined;
@@ -199,18 +200,21 @@ class WorkerLoader extends ThreadableVolumeLoader {
199
200
  super();
200
201
  this.loaderId = loaderId;
201
202
  this.workerHandle = workerHandle;
202
- workerHandle.onChannelData = this.onChannelData.bind(this);
203
- workerHandle.onUpdateMetadata = this.onUpdateMetadata.bind(this);
204
203
  }
205
- checkIsOpen() {
206
- if (!this.isOpen || !this.workerHandle.isOpen) {
204
+ getLoaderId() {
205
+ if (this.loaderId === undefined || !this.workerHandle.isOpen) {
207
206
  throw new Error("Tried to use a closed loader");
208
207
  }
208
+ return this.loaderId;
209
209
  }
210
210
 
211
211
  /** Close and permanently invalidate this loader. */
212
212
  close() {
213
- this.isOpen = false;
213
+ if (this.loaderId === undefined) {
214
+ return;
215
+ }
216
+ this.workerHandle.sendMessage(WorkerMsgType.CLOSE_LOADER, undefined, this.loaderId);
217
+ this.loaderId = undefined;
214
218
  }
215
219
 
216
220
  /**
@@ -218,40 +222,37 @@ class WorkerLoader extends ThreadableVolumeLoader {
218
222
  * any chunks are prefetched in any other directions. Has no effect if this loader doesn't support prefetching.
219
223
  */
220
224
  setPrefetchPriority(directions) {
221
- return this.workerHandle.sendMessage(WorkerMsgType.SET_PREFETCH_PRIORITY_DIRECTIONS, directions);
225
+ return this.workerHandle.sendMessage(WorkerMsgType.SET_PREFETCH_PRIORITY_DIRECTIONS, directions, this.getLoaderId());
222
226
  }
223
227
  updateFetchOptions(fetchOptions) {
224
- return this.workerHandle.sendMessage(WorkerMsgType.UPDATE_FETCH_OPTIONS, fetchOptions);
228
+ return this.workerHandle.sendMessage(WorkerMsgType.UPDATE_FETCH_OPTIONS, fetchOptions, this.getLoaderId());
225
229
  }
226
230
  syncMultichannelLoading(sync) {
227
- return this.workerHandle.sendMessage(WorkerMsgType.SYNCHRONIZE_MULTICHANNEL_LOADING, sync);
231
+ return this.workerHandle.sendMessage(WorkerMsgType.SYNCHRONIZE_MULTICHANNEL_LOADING, sync, this.getLoaderId());
228
232
  }
229
233
  loadDims(loadSpec) {
230
- this.checkIsOpen();
231
- return this.workerHandle.sendMessage(WorkerMsgType.LOAD_DIMS, loadSpec);
234
+ return this.workerHandle.sendMessage(WorkerMsgType.LOAD_DIMS, loadSpec, this.getLoaderId());
232
235
  }
233
236
  async createImageInfo(loadSpec) {
234
- this.checkIsOpen();
235
237
  const {
236
238
  imageInfo,
237
239
  loadSpec: adjustedLoadSpec
238
- } = await this.workerHandle.sendMessage(WorkerMsgType.CREATE_VOLUME, loadSpec);
240
+ } = await this.workerHandle.sendMessage(WorkerMsgType.CREATE_VOLUME, loadSpec, this.getLoaderId());
239
241
  return {
240
242
  imageInfo,
241
243
  loadSpec: rebuildLoadSpec(adjustedLoadSpec)
242
244
  };
243
245
  }
244
246
  loadRawChannelData(imageInfo, loadSpec, onUpdateMetadata, onData) {
245
- this.checkIsOpen();
246
247
  this.currentLoadCallback = onData;
247
248
  this.currentMetadataUpdateCallback = onUpdateMetadata;
248
249
  this.currentLoadId += 1;
249
- return this.workerHandle.sendMessage(WorkerMsgType.LOAD_VOLUME_DATA, {
250
+ const message = {
250
251
  imageInfo,
251
252
  loadSpec,
252
- loaderId: this.loaderId,
253
253
  loadId: this.currentLoadId
254
- });
254
+ };
255
+ return this.workerHandle.sendMessage(WorkerMsgType.LOAD_VOLUME_DATA, message, this.getLoaderId());
255
256
  }
256
257
  onChannelData(e) {
257
258
  if (e.loaderId !== this.loaderId || e.loadId !== this.currentLoadId) {
@@ -2,15 +2,20 @@
2
2
  export let WorkerMsgType = /*#__PURE__*/function (WorkerMsgType) {
3
3
  WorkerMsgType[WorkerMsgType["INIT"] = 0] = "INIT";
4
4
  WorkerMsgType[WorkerMsgType["CREATE_LOADER"] = 1] = "CREATE_LOADER";
5
- WorkerMsgType[WorkerMsgType["CREATE_VOLUME"] = 2] = "CREATE_VOLUME";
6
- WorkerMsgType[WorkerMsgType["LOAD_DIMS"] = 3] = "LOAD_DIMS";
7
- WorkerMsgType[WorkerMsgType["LOAD_VOLUME_DATA"] = 4] = "LOAD_VOLUME_DATA";
8
- WorkerMsgType[WorkerMsgType["SET_PREFETCH_PRIORITY_DIRECTIONS"] = 5] = "SET_PREFETCH_PRIORITY_DIRECTIONS";
9
- WorkerMsgType[WorkerMsgType["SYNCHRONIZE_MULTICHANNEL_LOADING"] = 6] = "SYNCHRONIZE_MULTICHANNEL_LOADING";
10
- WorkerMsgType[WorkerMsgType["UPDATE_FETCH_OPTIONS"] = 7] = "UPDATE_FETCH_OPTIONS";
5
+ WorkerMsgType[WorkerMsgType["CLOSE_LOADER"] = 2] = "CLOSE_LOADER";
6
+ WorkerMsgType[WorkerMsgType["CREATE_VOLUME"] = 3] = "CREATE_VOLUME";
7
+ WorkerMsgType[WorkerMsgType["LOAD_DIMS"] = 4] = "LOAD_DIMS";
8
+ WorkerMsgType[WorkerMsgType["LOAD_VOLUME_DATA"] = 5] = "LOAD_VOLUME_DATA";
9
+ WorkerMsgType[WorkerMsgType["SET_PREFETCH_PRIORITY_DIRECTIONS"] = 6] = "SET_PREFETCH_PRIORITY_DIRECTIONS";
10
+ WorkerMsgType[WorkerMsgType["SYNCHRONIZE_MULTICHANNEL_LOADING"] = 7] = "SYNCHRONIZE_MULTICHANNEL_LOADING";
11
+ WorkerMsgType[WorkerMsgType["UPDATE_FETCH_OPTIONS"] = 8] = "UPDATE_FETCH_OPTIONS";
11
12
  return WorkerMsgType;
12
13
  }({});
13
14
 
15
+ /** The variants of `WorkerMessageType` which represent "global" actions that don't require a specific loader */
16
+
17
+ /** The variants of `WorkerMessageType` which represent actions on a specific loader */
18
+
14
19
  /** The kind of response a worker can return - `SUCCESS`, `ERROR`, or `EVENT`. */
15
20
  export let WorkerResponseResult = /*#__PURE__*/function (WorkerResponseResult) {
16
21
  WorkerResponseResult[WorkerResponseResult["SUCCESS"] = 0] = "SUCCESS";
@@ -21,12 +26,17 @@ export let WorkerResponseResult = /*#__PURE__*/function (WorkerResponseResult) {
21
26
 
22
27
  /** The kind of events that can occur when loading */
23
28
  export let WorkerEventType = /*#__PURE__*/function (WorkerEventType) {
29
+ /** Fired to update a `Volume`'s `imageInfo` and/or `loadSpec` based on loaded data (time, channels, region, etc.) */
24
30
  WorkerEventType[WorkerEventType["METADATA_UPDATE"] = 0] = "METADATA_UPDATE";
31
+ /** Fired when data for a channel (or batch of channels) is loaded */
25
32
  WorkerEventType[WorkerEventType["CHANNEL_LOAD"] = 1] = "CHANNEL_LOAD";
26
33
  return WorkerEventType;
27
34
  }({});
28
35
 
29
- /** All messages to/from a worker carry a `msgId`, a `type`, and a `payload` (whose type is determined by `type`). */
36
+ /**
37
+ * All messages to/from a worker carry a `msgId`, a `type`, and a `payload` (whose type is determined by `type`).
38
+ * Messages which operate on a specific loader also require a `loaderId`.
39
+ */
30
40
 
31
41
  /** Maps each `WorkerMsgType` to the type of the payload of requests of that type. */
32
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aics/vole-core",
3
- "version": "3.12.4",
3
+ "version": "3.13.0",
4
4
  "description": "volume renderer for 3d, 4d, or 5d imaging data with OME-Zarr support",
5
5
  "main": "es/index.js",
6
6
  "type": "module",
@@ -16,15 +16,15 @@
16
16
  "build-docs": "node node_modules/documentation/bin/documentation.js build src/Histogram.ts src/View3d.ts src/Volume.ts src/VolumeMaker.ts src/VolumeLoader.ts -f html -o docs --shallow",
17
17
  "build": "npm run transpileES && npm run build-types",
18
18
  "build-types": "tsc -p tsconfig.types.json",
19
- "build-demo": "webpack --config webpack.demo.js",
19
+ "build-demo": "vite build public/ --config vite.config.ts --outDir ./demo",
20
20
  "clean": "rimraf es/",
21
- "format": "prettier --write src/**/*.js",
22
- "gh-build": "webpack --config webpack.dev.js",
23
- "dev": "webpack serve --config webpack.dev.js",
24
- "start": "webpack serve --config webpack.dev.js",
21
+ "format": "prettier --write src/**/*.ts",
22
+ "gh-build": "vite build public/ --config vite.config.ts --outDir ./vole-core",
23
+ "dev": "vite serve",
24
+ "start": "vite serve",
25
25
  "lint": "eslint --config ./.eslintrc.json --ignore-path ./.eslintignore --ext .js --ext .ts ./src",
26
- "test": "cross-env NODE_OPTIONS=\"--loader ts-node/esm\" mocha --require ts-node/register --es-module-specifier-resolution=node src/**/test/*.[jt]s",
27
- "transpileES": "babel src --out-dir es --extensions .js,.ts --ignore **/*.test.js",
26
+ "test": "vitest run",
27
+ "transpileES": "babel src --out-dir es --extensions .js,.ts --ignore **/*.test.ts",
28
28
  "typeCheck": "tsc -p tsconfig.json --noEmit"
29
29
  },
30
30
  "author": "Daniel Toloudis",
@@ -48,16 +48,13 @@
48
48
  "@babel/preset-typescript": "^7.24.7",
49
49
  "@babel/register": "^7.24.6",
50
50
  "@tweakpane/core": "^1.1.9",
51
- "@types/chai": "^4.3.12",
52
- "@types/mocha": "^9.1.1",
53
- "@types/three": "^0.144.0",
51
+ "@types/three": "^0.171.0",
54
52
  "@typescript-eslint/eslint-plugin": "^5.30.5",
55
53
  "@typescript-eslint/parser": "^5.30.5",
56
54
  "acorn": "^8.7.0",
57
55
  "babel-loader": "^8.2.2",
58
56
  "babel-plugin-inline-import": "^3.0.0",
59
57
  "babel-plugin-inline-import-data-uri": "^1.0.1",
60
- "chai": "^5.1.0",
61
58
  "clean-webpack-plugin": "^4.0.0-alpha.0",
62
59
  "copy-webpack-plugin": "^9.0.1",
63
60
  "cross-env": "^7.0.3",
@@ -67,15 +64,18 @@
67
64
  "file-loader": "^6.2.0",
68
65
  "html-webpack-plugin": "^5.3.2",
69
66
  "husky": "^7.0.1",
67
+ "jsdom": "^25.0.1",
70
68
  "lil-gui": "^0.19.2",
71
69
  "lint-staged": "^13.2.1",
72
- "mocha": "^10.3.0",
73
70
  "prettier": "^2.3.2",
74
71
  "raw-loader": "^4.0.2",
75
72
  "rimraf": "^3.0.2",
76
73
  "ts-node": "^10.9.2",
77
74
  "typescript": "^4.3.5",
78
75
  "url-loader": "^4.1.1",
76
+ "vite": "^6.0.6",
77
+ "vitest": "^2.1.8",
78
+ "vite-plugin-glsl": "^1.3.1",
79
79
  "webpack": "^5.69.1",
80
80
  "webpack-cli": "^4.9.2",
81
81
  "webpack-dev-server": "^4.7.4"
@@ -1,208 +0,0 @@
1
- import { expect } from "chai";
2
- import ChunkPrefetchIterator from "../loaders/zarr_utils/ChunkPrefetchIterator";
3
- import { PrefetchDirection } from "../loaders/zarr_utils/types";
4
- const EXPECTED_3X3X3X3 = [[0, 0, 1, 1, 1],
5
- // T-
6
- [2, 0, 1, 1, 1],
7
- // T+
8
- [1, 0, 0, 1, 1],
9
- // Z-
10
- [1, 0, 2, 1, 1],
11
- // Z+
12
- [1, 0, 1, 0, 1],
13
- // Y-
14
- [1, 0, 1, 2, 1],
15
- // Y+
16
- [1, 0, 1, 1, 0],
17
- // X-
18
- [1, 0, 1, 1, 2] // X+
19
- ];
20
-
21
- // move from the middle of a 3x3x3x3 cube to the middle of a 5x5x5x5 cube
22
- const EXPECTED_5X5X5X5_1 = EXPECTED_3X3X3X3.map(([t, c, z, y, x]) => [t + 1, c, z + 1, y + 1, x + 1]);
23
- // offset = 2!
24
- const EXPECTED_5X5X5X5_2 = [[0, 0, 2, 2, 2],
25
- // T--
26
- [4, 0, 2, 2, 2],
27
- // T++
28
- [2, 0, 0, 2, 2],
29
- // Z--
30
- [2, 0, 4, 2, 2],
31
- // Z++
32
- [2, 0, 2, 0, 2],
33
- // Y--
34
- [2, 0, 2, 4, 2],
35
- // Y++
36
- [2, 0, 2, 2, 0],
37
- // X--
38
- [2, 0, 2, 2, 4] // X++
39
- ];
40
- function validate(iter, expected) {
41
- expect([...iter]).to.deep.equal(expected);
42
- }
43
- describe("ChunkPrefetchIterator", () => {
44
- it("iterates outward in TZYX order, negative then positive", () => {
45
- // 3x3x3x3, with one chunk in the center
46
- const iterator = new ChunkPrefetchIterator([[1, 0, 1, 1, 1]], [1, 1, 1, 1], [[3, 1, 3, 3, 3]]);
47
- validate(iterator, EXPECTED_3X3X3X3);
48
- });
49
- it("finds the borders of a set of multiple chunks and iterates outward from them", () => {
50
- // 4x4x4, with a 2x2x2 set of chunks in the center
51
- const fetchedChunks = [[1, 0, 1, 1, 1], [1, 0, 1, 1, 2], [1, 0, 1, 2, 1], [1, 0, 1, 2, 2], [1, 0, 2, 1, 1], [1, 0, 2, 1, 2], [1, 0, 2, 2, 1], [1, 0, 2, 2, 2]];
52
- const iterator = new ChunkPrefetchIterator(fetchedChunks, [1, 1, 1, 1], [[3, 1, 4, 4, 4]]);
53
- const expected = [...fetchedChunks.map(([_t, c, z, y, x]) => [0, c, z, y, x]),
54
- // T-
55
- ...fetchedChunks.map(([_t, c, z, y, x]) => [2, c, z, y, x]),
56
- // T+
57
- ...fetchedChunks.filter(([_t, _c, z, _y, _x]) => z === 1).map(([t, c, _z, y, x]) => [t, c, 0, y, x]),
58
- // Z-
59
- ...fetchedChunks.filter(([_t, _c, z, _y, _x]) => z === 2).map(([t, c, _z, y, x]) => [t, c, 3, y, x]),
60
- // Z+
61
- ...fetchedChunks.filter(([_t, _c, _z, y, _x]) => y === 1).map(([t, c, z, _y, x]) => [t, c, z, 0, x]),
62
- // Y-
63
- ...fetchedChunks.filter(([_t, _c, _z, y, _x]) => y === 2).map(([t, c, z, _y, x]) => [t, c, z, 3, x]),
64
- // Y+
65
- ...fetchedChunks.filter(([_t, _c, _z, _y, x]) => x === 1).map(([t, c, z, y, _x]) => [t, c, z, y, 0]),
66
- // X-
67
- ...fetchedChunks.filter(([_t, _c, _z, _y, x]) => x === 2).map(([t, c, z, y, _x]) => [t, c, z, y, 3]) // X+
68
- ];
69
- validate(iterator, expected);
70
- });
71
- it("iterates through the same offset in all dimensions before increasing the offset", () => {
72
- // 5x5x5, with one chunk in the center
73
- const iterator = new ChunkPrefetchIterator([[2, 0, 2, 2, 2]], [2, 2, 2, 2], [[5, 1, 5, 5, 5]]);
74
- const expected = [
75
- // offset = 1
76
- ...EXPECTED_5X5X5X5_1,
77
- // offset = 2!
78
- ...EXPECTED_5X5X5X5_2];
79
- validate(iterator, expected);
80
- });
81
- it("stops at the max offset in each dimension", () => {
82
- // 5x5x5, with one chunk in the center
83
- const iterator = new ChunkPrefetchIterator([[2, 0, 2, 2, 2]], [1, 1, 1, 1], [[5, 1, 5, 5, 5]]);
84
- validate(iterator, EXPECTED_5X5X5X5_1); // never reaches offset = 2, as it does above
85
- });
86
- it("stops at the borders of the zarr", () => {
87
- // 3x3x3x3, with one chunk in the center
88
- const iterator = new ChunkPrefetchIterator([[1, 0, 1, 1, 1]], [2, 2, 2, 2], [[3, 1, 3, 3, 3]]);
89
- validate(iterator, EXPECTED_3X3X3X3);
90
- });
91
- it("does not iterate in dimensions which are entirely covered by the fetched set", () => {
92
- // 3x3x3x3, with a 1x1x3x3 slice covering all of x and y
93
- const fetchedChunks = [[1, 0, 1, 0, 0],
94
- // 0, 0
95
- [1, 0, 1, 0, 1],
96
- // 0, 1
97
- [1, 0, 1, 0, 2],
98
- // 0, 2
99
- [1, 0, 1, 1, 0],
100
- // 1, 0
101
- [1, 0, 1, 1, 1],
102
- // 1, 1
103
- [1, 0, 1, 1, 2],
104
- // 1, 2
105
- [1, 0, 1, 2, 0],
106
- // 2, 0
107
- [1, 0, 1, 2, 1],
108
- // 2, 1
109
- [1, 0, 1, 2, 2] // 2, 2
110
- ];
111
- const iterator = new ChunkPrefetchIterator(fetchedChunks, [1, 1, 1, 1], [[3, 1, 3, 3, 3]]);
112
- const expected = [...fetchedChunks.map(([_t, c, z, y, x]) => [0, c, z, y, x]),
113
- // T-
114
- ...fetchedChunks.map(([_t, c, z, y, x]) => [2, c, z, y, x]),
115
- // T+
116
- ...fetchedChunks.map(([t, c, _z, y, x]) => [t, c, 0, y, x]),
117
- // Z-
118
- ...fetchedChunks.map(([t, c, _z, y, x]) => [t, c, 2, y, x]) // Z+
119
- // skips x and y
120
- ];
121
- validate(iterator, expected);
122
- });
123
- it("does not iterate in dimensions where the max offset is 0", () => {
124
- // 3x3x3x3, with one chunk in the center
125
- const iterator = new ChunkPrefetchIterator([[1, 0, 1, 1, 1]], [1, 0, 1, 0], [[3, 1, 3, 3, 3]]);
126
- const expected = [[0, 0, 1, 1, 1],
127
- // T-
128
- [2, 0, 1, 1, 1],
129
- // T+
130
- // skips z
131
- [1, 0, 1, 0, 1],
132
- // Y-
133
- [1, 0, 1, 2, 1] // Y+
134
- // skips x
135
- ];
136
- validate(iterator, expected);
137
- });
138
- it("yields chunks in all prioritized directions first", () => {
139
- // 5x5x5, with one chunk in the center
140
- const iterator = new ChunkPrefetchIterator([[2, 0, 2, 2, 2]], [2, 2, 2, 2], [[5, 1, 5, 5, 5]], [PrefetchDirection.T_PLUS, PrefetchDirection.Y_MINUS]);
141
- const expected = [[3, 0, 2, 2, 2],
142
- // T+
143
- [2, 0, 2, 1, 2],
144
- // Y-
145
- [4, 0, 2, 2, 2],
146
- // T++
147
- [2, 0, 2, 0, 2],
148
- // Y--
149
- ...EXPECTED_5X5X5X5_1.filter(([t, _c, _z, y, _x]) => t <= 2 && y >= 2), ...EXPECTED_5X5X5X5_2.filter(([t, _c, _z, y, _x]) => t <= 2 && y >= 2)];
150
- validate(iterator, expected);
151
- });
152
- it("continues iterating in other dimensions when some reach their limits", () => {
153
- // final boss: 4x4x6 volume with off-center fetched set
154
- // t has a max offset of 2, but is already at its maximum of 2 and never iterates in positive direction
155
- // z has a max offset of 2, but stops early in negative direction at 0
156
- // y has a max offset of 2, but is already covered in the negative direction by chunks in the fetched set
157
- // x has a max offset of 1, which stops iteration before it gets to either edge
158
- const fetchedChunks = [[2, 0, 1, 0, 2], [2, 0, 1, 0, 3], [2, 0, 1, 1, 2], [2, 0, 1, 1, 3]];
159
- const iterator = new ChunkPrefetchIterator(fetchedChunks, [2, 2, 2, 1], [[3, 1, 4, 4, 6]]);
160
-
161
- // prettier-ignore
162
- const expected = [...fetchedChunks.map(([_t, c, z, y, x]) => [1, c, z, y, x]),
163
- // T-
164
- // skip t+: already at max t
165
- ...fetchedChunks.map(([t, c, _z, y, x]) => [t, c, 0, y, x]),
166
- // Z-
167
- ...fetchedChunks.map(([t, c, _z, y, x]) => [t, c, 2, y, x]),
168
- // Z+
169
- // skip y-: already covered by fetched chunks
170
- [2, 0, 1, 2, 2], [2, 0, 1, 2, 3],
171
- // Y+
172
- [2, 0, 1, 0, 1], [2, 0, 1, 1, 1],
173
- // X-
174
- [2, 0, 1, 0, 4], [2, 0, 1, 1, 4],
175
- // X+
176
- ...fetchedChunks.map(([_t, c, z, y, x]) => [0, c, z, y, x]),
177
- // T--
178
- // skip t++: still at max t
179
- // skip z--: already reached z = 0 above
180
- ...fetchedChunks.map(([t, c, _z, y, x]) => [t, c, 3, y, x]),
181
- // Z++
182
- // skip y-: still already covered by fetched chunks
183
- [2, 0, 1, 3, 2], [2, 0, 1, 3, 3] // Y+
184
- // skip x: already at max offset in x
185
- ];
186
- validate(iterator, expected);
187
- });
188
- it("correctly handles sources with differing chunk dimensions", () => {
189
- const allChannels = (x, y, ch = [0, 1, 2, 3]) => ch.map(c => [0, c, 0, y, x]);
190
- const iterator = new ChunkPrefetchIterator(allChannels(1, 2), [0, 0, 2, 2], [[0, 1, 0, 4, 3], [0, 2, 0, 5, 2], [0, 1, 0, 3, 4]]);
191
- const expected = [...allChannels(1, 1),
192
- // Y-
193
- ...allChannels(1, 3, [0, 1, 2]),
194
- // Y+: channel 3 is maxed out
195
- ...allChannels(0, 2),
196
- // X-
197
- ...allChannels(2, 2, [0, 3]),
198
- // X+: channels 1 and 2 are maxed out
199
- ...allChannels(1, 0),
200
- // Y--
201
- ...allChannels(1, 4, [1, 2]),
202
- // Y++: channels 0 and 3 are maxed out
203
- // skip X--
204
- [0, 3, 0, 2, 3] // X++: all channels but channel 3 are maxed out
205
- ];
206
- validate(iterator, expected);
207
- });
208
- });