@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.
Files changed (50) hide show
  1. package/README.md +21 -13
  2. package/es/View3d.js +21 -5
  3. package/es/Volume.js +1 -1
  4. package/es/VolumeCache.js +10 -3
  5. package/es/VolumeRenderSettings.js +11 -0
  6. package/es/loaders/JsonImageInfoLoader.js +2 -1
  7. package/es/loaders/OmeZarrLoader.js +30 -31
  8. package/es/loaders/TiffLoader.js +3 -1
  9. package/es/loaders/VolumeLoadError.js +1 -1
  10. package/es/loaders/zarr_utils/ChunkPrefetchIterator.js +7 -0
  11. package/es/loaders/zarr_utils/validation.js +18 -7
  12. package/es/loaders/zarr_utils/wrapArray.js +39 -0
  13. package/es/types/NaiveSurfaceNets.d.ts +1 -1
  14. package/es/types/RayMarchedAtlasVolume.d.ts +1 -1
  15. package/es/types/ThreeJsPanel.d.ts +2 -2
  16. package/es/types/TrackballControls.d.ts +1 -1
  17. package/es/types/View3d.d.ts +6 -2
  18. package/es/types/VolumeCache.d.ts +5 -2
  19. package/es/types/VolumeDrawable.d.ts +1 -1
  20. package/es/types/VolumeRenderImpl.d.ts +1 -1
  21. package/es/types/index.d.ts +1 -1
  22. package/es/types/loaders/zarr_utils/types.d.ts +17 -12
  23. package/es/types/loaders/zarr_utils/validation.d.ts +14 -2
  24. package/es/types/loaders/zarr_utils/wrapArray.d.ts +7 -0
  25. package/es/types/workers/VolumeLoaderContext.d.ts +9 -13
  26. package/es/types/workers/types.d.ts +25 -16
  27. package/es/workers/VolumeLoadWorker.js +54 -32
  28. package/es/workers/VolumeLoaderContext.js +52 -51
  29. package/es/workers/types.js +17 -7
  30. package/package.json +14 -14
  31. package/es/loaders/zarr_utils/WrappedStore.js +0 -51
  32. package/es/test/ChunkPrefetchIterator.test.js +0 -208
  33. package/es/test/RequestQueue.test.js +0 -442
  34. package/es/test/SubscribableRequestQueue.test.js +0 -244
  35. package/es/test/VolumeCache.test.js +0 -118
  36. package/es/test/VolumeRenderSettings.test.js +0 -71
  37. package/es/test/lut.test.js +0 -671
  38. package/es/test/num_utils.test.js +0 -140
  39. package/es/test/volume.test.js +0 -98
  40. package/es/test/zarr_utils.test.js +0 -358
  41. package/es/types/loaders/zarr_utils/WrappedStore.d.ts +0 -24
  42. package/es/types/test/ChunkPrefetchIterator.test.d.ts +0 -1
  43. package/es/types/test/RequestQueue.test.d.ts +0 -1
  44. package/es/types/test/SubscribableRequestQueue.test.d.ts +0 -1
  45. package/es/types/test/VolumeCache.test.d.ts +0 -1
  46. package/es/types/test/VolumeRenderSettings.test.d.ts +0 -1
  47. package/es/types/test/lut.test.d.ts +0 -1
  48. package/es/types/test/num_utils.test.d.ts +0 -1
  49. package/es/types/test/volume.test.d.ts +0 -1
  50. 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
- This is a WebGL canvas-based volume viewer. It can display multichannel volume data with high channel counts, and is optimized for OME-Zarr files. With OME-Zarr, the viewer can prefetch and cache Zarr chunks in browser memory for optimized performance.
3
+ ![NPM Version](https://img.shields.io/npm/v/%40aics%2Fvole-core)
4
+ ![NPM Last Update](https://img.shields.io/npm/last-update/%40aics%2Fvole-core)
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
- # Example
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
- # React example
65
+ ## React example
63
66
 
64
- See [vole-app](https://github.com/allen-cell-animated/website-3d-cell-viewer) for a complete application that wraps View3D in a React component.
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
- # Acknowledgements
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
- ## BisQue license
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
- Copyright (c) 2011, T. Kroes <t.kroes@tudelft.nl>
113
- All rights reserved.
114
- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
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
- 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.
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
- this.canvas3d.redraw();
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
- this.loaderContext?.getActiveLoader()?.updateFetchOptions({
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
- this.loaderContext?.getActiveLoader()?.updateFetchOptions({
854
+ loader?.updateFetchOptions?.({
839
855
  onlyPriorityDirections: !event.value
840
856
  });
841
857
  });
package/es/Volume.js CHANGED
@@ -139,7 +139,7 @@ export default class Volume {
139
139
  }
140
140
  }
141
141
  if (shouldReload) {
142
- this.loadNewData(onChannelLoaded);
142
+ await this.loadNewData(onChannelLoaded);
143
143
  }
144
144
  }
145
145
  async loadScaleLevelDims() {
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.byteLength;
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
- if (data.byteLength > this.maxSize) {
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 += data.byteLength;
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 "@zarrita/core";
3
- import { get as zarrGet, slice } from "@zarrita/indexing";
4
- // Importing `FetchStore` from its home subpackage (@zarrita/storage) causes errors.
5
- // Getting it from the top-level package means we don't get its type. This is also a bug, but it's more acceptable.
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 WrappedStore from "./zarr_utils/WrappedStore.js";
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 { validateOMEZarrMetadata } from "./zarr_utils/validation.js";
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 WrappedStore(new FetchStore(url), cache, queue);
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 > group.attrs.multiscales?.length) {
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(group.attrs, scene, urlsArr.length > 1 ? `Zarr source ${i}` : "Zarr");
114
+ validateOMEZarrMetadata(meta, scene, sourceName);
113
115
  const {
114
116
  multiscales,
115
117
  omero
116
- } = group.attrs;
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
- async prefetchChunk(scaleLevel, coords, subscriber) {
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
- await store.get(key, {
337
+ scaleLevel.getChunk(this.orderByDimension(coords), {
342
338
  subscriber,
343
339
  isPrefetch: true
344
- }).catch(wrapVolumeLoadError(`Unable to prefetch chunk with key ${key}`, VolumeLoadErrorType.LOAD_DATA_FAILED, CHUNK_REQUEST_CANCEL_REASON));
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
- key
351
+ coords
353
352
  }) => {
354
- const numDims = getDimensionCount(this.sources[sourceIdx].axesTCZYX);
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 reportKeyBase = (sourceIdx, key, sub) => {
436
+ const reportChunkBase = (sourceIdx, coords, sub) => {
439
437
  if (sub === subscriber) {
440
438
  keys.push({
441
439
  sourceIdx,
442
- key
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 reportKey = (key, sub) => reportKeyBase(sourceIdx, key, sub);
462
- const result = await zarrGet(level, sliceSpec, {
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
- reportKey
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) {
@@ -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 "@zarrita/core";
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 `data` has the minimal amount of data required to open a volume. Since
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(data, multiscaleIdx = 0, name = "zarr") {
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 = data.multiscales[multiscaleIdx];
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;
@@ -1,5 +1,5 @@
1
1
  export default TrackballControls;
2
- declare class TrackballControls extends EventDispatcher<import("three").Event> {
2
+ declare class TrackballControls extends EventDispatcher<any> {
3
3
  constructor(object: any, domElement: any);
4
4
  object: any;
5
5
  domElement: any;
@@ -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: ArrayBuffer): boolean;
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): ArrayBuffer | undefined;
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;
@@ -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, };