@aics/vole-core 3.12.4

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 (141) hide show
  1. package/LICENSE.txt +26 -0
  2. package/README.md +119 -0
  3. package/es/Atlas2DSlice.js +224 -0
  4. package/es/Channel.js +264 -0
  5. package/es/FileSaver.js +31 -0
  6. package/es/FusedChannelData.js +192 -0
  7. package/es/Histogram.js +250 -0
  8. package/es/ImageInfo.js +127 -0
  9. package/es/Light.js +74 -0
  10. package/es/Lut.js +500 -0
  11. package/es/MarchingCubes.js +507 -0
  12. package/es/MeshVolume.js +334 -0
  13. package/es/NaiveSurfaceNets.js +251 -0
  14. package/es/PathTracedVolume.js +482 -0
  15. package/es/RayMarchedAtlasVolume.js +250 -0
  16. package/es/RenderToBuffer.js +31 -0
  17. package/es/ThreeJsPanel.js +633 -0
  18. package/es/Timing.js +28 -0
  19. package/es/TrackballControls.js +538 -0
  20. package/es/View3d.js +848 -0
  21. package/es/Volume.js +352 -0
  22. package/es/VolumeCache.js +161 -0
  23. package/es/VolumeDims.js +16 -0
  24. package/es/VolumeDrawable.js +702 -0
  25. package/es/VolumeMaker.js +101 -0
  26. package/es/VolumeRenderImpl.js +1 -0
  27. package/es/VolumeRenderSettings.js +203 -0
  28. package/es/constants/basicShaders.js +29 -0
  29. package/es/constants/colors.js +59 -0
  30. package/es/constants/denoiseShader.js +43 -0
  31. package/es/constants/lights.js +42 -0
  32. package/es/constants/materials.js +85 -0
  33. package/es/constants/pathtraceOutputShader.js +13 -0
  34. package/es/constants/scaleBarSVG.js +21 -0
  35. package/es/constants/time.js +34 -0
  36. package/es/constants/volumePTshader.js +153 -0
  37. package/es/constants/volumeRayMarchShader.js +123 -0
  38. package/es/constants/volumeSliceShader.js +115 -0
  39. package/es/index.js +21 -0
  40. package/es/loaders/IVolumeLoader.js +131 -0
  41. package/es/loaders/JsonImageInfoLoader.js +255 -0
  42. package/es/loaders/OmeZarrLoader.js +495 -0
  43. package/es/loaders/OpenCellLoader.js +65 -0
  44. package/es/loaders/RawArrayLoader.js +89 -0
  45. package/es/loaders/TiffLoader.js +219 -0
  46. package/es/loaders/VolumeLoadError.js +44 -0
  47. package/es/loaders/VolumeLoaderUtils.js +221 -0
  48. package/es/loaders/index.js +40 -0
  49. package/es/loaders/zarr_utils/ChunkPrefetchIterator.js +143 -0
  50. package/es/loaders/zarr_utils/WrappedStore.js +51 -0
  51. package/es/loaders/zarr_utils/types.js +24 -0
  52. package/es/loaders/zarr_utils/utils.js +225 -0
  53. package/es/loaders/zarr_utils/validation.js +49 -0
  54. package/es/test/ChunkPrefetchIterator.test.js +208 -0
  55. package/es/test/RequestQueue.test.js +442 -0
  56. package/es/test/SubscribableRequestQueue.test.js +244 -0
  57. package/es/test/VolumeCache.test.js +118 -0
  58. package/es/test/VolumeRenderSettings.test.js +71 -0
  59. package/es/test/lut.test.js +671 -0
  60. package/es/test/num_utils.test.js +140 -0
  61. package/es/test/volume.test.js +98 -0
  62. package/es/test/zarr_utils.test.js +358 -0
  63. package/es/types/Atlas2DSlice.d.ts +41 -0
  64. package/es/types/Channel.d.ts +44 -0
  65. package/es/types/FileSaver.d.ts +6 -0
  66. package/es/types/FusedChannelData.d.ts +26 -0
  67. package/es/types/Histogram.d.ts +57 -0
  68. package/es/types/ImageInfo.d.ts +87 -0
  69. package/es/types/Light.d.ts +27 -0
  70. package/es/types/Lut.d.ts +67 -0
  71. package/es/types/MarchingCubes.d.ts +53 -0
  72. package/es/types/MeshVolume.d.ts +40 -0
  73. package/es/types/NaiveSurfaceNets.d.ts +11 -0
  74. package/es/types/PathTracedVolume.d.ts +65 -0
  75. package/es/types/RayMarchedAtlasVolume.d.ts +41 -0
  76. package/es/types/RenderToBuffer.d.ts +17 -0
  77. package/es/types/ThreeJsPanel.d.ts +107 -0
  78. package/es/types/Timing.d.ts +11 -0
  79. package/es/types/TrackballControls.d.ts +51 -0
  80. package/es/types/View3d.d.ts +357 -0
  81. package/es/types/Volume.d.ts +152 -0
  82. package/es/types/VolumeCache.d.ts +43 -0
  83. package/es/types/VolumeDims.d.ts +28 -0
  84. package/es/types/VolumeDrawable.d.ts +108 -0
  85. package/es/types/VolumeMaker.d.ts +49 -0
  86. package/es/types/VolumeRenderImpl.d.ts +22 -0
  87. package/es/types/VolumeRenderSettings.d.ts +98 -0
  88. package/es/types/constants/basicShaders.d.ts +4 -0
  89. package/es/types/constants/colors.d.ts +2 -0
  90. package/es/types/constants/denoiseShader.d.ts +40 -0
  91. package/es/types/constants/lights.d.ts +38 -0
  92. package/es/types/constants/materials.d.ts +20 -0
  93. package/es/types/constants/pathtraceOutputShader.d.ts +11 -0
  94. package/es/types/constants/scaleBarSVG.d.ts +2 -0
  95. package/es/types/constants/time.d.ts +19 -0
  96. package/es/types/constants/volumePTshader.d.ts +137 -0
  97. package/es/types/constants/volumeRayMarchShader.d.ts +117 -0
  98. package/es/types/constants/volumeSliceShader.d.ts +109 -0
  99. package/es/types/glsl.d.js +0 -0
  100. package/es/types/index.d.ts +28 -0
  101. package/es/types/loaders/IVolumeLoader.d.ts +113 -0
  102. package/es/types/loaders/JsonImageInfoLoader.d.ts +80 -0
  103. package/es/types/loaders/OmeZarrLoader.d.ts +87 -0
  104. package/es/types/loaders/OpenCellLoader.d.ts +9 -0
  105. package/es/types/loaders/RawArrayLoader.d.ts +33 -0
  106. package/es/types/loaders/TiffLoader.d.ts +45 -0
  107. package/es/types/loaders/VolumeLoadError.d.ts +18 -0
  108. package/es/types/loaders/VolumeLoaderUtils.d.ts +38 -0
  109. package/es/types/loaders/index.d.ts +22 -0
  110. package/es/types/loaders/zarr_utils/ChunkPrefetchIterator.d.ts +22 -0
  111. package/es/types/loaders/zarr_utils/WrappedStore.d.ts +24 -0
  112. package/es/types/loaders/zarr_utils/types.d.ts +94 -0
  113. package/es/types/loaders/zarr_utils/utils.d.ts +23 -0
  114. package/es/types/loaders/zarr_utils/validation.d.ts +7 -0
  115. package/es/types/test/ChunkPrefetchIterator.test.d.ts +1 -0
  116. package/es/types/test/RequestQueue.test.d.ts +1 -0
  117. package/es/types/test/SubscribableRequestQueue.test.d.ts +1 -0
  118. package/es/types/test/VolumeCache.test.d.ts +1 -0
  119. package/es/types/test/VolumeRenderSettings.test.d.ts +1 -0
  120. package/es/types/test/lut.test.d.ts +1 -0
  121. package/es/types/test/num_utils.test.d.ts +1 -0
  122. package/es/types/test/volume.test.d.ts +1 -0
  123. package/es/types/test/zarr_utils.test.d.ts +1 -0
  124. package/es/types/types.d.ts +115 -0
  125. package/es/types/utils/RequestQueue.d.ts +112 -0
  126. package/es/types/utils/SubscribableRequestQueue.d.ts +52 -0
  127. package/es/types/utils/num_utils.d.ts +43 -0
  128. package/es/types/workers/VolumeLoaderContext.d.ts +106 -0
  129. package/es/types/workers/types.d.ts +101 -0
  130. package/es/types/workers/util.d.ts +3 -0
  131. package/es/types.js +75 -0
  132. package/es/typings.d.js +0 -0
  133. package/es/utils/RequestQueue.js +267 -0
  134. package/es/utils/SubscribableRequestQueue.js +187 -0
  135. package/es/utils/num_utils.js +231 -0
  136. package/es/workers/FetchTiffWorker.js +153 -0
  137. package/es/workers/VolumeLoadWorker.js +129 -0
  138. package/es/workers/VolumeLoaderContext.js +271 -0
  139. package/es/workers/types.js +41 -0
  140. package/es/workers/util.js +8 -0
  141. package/package.json +83 -0
