@aics/vole-core 3.12.4 → 3.13.1
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 +21 -13
- package/es/View3d.js +21 -5
- package/es/Volume.js +1 -1
- package/es/VolumeCache.js +10 -3
- package/es/VolumeRenderSettings.js +11 -0
- package/es/loaders/JsonImageInfoLoader.js +2 -1
- package/es/loaders/OmeZarrLoader.js +30 -31
- package/es/loaders/TiffLoader.js +3 -1
- package/es/loaders/VolumeLoadError.js +1 -1
- package/es/loaders/zarr_utils/ChunkPrefetchIterator.js +7 -0
- package/es/loaders/zarr_utils/validation.js +18 -7
- package/es/loaders/zarr_utils/wrapArray.js +39 -0
- 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/View3d.d.ts +6 -2
- package/es/types/VolumeCache.d.ts +5 -2
- 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/loaders/zarr_utils/types.d.ts +17 -12
- package/es/types/loaders/zarr_utils/validation.d.ts +14 -2
- package/es/types/loaders/zarr_utils/wrapArray.d.ts +7 -0
- 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 +14 -14
- package/es/loaders/zarr_utils/WrappedStore.js +0 -51
- 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/loaders/zarr_utils/WrappedStore.d.ts +0 -24
- 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
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Vol-E core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
**Vol-E core** is a WebGL canvas-based volume viewer. It can display multichannel volume data with high channel counts. The viewer is optimized for OME-Zarr files, and can prefetch and cache Zarr chunks in browser memory for performance.
|
|
4
7
|
|
|
5
8
|
The Vol-E core package exposes several key modules:
|
|
6
9
|
|
|
@@ -16,7 +19,7 @@ There are several ways to deliver volume data to the viewer:
|
|
|
16
19
|
- Load raw TypedArrays of 3d volume data ( see `RawArrayLoader` and `Volume.setChannelDataFromVolume` ).
|
|
17
20
|
- (legacy) Load texture atlases as .png files or Uint8Arrays containing volume slices tiled across a 2d image ( see `JsonImageInfoLoader` and `Volume.setChannelDataFromAtlas` ).
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
## Example
|
|
20
23
|
|
|
21
24
|
See [`public/index.ts`](./public/index.ts) for a working example. (`npm install; npm run dev` will run that code)
|
|
22
25
|
|
|
@@ -59,19 +62,20 @@ view3D.addVolume(volume);
|
|
|
59
62
|
loader.loadVolumeData(volume);
|
|
60
63
|
```
|
|
61
64
|
|
|
62
|
-
|
|
65
|
+
## React example
|
|
63
66
|
|
|
64
|
-
See [vole-app](https://github.com/allen-cell-animated/
|
|
67
|
+
See [vole-app](https://github.com/allen-cell-animated/vole-app) for a complete application that wraps Vol-E core in a React component.
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
## Acknowledgements
|
|
67
70
|
|
|
68
71
|
The ray marched volume shader is a heavily modified version of one that has its origins in [Bisque](http://bioimage.ucsb.edu/bisque).
|
|
69
72
|
The core path tracing implementation was adapted from ExposureRender.
|
|
70
73
|
|
|
71
|
-
|
|
74
|
+
### BisQue license
|
|
72
75
|
|
|
73
76
|
Center for Bio-Image Informatics, University of California at Santa Barbara
|
|
74
77
|
|
|
78
|
+
```text
|
|
75
79
|
Copyright (c) 2007-2017 by the Regents of the University of California
|
|
76
80
|
All rights reserved
|
|
77
81
|
|
|
@@ -106,14 +110,18 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
106
110
|
The views and conclusions contained in the software and documentation
|
|
107
111
|
are those of the authors and should not be interpreted as representing
|
|
108
112
|
official policies, either expressed or implied, of the Regents of the University of California.
|
|
113
|
+
```
|
|
109
114
|
|
|
110
115
|
## Exposure Render license
|
|
111
116
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
116
|
-
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
117
|
-
- Neither the name of the TU Delft nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
117
|
+
```text
|
|
118
|
+
Copyright (c) 2011, T. Kroes <t.kroes@tudelft.nl>
|
|
119
|
+
All rights reserved.
|
|
118
120
|
|
|
119
|
-
|
|
121
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
122
|
+
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
123
|
+
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
124
|
+
- Neither the name of the TU Delft nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
125
|
+
|
|
126
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
127
|
+
```
|
package/es/View3d.js
CHANGED
|
@@ -96,6 +96,9 @@ export class View3d {
|
|
|
96
96
|
getDOMElement() {
|
|
97
97
|
return this.canvas3d.containerdiv;
|
|
98
98
|
}
|
|
99
|
+
getCanvasDOMElement() {
|
|
100
|
+
return this.canvas3d.renderer.domElement;
|
|
101
|
+
}
|
|
99
102
|
getCameraState() {
|
|
100
103
|
return this.canvas3d.getCameraState();
|
|
101
104
|
}
|
|
@@ -106,9 +109,16 @@ export class View3d {
|
|
|
106
109
|
|
|
107
110
|
/**
|
|
108
111
|
* Force a redraw.
|
|
112
|
+
* @param synchronous If true, the redraw will be done synchronously. If false (default), the
|
|
113
|
+
* redraw will be done asynchronously via `requestAnimationFrame`. Redraws should be done async
|
|
114
|
+
* whenever possible for the best performance.
|
|
109
115
|
*/
|
|
110
|
-
redraw() {
|
|
111
|
-
|
|
116
|
+
redraw(synchronous = false) {
|
|
117
|
+
if (synchronous) {
|
|
118
|
+
this.canvas3d.onAnimationLoop();
|
|
119
|
+
} else {
|
|
120
|
+
this.canvas3d.redraw();
|
|
121
|
+
}
|
|
112
122
|
}
|
|
113
123
|
unsetImage() {
|
|
114
124
|
if (this.image) {
|
|
@@ -206,10 +216,11 @@ export class View3d {
|
|
|
206
216
|
}
|
|
207
217
|
setTime(volume, time, onChannelLoaded) {
|
|
208
218
|
const timeClamped = Math.max(0, Math.min(time, volume.imageInfo.times - 1));
|
|
209
|
-
volume.updateRequiredData({
|
|
219
|
+
const loadPromise = volume.updateRequiredData({
|
|
210
220
|
time: timeClamped
|
|
211
221
|
}, onChannelLoaded);
|
|
212
222
|
this.updateTimestepIndicator(volume);
|
|
223
|
+
return loadPromise;
|
|
213
224
|
}
|
|
214
225
|
|
|
215
226
|
/**
|
|
@@ -826,16 +837,21 @@ export class View3d {
|
|
|
826
837
|
const prefetch = pane.addFolder({
|
|
827
838
|
title: "Prefetch"
|
|
828
839
|
});
|
|
840
|
+
// Not all `IVolumeLoader`s implement `updateFetchOptions`. This cast makes it sound to try to call it, but we
|
|
841
|
+
// still have to be careful to null-check it!
|
|
842
|
+
// TODO depending on how the relationship between loaders and images pans out, it's not impossible that the loader
|
|
843
|
+
// for an image will be changeable and this variable will capture a stale reference to old loaders. Careful!
|
|
844
|
+
const loader = this.image?.volume.loader;
|
|
829
845
|
// one number will be used for all axis directions
|
|
830
846
|
prefetch.addInput(allGlobalLoadingOptions, "numChunksToPrefetchAhead").on("change", event => {
|
|
831
|
-
|
|
847
|
+
loader?.updateFetchOptions?.({
|
|
832
848
|
maxPrefetchDistance: [event.value, event.value, event.value, event.value]
|
|
833
849
|
});
|
|
834
850
|
this.image?.volume.updateRequiredData({});
|
|
835
851
|
});
|
|
836
852
|
// should we try to prefetch along Z even if we are only playing along T?
|
|
837
853
|
prefetch.addInput(allGlobalLoadingOptions, "prefetchAlongNonPlayingAxis").on("change", event => {
|
|
838
|
-
|
|
854
|
+
loader?.updateFetchOptions?.({
|
|
839
855
|
onlyPriorityDirections: !event.value
|
|
840
856
|
});
|
|
841
857
|
});
|
package/es/Volume.js
CHANGED
package/es/VolumeCache.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export const isChunk = data => data.data !== undefined;
|
|
2
|
+
const chunkSize = ({
|
|
3
|
+
data
|
|
4
|
+
}) => Array.isArray(data) ? data.length : data.byteLength;
|
|
5
|
+
const dataSize = data => data.byteLength ?? chunkSize(data);
|
|
6
|
+
|
|
1
7
|
/** Default: 250MB. Should be large enough to be useful but safe for most any computer that can run the app */
|
|
2
8
|
const CACHE_MAX_SIZE_DEFAULT = 250_000_000;
|
|
3
9
|
export default class VolumeCache {
|
|
@@ -31,7 +37,7 @@ export default class VolumeCache {
|
|
|
31
37
|
*/
|
|
32
38
|
removeEntryFromStore(entry) {
|
|
33
39
|
this.entries.delete(entry.key);
|
|
34
|
-
this.currentSize -= entry.data
|
|
40
|
+
this.currentSize -= dataSize(entry.data);
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/**
|
|
@@ -98,7 +104,8 @@ export default class VolumeCache {
|
|
|
98
104
|
* @returns {boolean} a boolean indicating whether the insertion succeeded.
|
|
99
105
|
*/
|
|
100
106
|
insert(key, data) {
|
|
101
|
-
|
|
107
|
+
const size = dataSize(data);
|
|
108
|
+
if (size > this.maxSize) {
|
|
102
109
|
console.error("VolumeCache: attempt to insert a single entry larger than the cache");
|
|
103
110
|
return false;
|
|
104
111
|
}
|
|
@@ -120,7 +127,7 @@ export default class VolumeCache {
|
|
|
120
127
|
};
|
|
121
128
|
this.addEntryAsFirst(newEntry);
|
|
122
129
|
this.entries.set(key, newEntry);
|
|
123
|
-
this.currentSize +=
|
|
130
|
+
this.currentSize += size;
|
|
124
131
|
|
|
125
132
|
// Evict until size is within limit
|
|
126
133
|
while (this.currentSize > this.maxSize) {
|
|
@@ -3,14 +3,23 @@ import { Euler, Vector2, Vector3 } from "three";
|
|
|
3
3
|
* Marks groups of related settings that may have changed.
|
|
4
4
|
*/
|
|
5
5
|
export let SettingsFlags = /*#__PURE__*/function (SettingsFlags) {
|
|
6
|
+
/** parameters: translation, rotation, scale, flipAxes */
|
|
6
7
|
SettingsFlags[SettingsFlags["TRANSFORM"] = 1] = "TRANSFORM";
|
|
8
|
+
/** parameters: gammaMin, gammaLevel, gammaMax, brightness*/
|
|
7
9
|
SettingsFlags[SettingsFlags["CAMERA"] = 2] = "CAMERA";
|
|
10
|
+
/** parameters: showBoundingBox, boundingBoxColor */
|
|
8
11
|
SettingsFlags[SettingsFlags["BOUNDING_BOX"] = 4] = "BOUNDING_BOX";
|
|
12
|
+
/** parameters: bounds, zSlice */
|
|
9
13
|
SettingsFlags[SettingsFlags["ROI"] = 8] = "ROI";
|
|
14
|
+
/** parameters: maskAlpha */
|
|
10
15
|
SettingsFlags[SettingsFlags["MASK_ALPHA"] = 16] = "MASK_ALPHA";
|
|
16
|
+
/** parameters: density, diffuse, specular, emissive, glossiness */
|
|
11
17
|
SettingsFlags[SettingsFlags["MATERIAL"] = 32] = "MATERIAL";
|
|
18
|
+
/** parameters: resolution, useInterpolation, pixelSamplingRate, primaryRayStepSize, secondaryRayStepSize*/
|
|
12
19
|
SettingsFlags[SettingsFlags["SAMPLING"] = 64] = "SAMPLING";
|
|
20
|
+
/** parameters: isOrtho, orthoScale, viewAxis, visible, maxProjectMode */
|
|
13
21
|
SettingsFlags[SettingsFlags["VIEW"] = 128] = "VIEW";
|
|
22
|
+
/** parameters: maskChannelIndex */
|
|
14
23
|
SettingsFlags[SettingsFlags["MASK_DATA"] = 256] = "MASK_DATA";
|
|
15
24
|
SettingsFlags[SettingsFlags["ALL"] = 1023] = "ALL";
|
|
16
25
|
return SettingsFlags;
|
|
@@ -19,7 +28,9 @@ export let Axis = /*#__PURE__*/function (Axis) {
|
|
|
19
28
|
Axis["X"] = "x";
|
|
20
29
|
Axis["Y"] = "y";
|
|
21
30
|
Axis["Z"] = "z";
|
|
31
|
+
/** Alias for NONE, indicates 3D mode */
|
|
22
32
|
Axis["XYZ"] = "";
|
|
33
|
+
/** No current axis, indicates 3D mode */
|
|
23
34
|
Axis["NONE"] = "";
|
|
24
35
|
return Axis;
|
|
25
36
|
}({});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Box3, Vector3 } from "three";
|
|
2
2
|
import { ThreadableVolumeLoader } from "./IVolumeLoader.js";
|
|
3
3
|
import { computeAtlasSize } from "../ImageInfo.js";
|
|
4
|
+
import { isChunk } from "../VolumeCache.js";
|
|
4
5
|
import { getDataRange } from "../utils/num_utils.js";
|
|
5
6
|
|
|
6
7
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
@@ -169,7 +170,7 @@ class JsonImageInfoLoader extends ThreadableVolumeLoader {
|
|
|
169
170
|
for (let j = 0; j < Math.min(image.channels.length, 4); ++j) {
|
|
170
171
|
const chindex = image.channels[j];
|
|
171
172
|
const cacheResult = cache?.get(`${image.name}/${chindex}`);
|
|
172
|
-
if (cacheResult) {
|
|
173
|
+
if (cacheResult && !isChunk(cacheResult)) {
|
|
173
174
|
// all data coming from this loader is natively 8-bit
|
|
174
175
|
const channelData = new Uint8Array(cacheResult);
|
|
175
176
|
if (syncChannels) {
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { Box3, Vector3 } from "three";
|
|
2
|
-
import * as zarr from "
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { FetchStore } from "zarrita";
|
|
2
|
+
import * as zarr from "zarrita";
|
|
3
|
+
const {
|
|
4
|
+
slice
|
|
5
|
+
} = zarr;
|
|
7
6
|
import SubscribableRequestQueue from "../utils/SubscribableRequestQueue.js";
|
|
8
7
|
import { ThreadableVolumeLoader } from "./IVolumeLoader.js";
|
|
9
8
|
import { composeSubregion, computePackedAtlasDims, convertSubregionToPixels, pickLevelToLoad, unitNameToSymbol } from "./VolumeLoaderUtils.js";
|
|
10
9
|
import ChunkPrefetchIterator from "./zarr_utils/ChunkPrefetchIterator.js";
|
|
11
|
-
import
|
|
12
|
-
import { getDimensionCount, getScale, getSourceChannelNames, matchSourceScaleLevels, orderByDimension, orderByTCZYX, remapAxesToTCZYX } from "./zarr_utils/utils.js";
|
|
10
|
+
import { getScale, getSourceChannelNames, matchSourceScaleLevels, orderByDimension, orderByTCZYX, remapAxesToTCZYX } from "./zarr_utils/utils.js";
|
|
13
11
|
import { VolumeLoadError, VolumeLoadErrorType, wrapVolumeLoadError } from "./VolumeLoadError.js";
|
|
14
|
-
import
|
|
12
|
+
import wrapArray from "./zarr_utils/wrapArray.js";
|
|
13
|
+
import { assertMetadataHasMultiscales, toOMEZarrMetaV4, validateOMEZarrMetadata } from "./zarr_utils/validation.js";
|
|
15
14
|
const CHUNK_REQUEST_CANCEL_REASON = "chunk request cancelled";
|
|
16
15
|
|
|
17
16
|
// returns the converted data and the original min and max values
|
|
@@ -97,23 +96,26 @@ class OMEZarrLoader extends ThreadableVolumeLoader {
|
|
|
97
96
|
|
|
98
97
|
// Create one `ZarrSource` per URL
|
|
99
98
|
const sourceProms = urlsArr.map(async (url, i) => {
|
|
100
|
-
const store = new
|
|
99
|
+
const store = new zarr.FetchStore(url);
|
|
101
100
|
const root = zarr.root(store);
|
|
102
101
|
const group = await zarr.open(root, {
|
|
103
102
|
kind: "group"
|
|
104
103
|
}).catch(wrapVolumeLoadError(`Failed to open OME-Zarr data at ${url}`, VolumeLoadErrorType.NOT_FOUND));
|
|
104
|
+
const sourceName = urlsArr.length > 1 ? `Zarr source ${i}` : "Zarr";
|
|
105
|
+
const meta = toOMEZarrMetaV4(group.attrs);
|
|
106
|
+
assertMetadataHasMultiscales(meta, sourceName);
|
|
105
107
|
|
|
106
108
|
// Pick scene (multiscale)
|
|
107
109
|
let scene = scenesArr[Math.min(i, scenesArr.length - 1)];
|
|
108
|
-
if (scene >
|
|
110
|
+
if (scene > meta.multiscales?.length) {
|
|
109
111
|
console.warn(`WARNING: OMEZarrLoader: scene ${scene} is invalid. Using scene 0.`);
|
|
110
112
|
scene = 0;
|
|
111
113
|
}
|
|
112
|
-
validateOMEZarrMetadata(
|
|
114
|
+
validateOMEZarrMetadata(meta, scene, sourceName);
|
|
113
115
|
const {
|
|
114
116
|
multiscales,
|
|
115
117
|
omero
|
|
116
|
-
} =
|
|
118
|
+
} = meta;
|
|
117
119
|
const multiscaleMetadata = multiscales[scene];
|
|
118
120
|
|
|
119
121
|
// Open all scale levels of multiscale
|
|
@@ -121,7 +123,7 @@ class OMEZarrLoader extends ThreadableVolumeLoader {
|
|
|
121
123
|
path
|
|
122
124
|
}) => zarr.open(root.resolve(path), {
|
|
123
125
|
kind: "array"
|
|
124
|
-
}).catch(wrapVolumeLoadError(`Failed to open scale level ${path} of OME-Zarr data at ${url}`, VolumeLoadErrorType.NOT_FOUND)));
|
|
126
|
+
}).then(array => wrapArray(array, url, cache, queue)).catch(wrapVolumeLoadError(`Failed to open scale level ${path} of OME-Zarr data at ${url}`, VolumeLoadErrorType.NOT_FOUND)));
|
|
125
127
|
const scaleLevels = await Promise.all(lvlProms);
|
|
126
128
|
const axesTCZYX = remapAxesToTCZYX(multiscaleMetadata.axes);
|
|
127
129
|
return {
|
|
@@ -330,30 +332,25 @@ class OMEZarrLoader extends ThreadableVolumeLoader {
|
|
|
330
332
|
loadSpec: fullExtentLoadSpec
|
|
331
333
|
});
|
|
332
334
|
}
|
|
333
|
-
|
|
334
|
-
const {
|
|
335
|
-
store,
|
|
336
|
-
path
|
|
337
|
-
} = scaleLevel;
|
|
338
|
-
const separator = path.endsWith("/") ? "" : "/";
|
|
339
|
-
const key = path + separator + this.orderByDimension(coords).join("/");
|
|
335
|
+
prefetchChunk(scaleLevel, coords, subscriber) {
|
|
340
336
|
// Calling `get` and doing nothing with the result still triggers a cache check, fetch, and insertion
|
|
341
|
-
|
|
337
|
+
scaleLevel.getChunk(this.orderByDimension(coords), {
|
|
342
338
|
subscriber,
|
|
343
339
|
isPrefetch: true
|
|
344
|
-
}).catch(wrapVolumeLoadError(`Unable to prefetch chunk with
|
|
340
|
+
}).catch(wrapVolumeLoadError(`Unable to prefetch chunk with coords ${coords.join(", ")}`, VolumeLoadErrorType.LOAD_DATA_FAILED, CHUNK_REQUEST_CANCEL_REASON));
|
|
345
341
|
}
|
|
346
342
|
|
|
347
343
|
/** Reads a list of chunk keys requested by a `loadVolumeData` call and sets up appropriate prefetch requests. */
|
|
348
344
|
beginPrefetch(keys, scaleLevel) {
|
|
349
345
|
// Convert keys to arrays of coords
|
|
346
|
+
if (keys.length === 0) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
350
349
|
const chunkCoords = keys.map(({
|
|
351
350
|
sourceIdx,
|
|
352
|
-
|
|
351
|
+
coords
|
|
353
352
|
}) => {
|
|
354
|
-
const
|
|
355
|
-
const coordsInDimensionOrder = key.trim().split("/").slice(-numDims).filter(s => s !== "").map(s => parseInt(s, 10));
|
|
356
|
-
const sourceCoords = this.orderByTCZYX(coordsInDimensionOrder, 0, sourceIdx);
|
|
353
|
+
const sourceCoords = this.orderByTCZYX(coords, 0, sourceIdx);
|
|
357
354
|
// Convert source channel index to absolute channel index for `ChunkPrefetchIterator`'s benefit
|
|
358
355
|
// (we match chunk coordinates output from `ChunkPrefetchIterator` back to sources below)
|
|
359
356
|
sourceCoords[1] += this.sources[sourceIdx].channelOffset;
|
|
@@ -366,6 +363,7 @@ class OMEZarrLoader extends ThreadableVolumeLoader {
|
|
|
366
363
|
const chunkDimsUnordered = level.shape.map((dim, idx) => Math.ceil(dim / level.chunks[idx]));
|
|
367
364
|
return this.orderByTCZYX(chunkDimsUnordered, 1);
|
|
368
365
|
});
|
|
366
|
+
|
|
369
367
|
// `ChunkPrefetchIterator` yields chunk coordinates in order of roughly how likely they are to be loaded next
|
|
370
368
|
const prefetchIterator = new ChunkPrefetchIterator(chunkCoords, this.fetchOptions.maxPrefetchDistance, chunkDimsTCZYX, this.priorityDirections, this.fetchOptions.onlyPriorityDirections);
|
|
371
369
|
const subscriber = this.requestQueue.addSubscriber();
|
|
@@ -435,11 +433,11 @@ class OMEZarrLoader extends ThreadableVolumeLoader {
|
|
|
435
433
|
|
|
436
434
|
// Prefetch housekeeping: we want to save keys involved in this load to prefetch later
|
|
437
435
|
const keys = [];
|
|
438
|
-
const
|
|
436
|
+
const reportChunkBase = (sourceIdx, coords, sub) => {
|
|
439
437
|
if (sub === subscriber) {
|
|
440
438
|
keys.push({
|
|
441
439
|
sourceIdx,
|
|
442
|
-
|
|
440
|
+
coords
|
|
443
441
|
});
|
|
444
442
|
}
|
|
445
443
|
};
|
|
@@ -458,11 +456,12 @@ class OMEZarrLoader extends ThreadableVolumeLoader {
|
|
|
458
456
|
const unorderedSpec = [loadSpec.time, sourceCh, slice(min.z, max.z), slice(min.y, max.y), slice(min.x, max.x)];
|
|
459
457
|
const level = this.sources[sourceIdx].scaleLevels[multiscaleLevel];
|
|
460
458
|
const sliceSpec = this.orderByDimension(unorderedSpec, sourceIdx);
|
|
461
|
-
const
|
|
462
|
-
|
|
459
|
+
const reportChunk = (coords, sub) => reportChunkBase(sourceIdx, coords, sub);
|
|
460
|
+
console.log(level);
|
|
461
|
+
const result = await zarr.get(level, sliceSpec, {
|
|
463
462
|
opts: {
|
|
464
463
|
subscriber,
|
|
465
|
-
|
|
464
|
+
reportChunk
|
|
466
465
|
}
|
|
467
466
|
}).catch(wrapVolumeLoadError("Could not load OME-Zarr volume data", VolumeLoadErrorType.LOAD_DATA_FAILED, CHUNK_REQUEST_CANCEL_REASON));
|
|
468
467
|
if (result?.data === undefined) {
|
package/es/loaders/TiffLoader.js
CHANGED
|
@@ -191,7 +191,9 @@ class TiffLoader extends ThreadableVolumeLoader {
|
|
|
191
191
|
bytesPerSample: getBytesPerSample(dims.pixeltype),
|
|
192
192
|
url: this.url
|
|
193
193
|
};
|
|
194
|
-
const worker = new Worker(new URL("../workers/FetchTiffWorker", import.meta.url)
|
|
194
|
+
const worker = new Worker(new URL("../workers/FetchTiffWorker", import.meta.url), {
|
|
195
|
+
type: "module"
|
|
196
|
+
});
|
|
195
197
|
worker.onmessage = e => {
|
|
196
198
|
if (e.data.isError) {
|
|
197
199
|
reject(deserializeError(e.data.error));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { errorConstructors } from "serialize-error";
|
|
2
|
-
import { NodeNotFoundError, KeyError } from "
|
|
2
|
+
import { NodeNotFoundError, KeyError } from "zarrita";
|
|
3
3
|
// geotiff doesn't export its error types...
|
|
4
4
|
|
|
5
5
|
/** Groups possible load errors into a few broad categories which we can give similar guidance to the user about. */
|
|
@@ -34,6 +34,13 @@ export default class ChunkPrefetchIterator {
|
|
|
34
34
|
updateMinMax(chunk[4], extrema[3]);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// Bail out if we have any non-finite values in the extrema (the iterator will be empty)
|
|
38
|
+
if (extrema.flat().some(val => !Number.isFinite(val))) {
|
|
39
|
+
this.directionStates = [];
|
|
40
|
+
this.priorityDirectionStates = [];
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
// Create `PrefetchDirectionState`s for each direction
|
|
38
45
|
this.directionStates = [];
|
|
39
46
|
this.priorityDirectionStates = [];
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { VolumeLoadError, VolumeLoadErrorType } from "../VolumeLoadError.js";
|
|
2
|
+
/**
|
|
3
|
+
* If `meta` is the top-level metadata of a zarr node formatted according to the OME-Zarr spec version 0.5, returns
|
|
4
|
+
* the object formatted according to v0.4 of the spec. For our purposes this just means flattening out the `ome` key.
|
|
5
|
+
*
|
|
6
|
+
* Return type is `unknown` because this does no actual validation; use `validateOMEZarrMetadata` for that.
|
|
7
|
+
*/
|
|
8
|
+
export const toOMEZarrMetaV4 = meta => meta.ome ?? meta;
|
|
2
9
|
function isObjectWithProp(obj, prop) {
|
|
3
10
|
return typeof obj === "object" && obj !== null && prop in obj;
|
|
4
11
|
}
|
|
@@ -17,18 +24,22 @@ function assertPropIsArray(obj, prop, name = "zarr") {
|
|
|
17
24
|
}
|
|
18
25
|
}
|
|
19
26
|
|
|
27
|
+
/** Intermediate stage of validation, before we've picked a single multiscale to validate */
|
|
28
|
+
|
|
29
|
+
export function assertMetadataHasMultiscales(meta, name = "zarr") {
|
|
30
|
+
// data is an object with a key "multiscales", which is a non-empty array
|
|
31
|
+
assertMetadataHasProp(meta, "multiscales", name);
|
|
32
|
+
assertPropIsArray(meta, "multiscales", name);
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
/**
|
|
21
|
-
* Validates that the `OMEZarrMetadata` record `
|
|
36
|
+
* Validates that the `OMEZarrMetadata` record `meta` has the minimal amount of data required to open a volume. Since
|
|
22
37
|
* we only ever open one multiscale, we only validate the multiscale metadata record at index `multiscaleIdx` here.
|
|
23
38
|
* `name` is used in error messages to identify the source of the metadata.
|
|
24
39
|
*/
|
|
25
|
-
export function validateOMEZarrMetadata(
|
|
26
|
-
// data is an object with a key "multiscales", which is an array
|
|
27
|
-
assertMetadataHasProp(data, "multiscales", name);
|
|
28
|
-
assertPropIsArray(data, "multiscales", name);
|
|
29
|
-
|
|
40
|
+
export function validateOMEZarrMetadata(meta, multiscaleIdx = 0, name = "zarr") {
|
|
30
41
|
// check that a multiscale metadata entry exists at `multiscaleIdx`
|
|
31
|
-
const multiscaleMeta =
|
|
42
|
+
const multiscaleMeta = meta.multiscales[multiscaleIdx];
|
|
32
43
|
if (!multiscaleMeta) {
|
|
33
44
|
throw new VolumeLoadError(`${name} metadata does not have requested multiscale level ${multiscaleIdx}`, {
|
|
34
45
|
type: VolumeLoadErrorType.INVALID_METADATA
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { isChunk } from "../../VolumeCache.js";
|
|
2
|
+
export default function wrapArray(array, basePath, cache, queue) {
|
|
3
|
+
const path = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
4
|
+
const keyBase = path + array.path + (array.path.endsWith("/") ? "" : "/");
|
|
5
|
+
const getChunk = async (coords, opts) => {
|
|
6
|
+
if (opts?.subscriber && opts.reportChunk) {
|
|
7
|
+
opts.reportChunk(coords, opts.subscriber);
|
|
8
|
+
}
|
|
9
|
+
const fullKey = keyBase + coords.join(",");
|
|
10
|
+
const cacheResult = cache?.get(fullKey);
|
|
11
|
+
if (cacheResult && isChunk(cacheResult)) {
|
|
12
|
+
return cacheResult;
|
|
13
|
+
}
|
|
14
|
+
let result;
|
|
15
|
+
if (queue && opts?.subscriber) {
|
|
16
|
+
result = await queue.addRequest(fullKey, opts?.subscriber, () => array.getChunk(coords, opts), opts.isPrefetch);
|
|
17
|
+
} else {
|
|
18
|
+
result = await array.getChunk(coords, opts);
|
|
19
|
+
}
|
|
20
|
+
cache?.insert(fullKey, result);
|
|
21
|
+
return result;
|
|
22
|
+
};
|
|
23
|
+
return new Proxy(array, {
|
|
24
|
+
get: (target, prop) => {
|
|
25
|
+
if (prop === "getChunk") {
|
|
26
|
+
return getChunk;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#no_private_property_forwarding
|
|
30
|
+
const value = target[prop];
|
|
31
|
+
if (value instanceof Function) {
|
|
32
|
+
return function (...args) {
|
|
33
|
+
return value.apply(target, args);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -7,5 +7,5 @@ declare function SurfaceNets(data: any, dims: any, isovalue: any): {
|
|
|
7
7
|
vertices: number[][];
|
|
8
8
|
faces: number[][];
|
|
9
9
|
};
|
|
10
|
-
declare function ConstructTHREEGeometry(surfaceNetResult: any): BufferGeometry[];
|
|
10
|
+
declare function ConstructTHREEGeometry(surfaceNetResult: any): BufferGeometry<import("three").NormalBufferAttributes>[];
|
|
11
11
|
import { BufferGeometry } from "three/src/core/BufferGeometry";
|
|
@@ -33,7 +33,7 @@ export default class RayMarchedAtlasVolume implements VolumeRenderImpl {
|
|
|
33
33
|
private createGeometry;
|
|
34
34
|
private createTickMarks;
|
|
35
35
|
cleanup(): void;
|
|
36
|
-
doRender(renderer: WebGLRenderer, camera: PerspectiveCamera | OrthographicCamera, depthTexture?: DepthTexture | Texture): void;
|
|
36
|
+
doRender(renderer: WebGLRenderer, camera: PerspectiveCamera | OrthographicCamera, depthTexture?: DepthTexture | Texture | null): void;
|
|
37
37
|
get3dObject(): Group;
|
|
38
38
|
private setUniform;
|
|
39
39
|
updateActiveChannels(channelcolors: FuseChannel[], channeldata: Channel[]): void;
|
|
@@ -18,7 +18,7 @@ export declare class ThreeJsPanel {
|
|
|
18
18
|
scene: Scene;
|
|
19
19
|
private meshRenderTarget;
|
|
20
20
|
private meshRenderToBuffer;
|
|
21
|
-
animateFuncs: ((renderer: WebGLRenderer, camera: PerspectiveCamera | OrthographicCamera, depthTexture?: DepthTexture) => void)[];
|
|
21
|
+
animateFuncs: ((renderer: WebGLRenderer, camera: PerspectiveCamera | OrthographicCamera, depthTexture?: DepthTexture | null) => void)[];
|
|
22
22
|
private inRenderLoop;
|
|
23
23
|
private requestedRender;
|
|
24
24
|
hasWebGL2: boolean;
|
|
@@ -81,7 +81,7 @@ export declare class ThreeJsPanel {
|
|
|
81
81
|
replaceCamera(newCam: PerspectiveCamera | OrthographicCamera): void;
|
|
82
82
|
replaceControls(newControls: TrackballControls): void;
|
|
83
83
|
switchViewMode(mode: string): void;
|
|
84
|
-
getMeshDepthTexture(): DepthTexture;
|
|
84
|
+
getMeshDepthTexture(): DepthTexture | null;
|
|
85
85
|
resize(comp: HTMLElement | null, w?: number, h?: number, _ow?: number, _oh?: number, _eOpts?: unknown): void;
|
|
86
86
|
setClearColor(color: Color, alpha: number): void;
|
|
87
87
|
getWidth(): number;
|
package/es/types/View3d.d.ts
CHANGED
|
@@ -49,12 +49,16 @@ export declare class View3d {
|
|
|
49
49
|
*/
|
|
50
50
|
capture(dataurlcallback: (name: string) => void): void;
|
|
51
51
|
getDOMElement(): HTMLDivElement;
|
|
52
|
+
getCanvasDOMElement(): HTMLCanvasElement;
|
|
52
53
|
getCameraState(): CameraState;
|
|
53
54
|
setCameraState(transform: Partial<CameraState>): void;
|
|
54
55
|
/**
|
|
55
56
|
* Force a redraw.
|
|
57
|
+
* @param synchronous If true, the redraw will be done synchronously. If false (default), the
|
|
58
|
+
* redraw will be done asynchronously via `requestAnimationFrame`. Redraws should be done async
|
|
59
|
+
* whenever possible for the best performance.
|
|
56
60
|
*/
|
|
57
|
-
redraw(): void;
|
|
61
|
+
redraw(synchronous?: boolean): void;
|
|
58
62
|
unsetImage(): VolumeDrawable | undefined;
|
|
59
63
|
/**
|
|
60
64
|
* Add a new volume image to the viewer. (The viewer currently only supports a single image at a time - adding repeatedly, without removing in between, is a potential resource leak)
|
|
@@ -92,7 +96,7 @@ export declare class View3d {
|
|
|
92
96
|
onVolumeChannelAdded(volume: Volume, newChannelIndex: number): void;
|
|
93
97
|
onVolumeLoadError(volume: Volume, error: unknown): void;
|
|
94
98
|
setLoadErrorHandler(handler: ((volume: Volume, error: unknown) => void) | undefined): void;
|
|
95
|
-
setTime(volume: Volume, time: number, onChannelLoaded?: PerChannelCallback): void
|
|
99
|
+
setTime(volume: Volume, time: number, onChannelLoaded?: PerChannelCallback): Promise<void>;
|
|
96
100
|
/**
|
|
97
101
|
* Nudge the scale level loaded into this volume off the one chosen by the loader.
|
|
98
102
|
* E.g. a bias of `1` will load 1 scale level lower than "ideal."
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { Chunk, DataType } from "zarrita";
|
|
2
|
+
export type CacheData = ArrayBuffer | Chunk<DataType>;
|
|
3
|
+
export declare const isChunk: (data: CacheData) => data is Chunk<DataType>;
|
|
1
4
|
export default class VolumeCache {
|
|
2
5
|
private entries;
|
|
3
6
|
readonly maxSize: number;
|
|
@@ -31,11 +34,11 @@ export default class VolumeCache {
|
|
|
31
34
|
* Adds a new entry to the cache.
|
|
32
35
|
* @returns {boolean} a boolean indicating whether the insertion succeeded.
|
|
33
36
|
*/
|
|
34
|
-
insert(key: string, data:
|
|
37
|
+
insert(key: string, data: CacheData): boolean;
|
|
35
38
|
/** Internal implementation of `get`. Returns all entry metadata, not just the raw data. */
|
|
36
39
|
private getEntry;
|
|
37
40
|
/** Attempts to get a single entry from the cache. */
|
|
38
|
-
get(key: string):
|
|
41
|
+
get(key: string): CacheData | undefined;
|
|
39
42
|
/** Clears all cache entries whose keys begin with the specified prefix. */
|
|
40
43
|
clearWithPrefix(prefix: string): void;
|
|
41
44
|
/** Clears all data from the cache. */
|
|
@@ -56,7 +56,7 @@ export default class VolumeDrawable {
|
|
|
56
56
|
setGamma(gmin: number, glevel: number, gmax: number): void;
|
|
57
57
|
setFlipAxes(flipX: -1 | 1, flipY: -1 | 1, flipZ: -1 | 1): void;
|
|
58
58
|
setMaxProjectMode(isMaxProject: boolean): void;
|
|
59
|
-
onAnimate(renderer: WebGLRenderer, camera: PerspectiveCamera | OrthographicCamera, depthTexture?: DepthTexture | Texture): void;
|
|
59
|
+
onAnimate(renderer: WebGLRenderer, camera: PerspectiveCamera | OrthographicCamera, depthTexture?: DepthTexture | Texture | null): void;
|
|
60
60
|
getViewMode(): Axis;
|
|
61
61
|
getIsovalue(channel: number): number | undefined;
|
|
62
62
|
hasIsosurface(channel: number): boolean;
|
|
@@ -13,7 +13,7 @@ export interface VolumeRenderImpl {
|
|
|
13
13
|
*/
|
|
14
14
|
updateSettings: (settings: VolumeRenderSettings, dirtyFlags?: number | SettingsFlags) => void;
|
|
15
15
|
get3dObject: () => Object3D;
|
|
16
|
-
doRender: (renderer: WebGLRenderer, camera: PerspectiveCamera | OrthographicCamera, depthTexture?: DepthTexture | Texture) => void;
|
|
16
|
+
doRender: (renderer: WebGLRenderer, camera: PerspectiveCamera | OrthographicCamera, depthTexture?: DepthTexture | Texture | null) => void;
|
|
17
17
|
updateVolumeDimensions: () => void;
|
|
18
18
|
cleanup: () => void;
|
|
19
19
|
viewpointMoved: () => void;
|
package/es/types/index.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { Light, AREA_LIGHT, SKY_LIGHT } from "./Light.js";
|
|
|
22
22
|
export type { ImageInfo } from "./ImageInfo.js";
|
|
23
23
|
export type { ControlPoint } from "./Lut.js";
|
|
24
24
|
export type { CreateLoaderOptions } from "./loaders/index.js";
|
|
25
|
-
export type { IVolumeLoader, PerChannelCallback } from "./loaders/IVolumeLoader.js";
|
|
25
|
+
export type { IVolumeLoader, PerChannelCallback, ThreadableVolumeLoader } from "./loaders/IVolumeLoader.js";
|
|
26
26
|
export type { ZarrLoaderFetchOptions } from "./loaders/OmeZarrLoader.js";
|
|
27
27
|
export type { WorkerLoader } from "./workers/VolumeLoaderContext.js";
|
|
28
28
|
export { Histogram, Lut, remapControlPoints, View3d, Volume, VolumeDrawable, LoadSpec, VolumeMaker, VolumeCache, RequestQueue, SubscribableRequestQueue, PrefetchDirection, OMEZarrLoader, JsonImageInfoLoader, RawArrayLoader, type RawArrayData, type RawArrayInfo, type RawArrayLoaderOptions, TiffLoader, VolumeLoaderContext, VolumeLoadError, VolumeLoadErrorType, VolumeFileFormat, createVolumeLoader, Channel, Light, ViewportCorner, AREA_LIGHT, RENDERMODE_PATHTRACE, RENDERMODE_RAYMARCH, SKY_LIGHT, type CameraState, };
|