@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.
- package/README.md +17 -12
- package/es/View3d.js +7 -2
- package/es/VolumeRenderSettings.js +11 -0
- package/es/loaders/TiffLoader.js +3 -1
- package/es/types/NaiveSurfaceNets.d.ts +1 -1
- package/es/types/RayMarchedAtlasVolume.d.ts +1 -1
- package/es/types/ThreeJsPanel.d.ts +2 -2
- package/es/types/TrackballControls.d.ts +1 -1
- package/es/types/VolumeDrawable.d.ts +1 -1
- package/es/types/VolumeRenderImpl.d.ts +1 -1
- package/es/types/index.d.ts +1 -1
- package/es/types/workers/VolumeLoaderContext.d.ts +9 -13
- package/es/types/workers/types.d.ts +25 -16
- package/es/workers/VolumeLoadWorker.js +54 -32
- package/es/workers/VolumeLoaderContext.js +52 -51
- package/es/workers/types.js +17 -7
- package/package.json +13 -13
- package/es/test/ChunkPrefetchIterator.test.js +0 -208
- package/es/test/RequestQueue.test.js +0 -442
- package/es/test/SubscribableRequestQueue.test.js +0 -244
- package/es/test/VolumeCache.test.js +0 -118
- package/es/test/VolumeRenderSettings.test.js +0 -71
- package/es/test/lut.test.js +0 -671
- package/es/test/num_utils.test.js +0 -140
- package/es/test/volume.test.js +0 -98
- package/es/test/zarr_utils.test.js +0 -358
- package/es/types/test/ChunkPrefetchIterator.test.d.ts +0 -1
- package/es/types/test/RequestQueue.test.d.ts +0 -1
- package/es/types/test/SubscribableRequestQueue.test.d.ts +0 -1
- package/es/types/test/VolumeCache.test.d.ts +0 -1
- package/es/types/test/VolumeRenderSettings.test.d.ts +0 -1
- package/es/types/test/lut.test.d.ts +0 -1
- package/es/types/test/num_utils.test.d.ts +0 -1
- package/es/types/test/volume.test.d.ts +0 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
174
|
+
const loaderId = await this.workerHandle.sendMessage(WorkerMsgType.CREATE_LOADER, {
|
|
169
175
|
path,
|
|
170
176
|
options
|
|
171
177
|
});
|
|
172
|
-
if (
|
|
178
|
+
if (loaderId === undefined) {
|
|
173
179
|
throw new Error("Failed to create loader");
|
|
174
180
|
}
|
|
175
|
-
this.
|
|
176
|
-
this.
|
|
177
|
-
|
|
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.
|
|
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
|
-
|
|
206
|
-
if (
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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) {
|
package/es/workers/types.js
CHANGED
|
@@ -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["
|
|
6
|
-
WorkerMsgType[WorkerMsgType["
|
|
7
|
-
WorkerMsgType[WorkerMsgType["
|
|
8
|
-
WorkerMsgType[WorkerMsgType["
|
|
9
|
-
WorkerMsgType[WorkerMsgType["
|
|
10
|
-
WorkerMsgType[WorkerMsgType["
|
|
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
|
-
/**
|
|
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.
|
|
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": "
|
|
19
|
+
"build-demo": "vite build public/ --config vite.config.ts --outDir ./demo",
|
|
20
20
|
"clean": "rimraf es/",
|
|
21
|
-
"format": "prettier --write src/**/*.
|
|
22
|
-
"gh-build": "
|
|
23
|
-
"dev": "
|
|
24
|
-
"start": "
|
|
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": "
|
|
27
|
-
"transpileES": "babel src --out-dir es --extensions .js,.ts --ignore **/*.test.
|
|
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/
|
|
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
|
-
});
|