package/es/Volume.js ADDED
@@ -0,0 +1,352 @@
1
+ import { Vector3 } from "three";
2
+ import Channel from "./Channel.js";
3
+ import { getColorByChannelIndex } from "./constants/colors.js";
4
+ import { LoadSpec } from "./loaders/IVolumeLoader.js";
5
+ import { MAX_ATLAS_EDGE, pickLevelToLoadUnscaled } from "./loaders/VolumeLoaderUtils.js";
6
+ import { CImageInfo, defaultImageInfo } from "./ImageInfo.js";
7
+ /**
8
+ * A renderable multichannel volume image with 8-bits per channel intensity values.
9
+ * @class
10
+ * @param {ImageInfo} imageInfo
11
+ */
12
+ export default class Volume {
13
+ /** `LoadSpec` representing the minimum data required to display what's in the viewer (subregion, channels, etc.).
14
+ * Used to intelligently issue load requests whenever required by a state change. Modify with `updateRequiredData`.
15
+ */
16
+
17
+ /** The maximum of the measurements of 3 axes in physical units (pixels*physicalSize) */
18
+
19
+ /** The physical size of a voxel in the original level 0 volume */
20
+
21
+ /** The physical dims of the whole volume (not accounting for subregion) */
22
+
23
+ /** Normalized physical size of the whole volume (not accounting for subregion) */
24
+
25
+ constructor(imageInfo = defaultImageInfo(), loadSpec = new LoadSpec(), loader) {
26
+ this.loaded = false;
27
+ this.imageInfo = new CImageInfo(imageInfo);
28
+ // TODO: use getter?
29
+ this.name = imageInfo.name;
30
+ this.loadSpec = {
31
+ // Fill in defaults for optional properties
32
+ multiscaleLevel: 0,
33
+ scaleLevelBias: 0,
34
+ maxAtlasEdge: MAX_ATLAS_EDGE,
35
+ channels: Array.from({
36
+ length: this.imageInfo.numChannels
37
+ }, (_val, idx) => idx),
38
+ ...loadSpec
39
+ };
40
+ this.loadSpecRequired = {
41
+ ...this.loadSpec,
42
+ channels: this.loadSpec.channels.slice(),
43
+ subregion: this.loadSpec.subregion.clone()
44
+ };
45
+ this.loader = loader;
46
+ // imageMetadata to be filled in by Volume Loaders
47
+ this.imageMetadata = {};
48
+ this.normRegionSize = new Vector3(1, 1, 1);
49
+ this.normRegionOffset = new Vector3(0, 0, 0);
50
+ this.physicalSize = new Vector3(1, 1, 1);
51
+ this.physicalScale = 1;
52
+ this.normPhysicalSize = new Vector3(1, 1, 1);
53
+ this.physicalPixelSize = this.imageInfo.physicalPixelSize;
54
+ this.tickMarkPhysicalLength = 1;
55
+ this.setVoxelSize(this.physicalPixelSize);
56
+ this.numChannels = this.imageInfo.numChannels;
57
+ this.channelNames = this.imageInfo.channelNames.slice();
58
+ this.channelColorsDefault = this.imageInfo.channelColors ? this.imageInfo.channelColors.slice() : this.channelNames.map((name, index) => getColorByChannelIndex(index));
59
+ // fill in gaps
60
+ if (this.channelColorsDefault.length < this.imageInfo.numChannels) {
61
+ for (let i = this.channelColorsDefault.length - 1; i < this.imageInfo.numChannels; ++i) {
62
+ this.channelColorsDefault[i] = getColorByChannelIndex(i);
63
+ }
64
+ }
65
+ this.channels = [];
66
+ for (let i = 0; i < this.imageInfo.numChannels; ++i) {
67
+ const channel = new Channel(this.channelNames[i]);
68
+ this.channels.push(channel);
69
+ // TODO pass in channel constructor...
70
+ channel.dims = this.imageInfo.subregionSize.toArray();
71
+ }
72
+ this.physicalUnitSymbol = this.imageInfo.spatialUnit;
73
+ this.volumeDataObservers = [];
74
+ }
75
+ setUnloaded() {
76
+ this.loaded = false;
77
+ this.channels.forEach(channel => {
78
+ channel.loaded = false;
79
+ });
80
+ }
81
+ isLoaded() {
82
+ return this.loaded;
83
+ }
84
+ updateDimensions() {
85
+ const {
86
+ volumeSize,
87
+ subregionSize,
88
+ subregionOffset
89
+ } = this.imageInfo;
90
+ this.setVoxelSize(this.physicalPixelSize);
91
+ this.normRegionSize = subregionSize.clone().divide(volumeSize);
92
+ this.normRegionOffset = subregionOffset.clone().divide(volumeSize);
93
+ }
94
+
95
+ /** Returns `true` iff differences between `loadSpec` and `loadSpecRequired` indicate new data *must* be loaded. */
96
+ mustLoadNewData() {
97
+ return this.loadSpec.useExplicitLevel !== this.loadSpecRequired.useExplicitLevel ||
98
+ // explicit vs automatic level changed
99
+ this.loadSpec.time !== this.loadSpecRequired.time ||
100
+ // time point changed
101
+ !this.loadSpec.subregion.containsBox(this.loadSpecRequired.subregion) ||
102
+ // new subregion not contained in old
103
+ this.loadSpecRequired.channels.some(channel => !this.loadSpec.channels.includes(channel)) // new channel(s)
104
+ ;
105
+ }
106
+
107
+ /**
108
+ * Returns `true` iff differences between `loadSpec` and `loadSpecRequired` indicate a new load *may* get a
109
+ * different scale level than is currently loaded.
110
+ *
111
+ * This checks for changes in properties that *can*, but do not *always*, change the scale level the loader picks.
112
+ * For example, a smaller `subregion` *may* mean a higher scale level will fit within memory constraints, or it may
113
+ * not. A higher `scaleLevelBias` *may* nudge the volume into a higher scale level, or we may already be at the max
114
+ * imposed by `multiscaleLevel`.
115
+ */
116
+ mayLoadNewScaleLevel() {
117
+ return !this.loadSpec.subregion.equals(this.loadSpecRequired.subregion) || this.loadSpecRequired.maxAtlasEdge !== this.loadSpec.maxAtlasEdge || this.loadSpecRequired.multiscaleLevel !== this.loadSpec.multiscaleLevel || this.loadSpecRequired.scaleLevelBias !== this.loadSpec.scaleLevelBias;
118
+ }
119
+
120
+ /** Call on any state update that may require new data to be loaded (subregion, enabled channels, time, etc.) */
121
+ async updateRequiredData(required, onChannelLoaded) {
122
+ this.loadSpecRequired = {
123
+ ...this.loadSpecRequired,
124
+ ...required
125
+ };
126
+ let shouldReload = this.mustLoadNewData();
127
+
128
+ // If we're not reloading due to required data changes, check if we should load a new scale level
129
+ if (!shouldReload && this.mayLoadNewScaleLevel()) {
130
+ // Loaders should cache loaded dimensions so that this call blocks no more than once per valid `LoadSpec`.
131
+ const dims = await this.loadScaleLevelDims();
132
+ if (dims) {
133
+ const dimsZYX = dims.map(({
134
+ shape
135
+ }) => [shape[2], shape[3], shape[4]]);
136
+ // Determine which scale level *would* be loaded, and see if it's different than what we have
137
+ const levelToLoad = pickLevelToLoadUnscaled(this.loadSpecRequired, dimsZYX);
138
+ shouldReload = this.imageInfo.multiscaleLevel !== levelToLoad;
139
+ }
140
+ }
141
+ if (shouldReload) {
142
+ this.loadNewData(onChannelLoaded);
143
+ }
144
+ }
145
+ async loadScaleLevelDims() {
146
+ try {
147
+ return await this.loader?.loadDims(this.loadSpecRequired);
148
+ } catch (e) {
149
+ this.volumeDataObservers.forEach(observer => observer.onVolumeLoadError(this, e));
150
+ return undefined;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Loads new data as specified in `this.loadSpecRequired`. Clones `loadSpecRequired` into `loadSpec` to indicate
156
+ * that the data that *must* be loaded is now the data that *has* been loaded.
157
+ */
158
+ async loadNewData(onChannelLoaded) {
159
+ this.setUnloaded();
160
+ this.loadSpec = {
161
+ ...this.loadSpecRequired,
162
+ subregion: this.loadSpecRequired.subregion.clone()
163
+ };
164
+ try {
165
+ await this.loader?.loadVolumeData(this, undefined, onChannelLoaded);
166
+ } catch (e) {
167
+ this.volumeDataObservers.forEach(observer => observer.onVolumeLoadError(this, e));
168
+ throw e;
169
+ }
170
+ }
171
+
172
+ // we calculate the physical size of the volume (voxels*pixel_size)
173
+ // and then normalize to the max physical dimension
174
+ // NOTE: This function MUST be called to set up some important dimensional info
175
+ setVoxelSize(size) {
176
+ // only set the data if it is > 0. zero is not an allowed value.
177
+ size.x = size.x > 0 ? size.x : 1.0;
178
+ size.y = size.y > 0 ? size.y : 1.0;
179
+ size.z = size.z > 0 ? size.z : 1.0;
180
+ this.physicalPixelSize = size;
181
+ this.physicalSize = this.imageInfo.originalSize.clone().multiply(this.physicalPixelSize);
182
+ // Volume is scaled such that its largest physical dimension is 1 world unit - save that dimension for conversions
183
+ this.physicalScale = Math.max(this.physicalSize.x, this.physicalSize.y, this.physicalSize.z);
184
+ // Compute the volume's max extent - scaled to max dimension.
185
+ this.normPhysicalSize = this.physicalSize.clone().divideScalar(this.physicalScale);
186
+ // While we're here, pick a power of 10 that divides into our max dimension a reasonable number of times
187
+ // and save it to be the length of tick marks in 3d.
188
+ this.tickMarkPhysicalLength = 10 ** Math.floor(Math.log10(this.physicalScale / 2));
189
+ }
190
+ setUnitSymbol(symbol) {
191
+ this.physicalUnitSymbol = symbol;
192
+ }
193
+
194
+ /** Computes the center of the volume subset */
195
+ getContentCenter() {
196
+ // center point: (normRegionSize / 2 + normRegionOffset - 0.5) * normPhysicalSize;
197
+ return this.normRegionSize.clone().divideScalar(2).add(this.normRegionOffset).subScalar(0.5).multiply(this.normPhysicalSize);
198
+ }
199
+ cleanup() {
200
+ // no op
201
+ }
202
+ getChannel(channelIndex) {
203
+ return this.channels[channelIndex];
204
+ }
205
+ onChannelLoaded(batch) {
206
+ // check to see if all channels are now loaded, and fire an event(?)
207
+ if (this.loadSpec.channels.every(channelIndex => this.channels[channelIndex].loaded)) {
208
+ this.loaded = true;
209
+ }
210
+ batch.forEach(channelIndex => this.channelLoadCallback?.(this, channelIndex));
211
+ this.volumeDataObservers.forEach(observer => observer.onVolumeData(this, batch));
212
+ }
213
+
214
+ /**
215
+ * Assign volume data via a 2d array containing the z slices as tiles across it. Assumes that the incoming data is consistent with the image's pre-existing imageInfo tile metadata.
216
+ * @param {number} channelIndex
217
+ * @param {Uint8Array} atlasdata
218
+ * @param {number} atlaswidth
219
+ * @param {number} atlasheight
220
+ */
221
+ setChannelDataFromAtlas(channelIndex, atlasdata, atlaswidth, atlasheight, range, dtype = "uint8") {
222
+ this.channels[channelIndex].setFromAtlas(atlasdata, atlaswidth, atlasheight, dtype, range[0], range[1], this.imageInfo.subregionSize);
223
+ this.onChannelLoaded([channelIndex]);
224
+ }
225
+
226
+ // ASSUMES that this.channelData.options is already set and incoming data is consistent with it
227
+ /**
228
+ * Assign volume data as a 3d array ordered x,y,z. The xy size must be equal to tilewidth*tileheight from the imageInfo used to construct this Volume. Assumes that the incoming data is consistent with the image's pre-existing imageInfo tile metadata.
229
+ * @param {number} channelIndex
230
+ * @param {Uint8Array} volumeData
231
+ */
232
+ setChannelDataFromVolume(channelIndex, volumeData, range, dtype = "uint8") {
233
+ const {
234
+ subregionSize,
235
+ atlasTileDims
236
+ } = this.imageInfo;
237
+ this.channels[channelIndex].setFromVolumeData(volumeData, subregionSize.x, subregionSize.y, subregionSize.z, atlasTileDims.x * subregionSize.x, atlasTileDims.y * subregionSize.y, range[0], range[1], dtype);
238
+ this.onChannelLoaded([channelIndex]);
239
+ }
240
+
241
+ // TODO: decide if this should update imageInfo or not. For now, leave imageInfo alone as the "original" data
242
+ /**
243
+ * Add a new channel ready to receive data from one of the setChannelDataFrom* calls.
244
+ * Name and color will be defaulted if not provided. For now, leave imageInfo alone as the "original" data
245
+ * @param {string} name
246
+ * @param {Array.<number>} color [r,g,b]
247
+ */
248
+ appendEmptyChannel(name, color) {
249
+ const idx = this.imageInfo.numChannels;
250
+ const chname = name || "channel_" + idx;
251
+ const chcolor = color || getColorByChannelIndex(idx);
252
+ this.numChannels += 1;
253
+ this.channelNames.push(chname);
254
+ this.channelColorsDefault.push(chcolor);
255
+ this.channels.push(new Channel(chname));
256
+ for (let i = 0; i < this.volumeDataObservers.length; ++i) {
257
+ this.volumeDataObservers[i].onVolumeChannelAdded(this, idx);
258
+ }
259
+ return idx;
260
+ }
261
+
262
+ /**
263
+ * Get a value from the volume data
264
+ * @return {number} the intensity value from the given channel at the given xyz location
265
+ * @param {number} c The channel index
266
+ * @param {number} x
267
+ * @param {number} y
268
+ * @param {number} z
269
+ */
270
+ getIntensity(c, x, y, z) {
271
+ return this.channels[c].getIntensity(x, y, z);
272
+ }
273
+
274
+ /**
275
+ * Get the 256-bin histogram for the given channel
276
+ * @return {Histogram} the histogram
277
+ * @param {number} c The channel index
278
+ */
279
+ getHistogram(c) {
280
+ return this.channels[c].getHistogram();
281
+ }
282
+
283
+ /**
284
+ * Set the lut for the given channel
285
+ * @param {number} c The channel index
286
+ * @param {Array.<number>} lut The lut as a 256 element array
287
+ */
288
+ setLut(c, lut) {
289
+ this.channels[c].setLut(lut);
290
+ }
291
+
292
+ /**
293
+ * Set the color palette for the given channel
294
+ * @param {number} c The channel index
295
+ * @param {Array.<number>} palette The colors as a 256 element array * RGBA
296
+ */
297
+ setColorPalette(c, palette) {
298
+ this.channels[c].setColorPalette(palette);
299
+ }
300
+
301
+ /**
302
+ * Set the color palette alpha multiplier for the given channel.
303
+ * This will blend between the ordinary color lut and this colorPalette lut.
304
+ * @param {number} c The channel index
305
+ * @param {number} alpha The alpha value as a number from 0 to 1
306
+ */
307
+ setColorPaletteAlpha(c, alpha) {
308
+ this.channels[c].setColorPaletteAlpha(alpha);
309
+ }
310
+
311
+ /**
312
+ * Return the intrinsic rotation associated with this volume (radians)
313
+ * @return {Array.<number>} the xyz Euler angles (radians)
314
+ */
315
+ getRotation() {
316
+ // default axis order is XYZ
317
+ return this.imageInfo.transform.rotation.toArray();
318
+ }
319
+
320
+ /**
321
+ * Return the intrinsic translation (pivot center delta) associated with this volume, in normalized volume units
322
+ * @return {Array.<number>} the xyz translation in normalized volume units
323
+ */
324
+ getTranslation() {
325
+ return this.voxelsToWorldSpace(this.imageInfo.transform.translation.toArray());
326
+ }
327
+
328
+ /**
329
+ * Return a translation in normalized volume units, given a translation in image voxels
330
+ * @return {Array.<number>} the xyz translation in normalized volume units
331
+ */
332
+ voxelsToWorldSpace(xyz) {
333
+ // ASSUME: xyz is in original (level 0) image voxels, compatible with physicalPixelSize.
334
+ // account for pixel_size and normalized scaling in the threejs volume representation we're using
335
+ const m = 1.0 / Math.max(this.physicalSize.x, Math.max(this.physicalSize.y, this.physicalSize.z));
336
+ return new Vector3().fromArray(xyz).multiply(this.physicalPixelSize).multiplyScalar(m).toArray();
337
+ }
338
+ addVolumeDataObserver(o) {
339
+ this.volumeDataObservers.push(o);
340
+ }
341
+ removeVolumeDataObserver(o) {
342
+ if (o) {
343
+ const i = this.volumeDataObservers.indexOf(o);
344
+ if (i !== -1) {
345
+ this.volumeDataObservers.splice(i, 1);
346
+ }
347
+ }
348
+ }
349
+ removeAllVolumeDataObservers() {
350
+ this.volumeDataObservers = [];
351
+ }
352
+ }
@@ -0,0 +1,161 @@
1
+ /** Default: 250MB. Should be large enough to be useful but safe for most any computer that can run the app */
2
+ const CACHE_MAX_SIZE_DEFAULT = 250_000_000;
3
+ export default class VolumeCache {
4
+ // Ends of a linked list of entries, to track LRU and evict efficiently
5
+
6
+ // TODO implement some way to manage used vs unused (prefetched) entries so
7
+ // that prefetched entries which are never used don't get highest priority!
8
+
9
+ constructor(maxSize = CACHE_MAX_SIZE_DEFAULT) {
10
+ this.entries = new Map();
11
+ this.maxSize = maxSize;
12
+ this.currentSize = 0;
13
+ this.first = null;
14
+ this.last = null;
15
+ }
16
+
17
+ // Hide these behind getters so they're definitely never set from the outside
18
+ /** The size of all data arrays currently stored in this cache, in bytes. */
19
+ get size() {
20
+ return this.currentSize;
21
+ }
22
+
23
+ /** The number of entries currently stored in this cache. */
24
+ get numberOfEntries() {
25
+ return this.entries.size;
26
+ }
27
+
28
+ /**
29
+ * Removes an entry from a store but NOT the LRU list.
30
+ * Only call from a method with the word "evict" in it!
31
+ */
32
+ removeEntryFromStore(entry) {
33
+ this.entries.delete(entry.key);
34
+ this.currentSize -= entry.data.byteLength;
35
+ }
36
+
37
+ /**
38
+ * Removes an entry from the LRU list but NOT its store.
39
+ * Entry must be replaced in list or removed from store, or it will never be evicted!
40
+ */
41
+ removeEntryFromList(entry) {
42
+ const {
43
+ prev,
44
+ next
45
+ } = entry;
46
+ if (prev) {
47
+ prev.next = next;
48
+ } else {
49
+ this.first = next;
50
+ }
51
+ if (next) {
52
+ next.prev = prev;
53
+ } else {
54
+ this.last = prev;
55
+ }
56
+ }
57
+
58
+ /** Adds an entry which is *not currently in the list* to the front of the list. */
59
+ addEntryAsFirst(entry) {
60
+ if (this.first) {
61
+ this.first.prev = entry;
62
+ } else {
63
+ this.last = entry;
64
+ }
65
+ entry.next = this.first;
66
+ entry.prev = null;
67
+ this.first = entry;
68
+ }
69
+
70
+ /** Moves an entry which is *currently in the list* to the front of the list. */
71
+ moveEntryToFirst(entry) {
72
+ if (entry === this.first) return;
73
+ this.removeEntryFromList(entry);
74
+ this.addEntryAsFirst(entry);
75
+ }
76
+
77
+ /** Evicts the least recently used entry from the cache. */
78
+ evictLast() {
79
+ if (!this.last) {
80
+ console.error("VolumeCache: attempt to evict last entry from cache when no last entry is set");
81
+ return;
82
+ }
83
+ this.removeEntryFromStore(this.last);
84
+ if (this.last.prev) {
85
+ this.last.prev.next = null;
86
+ }
87
+ this.last = this.last.prev;
88
+ }
89
+
90
+ /** Evicts a specific entry from the cache. */
91
+ evict(entry) {
92
+ this.removeEntryFromStore(entry);
93
+ this.removeEntryFromList(entry);
94
+ }
95
+
96
+ /**
97
+ * Adds a new entry to the cache.
98
+ * @returns {boolean} a boolean indicating whether the insertion succeeded.
99
+ */
100
+ insert(key, data) {
101
+ if (data.byteLength > this.maxSize) {
102
+ console.error("VolumeCache: attempt to insert a single entry larger than the cache");
103
+ return false;
104
+ }
105
+
106
+ // Check if entry is already in cache
107
+ // This will move the entry to the front of the LRU list, if present
108
+ const getResult = this.getEntry(key);
109
+ if (getResult !== undefined) {
110
+ getResult.data = data;
111
+ return true;
112
+ }
113
+
114
+ // Add new entry to cache
115
+ const newEntry = {
116
+ data,
117
+ prev: null,
118
+ next: null,
119
+ key
120
+ };
121
+ this.addEntryAsFirst(newEntry);
122
+ this.entries.set(key, newEntry);
123
+ this.currentSize += data.byteLength;
124
+
125
+ // Evict until size is within limit
126
+ while (this.currentSize > this.maxSize) {
127
+ this.evictLast();
128
+ }
129
+ return true;
130
+ }
131
+
132
+ /** Internal implementation of `get`. Returns all entry metadata, not just the raw data. */
133
+ getEntry(key) {
134
+ const result = this.entries.get(key);
135
+ if (result) {
136
+ this.moveEntryToFirst(result);
137
+ }
138
+ return result;
139
+ }
140
+
141
+ /** Attempts to get a single entry from the cache. */
142
+ get(key) {
143
+ return this.getEntry(key)?.data;
144
+ }
145
+
146
+ /** Clears all cache entries whose keys begin with the specified prefix. */
147
+ clearWithPrefix(prefix) {
148
+ for (const [key, entry] of this.entries.entries()) {
149
+ if (key.startsWith(prefix)) {
150
+ this.evict(entry);
151
+ }
152
+ }
153
+ }
154
+
155
+ /** Clears all data from the cache. */
156
+ clear() {
157
+ while (this.last) {
158
+ this.evictLast();
159
+ }
160
+ }
161
+ }
@@ -0,0 +1,16 @@
1
+ import { Vector3 } from "three";
2
+ export function defaultVolumeDims() {
3
+ return {
4
+ shape: [0, 0, 0, 0, 0],
5
+ spacing: [1, 1, 1, 1, 1],
6
+ spaceUnit: "μm",
7
+ timeUnit: "s",
8
+ dataType: "uint8"
9
+ };
10
+ }
11
+ export function volumeSize(volumeDims) {
12
+ return new Vector3(volumeDims.shape[4], volumeDims.shape[3], volumeDims.shape[2]);
13
+ }
14
+ export function physicalPixelSize(volumeDims) {
15
+ return new Vector3(volumeDims.spacing[4], volumeDims.spacing[3], volumeDims.spacing[2]);
16
+ }