@vitessce/spatial-accelerated 3.8.10 → 3.8.13
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/dist/index.js +10480 -1992
- package/dist-tsc/SpatialWrapper.js +2 -2
- package/dist-tsc/VolumeDataManager.d.ts +22 -0
- package/dist-tsc/VolumeDataManager.d.ts.map +1 -1
- package/dist-tsc/VolumeDataManager.js +76 -18
- package/dist-tsc/VolumeRenderManager.d.ts.map +1 -1
- package/dist-tsc/VolumeRenderManager.js +11 -4
- package/dist-tsc/VolumeShader.js +1 -0
- package/dist-tsc/VolumeView.d.ts.map +1 -1
- package/dist-tsc/VolumeView.js +101 -61
- package/dist-tsc/shaders/VolumeFragmentShader.d.ts +1 -1
- package/dist-tsc/shaders/VolumeFragmentShader.d.ts.map +1 -1
- package/dist-tsc/shaders/VolumeFragmentShader.js +22 -4
- package/package.json +11 -11
- package/src/SpatialWrapper.js +6 -6
- package/src/VolumeDataManager.js +91 -21
- package/src/VolumeRenderManager.js +16 -5
- package/src/VolumeShader.js +1 -0
- package/src/VolumeView.js +115 -61
- package/src/shaders/VolumeFragmentShader.js +22 -4
package/src/VolumeDataManager.js
CHANGED
|
@@ -45,7 +45,7 @@ const MAX_CHANNELS = 7; // [0, 1, 2, 3, 4, 5, 6]
|
|
|
45
45
|
// const manualChannelSelection = 0;
|
|
46
46
|
|
|
47
47
|
// Add a constant for initialization status
|
|
48
|
-
const INIT_STATUS = {
|
|
48
|
+
export const INIT_STATUS = {
|
|
49
49
|
NOT_STARTED: 'not_started',
|
|
50
50
|
IN_PROGRESS: 'in_progress',
|
|
51
51
|
COMPLETE: 'complete',
|
|
@@ -507,6 +507,7 @@ export class VolumeDataManager {
|
|
|
507
507
|
this.timeStamp = 0;
|
|
508
508
|
this.k = 40;
|
|
509
509
|
this.noNewRequests = false;
|
|
510
|
+
this.manuallyStopped = false; // Flag to track if user manually stopped loading
|
|
510
511
|
|
|
511
512
|
this.needsBailout = false; // Flag to indicate if we need to bail out of processing requests
|
|
512
513
|
|
|
@@ -517,9 +518,61 @@ export class VolumeDataManager {
|
|
|
517
518
|
// Store the last known configuration for context restoration
|
|
518
519
|
this._lastChannelConfig = null;
|
|
519
520
|
|
|
521
|
+
// Track loading progress
|
|
522
|
+
this.currentRequestCount = 0; // Number of requests currently being processed
|
|
523
|
+
this.totalBricksRequested = 0; // Total number of bricks requested in current batch
|
|
524
|
+
|
|
520
525
|
logWithColor('VolumeDataManager constructor complete');
|
|
521
526
|
}
|
|
522
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Get loading progress information
|
|
530
|
+
* @returns {Object} Loading progress with bricksLoaded, totalBricks, isLoading, and percentage
|
|
531
|
+
*/
|
|
532
|
+
getLoadingProgress() {
|
|
533
|
+
const bricksLoaded = this.bricksEverLoaded.size;
|
|
534
|
+
const isLoading = !this.noNewRequests && this.currentRequestCount > 0;
|
|
535
|
+
const percentage = this.totalBricksRequested > 0
|
|
536
|
+
? ((this.totalBricksRequested - this.currentRequestCount) / this.totalBricksRequested) * 100
|
|
537
|
+
: 0;
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
bricksLoaded,
|
|
541
|
+
currentRequestCount: this.currentRequestCount,
|
|
542
|
+
totalBricksRequested: this.totalBricksRequested,
|
|
543
|
+
isLoading,
|
|
544
|
+
percentage,
|
|
545
|
+
noNewRequests: this.noNewRequests,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Manually stop loading and render at highest resolution
|
|
551
|
+
*/
|
|
552
|
+
stopLoading() {
|
|
553
|
+
log.debug('Manually stopping data loading');
|
|
554
|
+
this.noNewRequests = true;
|
|
555
|
+
this.manuallyStopped = true;
|
|
556
|
+
this.needsBailout = true;
|
|
557
|
+
// Reset progress counters to reflect stopped state
|
|
558
|
+
this.currentRequestCount = 0;
|
|
559
|
+
this.totalBricksRequested = 0;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Manually restart loading
|
|
564
|
+
*/
|
|
565
|
+
restartLoading() {
|
|
566
|
+
log.debug('Manually restarting data loading');
|
|
567
|
+
this.noNewRequests = false;
|
|
568
|
+
this.manuallyStopped = false;
|
|
569
|
+
this.needsBailout = false;
|
|
570
|
+
this.triggerUsage = true;
|
|
571
|
+
// Reset progress counters
|
|
572
|
+
this.currentRequestCount = 0;
|
|
573
|
+
this.totalBricksRequested = 0;
|
|
574
|
+
}
|
|
575
|
+
|
|
523
576
|
/**
|
|
524
577
|
* Handle WebGL context loss
|
|
525
578
|
*/
|
|
@@ -942,23 +995,15 @@ export class VolumeDataManager {
|
|
|
942
995
|
this.ptTHREE = ptTHREE;
|
|
943
996
|
this.bcTHREE = bcTHREE;
|
|
944
997
|
|
|
945
|
-
// log.debug('gl', this.gl);
|
|
946
|
-
// log.debug('renderer', this.renderer);
|
|
947
|
-
|
|
948
998
|
logWithColor('initMRMCPT() COMPLETE');
|
|
949
999
|
}
|
|
950
1000
|
|
|
951
|
-
/*
|
|
952
|
-
testTexture() {
|
|
953
|
-
log.debug('testTexture pt', this.ptTHREE);
|
|
954
|
-
log.debug('testTexture bc', this.bcTHREE);
|
|
955
|
-
}
|
|
956
|
-
*/
|
|
957
1001
|
|
|
958
1002
|
async initTexture() {
|
|
959
1003
|
const requests = [
|
|
960
1004
|
{ x: 0, y: 0, z: 1 },
|
|
961
1005
|
];
|
|
1006
|
+
logWithColor('initTexture - loading first brick');
|
|
962
1007
|
await this.handleBrickRequests(requests);
|
|
963
1008
|
}
|
|
964
1009
|
|
|
@@ -1182,11 +1227,11 @@ export class VolumeDataManager {
|
|
|
1182
1227
|
if (!this.zarrStore || !this.zarrStore.arrays[resolution]) {
|
|
1183
1228
|
throw new Error('Zarr store or resolution not initialized');
|
|
1184
1229
|
}
|
|
1185
|
-
|
|
1186
|
-
// log.debug('loadZarrChunk', t, c, z, y, x, resolution);
|
|
1230
|
+
log.debug('loadZarrChunk', { t, c, z, y, x, resolution });
|
|
1187
1231
|
|
|
1188
1232
|
const array = this.zarrStore.arrays[resolution];
|
|
1189
1233
|
|
|
1234
|
+
|
|
1190
1235
|
// TODO: Use zarrita's get(arr, slice) method (rather than arr.getChunk),
|
|
1191
1236
|
// which will enable reading from arrays with other chunk shapes
|
|
1192
1237
|
// (i.e., not strictly 32x32x32 chunks).
|
|
@@ -1196,8 +1241,6 @@ export class VolumeDataManager {
|
|
|
1196
1241
|
|
|
1197
1242
|
const chunkEntry = await array.getChunk([t, c, z, y, x]);
|
|
1198
1243
|
|
|
1199
|
-
// log.debug('chunkEntry', chunkEntry);
|
|
1200
|
-
|
|
1201
1244
|
if (!chunkEntry) {
|
|
1202
1245
|
throw new Error(`No chunk found at coordinates [${t},${c},${z},${y},${x}]`);
|
|
1203
1246
|
}
|
|
@@ -1216,6 +1259,12 @@ export class VolumeDataManager {
|
|
|
1216
1259
|
return;
|
|
1217
1260
|
}
|
|
1218
1261
|
|
|
1262
|
+
// Check if loading has been manually stopped
|
|
1263
|
+
if (this.noNewRequests) {
|
|
1264
|
+
log.debug('processRequestData: loading stopped by user, skipping');
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1219
1268
|
// Check for context loss
|
|
1220
1269
|
if (this.isContextLost()) {
|
|
1221
1270
|
log.debug('processRequestData: WebGL context is lost, skipping');
|
|
@@ -1522,7 +1571,7 @@ export class VolumeDataManager {
|
|
|
1522
1571
|
|
|
1523
1572
|
log.debug('starting to load zarr chunk', { resolution, z, y, x, zarrChannel });
|
|
1524
1573
|
let chunk = await this.loadZarrChunk(0, zarrChannel, z, y, x, resolution);
|
|
1525
|
-
|
|
1574
|
+
log.debug('chunk', chunk);
|
|
1526
1575
|
|
|
1527
1576
|
// TODO(mark): is there a better way than iterating over all of the chunk values?
|
|
1528
1577
|
if (chunk instanceof Uint16Array) {
|
|
@@ -1538,12 +1587,20 @@ export class VolumeDataManager {
|
|
|
1538
1587
|
log.debug('this.channels.downsampleMin[channel]', this.channels.downsampleMin[channel]);
|
|
1539
1588
|
log.debug('this.channels.downsampleMax[channel]', this.channels.downsampleMax[channel]);
|
|
1540
1589
|
}
|
|
1541
|
-
//
|
|
1542
|
-
//
|
|
1590
|
+
// Use TypedArray view and bitwise operations to reduce memory allocations
|
|
1591
|
+
// Shift right by 8 to convert uint16 to uint8 range
|
|
1592
|
+
const uint16View = new Uint16Array(chunk.buffer);
|
|
1543
1593
|
const uint8Chunk = new Uint8Array(chunk.length);
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1594
|
+
|
|
1595
|
+
const minVal = this.channels.downsampleMin[channel];
|
|
1596
|
+
const maxVal = this.channels.downsampleMax[channel];
|
|
1597
|
+
const range = maxVal - minVal;
|
|
1598
|
+
|
|
1599
|
+
for (let i = 0; i < uint16View.length; i++) {
|
|
1600
|
+
// Clamp to min/max range, then scale to 0-255
|
|
1601
|
+
const val = uint16View[i];
|
|
1602
|
+
const clamped = Math.max(minVal, Math.min(maxVal, val));
|
|
1603
|
+
uint8Chunk[i] = ((clamped - minVal) * 255 / range) | 0; // Use bitwise OR for fast floor
|
|
1547
1604
|
}
|
|
1548
1605
|
chunk = uint8Chunk;
|
|
1549
1606
|
}
|
|
@@ -1634,6 +1691,11 @@ export class VolumeDataManager {
|
|
|
1634
1691
|
async handleBrickRequests(ptRequests) {
|
|
1635
1692
|
// log.debug('handleBrickRequests');
|
|
1636
1693
|
if (ptRequests.length === 0) return;
|
|
1694
|
+
|
|
1695
|
+
// Track loading progress
|
|
1696
|
+
this.totalBricksRequested = ptRequests.length;
|
|
1697
|
+
this.currentRequestCount = ptRequests.length;
|
|
1698
|
+
|
|
1637
1699
|
// log.debug('ptRequests', ptRequests);
|
|
1638
1700
|
// log.debug('this.BCFull', this.BCFull);
|
|
1639
1701
|
// log.debug('this.BCUnusedIndex', this.BCUnusedIndex);
|
|
@@ -1649,9 +1711,12 @@ export class VolumeDataManager {
|
|
|
1649
1711
|
/* upload each (sequentially or Promise.all if you prefer IO overlap) */
|
|
1650
1712
|
log.debug('handleBrickRequests: starting for loop');
|
|
1651
1713
|
for (let i = 0; i < ptRequests.length; ++i) {
|
|
1652
|
-
log.debug('uploading brick', ptRequests[i], slots[i]);
|
|
1653
1714
|
// eslint-disable-next-line no-await-in-loop
|
|
1654
1715
|
await this._uploadBrick(ptRequests[i], slots[i]);
|
|
1716
|
+
|
|
1717
|
+
// Update progress
|
|
1718
|
+
this.currentRequestCount = ptRequests.length - i - 1;
|
|
1719
|
+
|
|
1655
1720
|
// this.bricksAllocated++;
|
|
1656
1721
|
const rlength = this.bricksEverLoaded.size;
|
|
1657
1722
|
this.bricksEverLoaded.add(`${ptRequests[i].x},${ptRequests[i].y},${ptRequests[i].z}`);
|
|
@@ -1662,11 +1727,16 @@ export class VolumeDataManager {
|
|
|
1662
1727
|
if (this.needsBailout) {
|
|
1663
1728
|
log.debug('Bailing out of handleBrickRequests early due to needsBailout flag');
|
|
1664
1729
|
this.needsBailout = false; // Reset the flag
|
|
1730
|
+
this.currentRequestCount = 0; // Reset progress
|
|
1665
1731
|
break; // Exit the loop early
|
|
1666
1732
|
|
|
1667
1733
|
// TODO(mark): do something with the allocated slots that were not used?
|
|
1668
1734
|
}
|
|
1669
1735
|
}
|
|
1736
|
+
|
|
1737
|
+
// Reset progress when complete
|
|
1738
|
+
this.currentRequestCount = 0;
|
|
1739
|
+
|
|
1670
1740
|
// log.debug('this.bricksAllocated', this.bricksAllocated);
|
|
1671
1741
|
log.debug('this.bricksEverLoaded', this.bricksEverLoaded);
|
|
1672
1742
|
// log.debug('uploaded bricks');
|
|
@@ -81,9 +81,6 @@ export class VolumeRenderManager {
|
|
|
81
81
|
* @returns {Object} Extracted rendering settings
|
|
82
82
|
*/
|
|
83
83
|
extractRenderingSettingsFromProps(props) {
|
|
84
|
-
logWithColor('Extracting rendering settings from props');
|
|
85
|
-
// log.debug('props', props);
|
|
86
|
-
// log.debug('props.imageChannelCoordination', props.imageChannelCoordination[0]['A']);
|
|
87
84
|
const {
|
|
88
85
|
images = {},
|
|
89
86
|
imageLayerScopes = [],
|
|
@@ -95,6 +92,7 @@ export class VolumeRenderManager {
|
|
|
95
92
|
|
|
96
93
|
const layerScope = imageLayerScopes[0];
|
|
97
94
|
if (!layerScope) {
|
|
95
|
+
logWithColor('Extracting rendering settings from props - no layer scope');
|
|
98
96
|
return {
|
|
99
97
|
valid: false,
|
|
100
98
|
};
|
|
@@ -106,6 +104,7 @@ export class VolumeRenderManager {
|
|
|
106
104
|
|
|
107
105
|
const data = images[layerScope]?.image?.instance?.getData();
|
|
108
106
|
if (!data) {
|
|
107
|
+
logWithColor('Extracting rendering settings from props - no image data');
|
|
109
108
|
return {
|
|
110
109
|
valid: false,
|
|
111
110
|
};
|
|
@@ -121,6 +120,7 @@ export class VolumeRenderManager {
|
|
|
121
120
|
// TODO(mark): the defaults should be set based on the image dtype.
|
|
122
121
|
// TODO(mark): wait for all channel IQR ranges to be computed before initializing,
|
|
123
122
|
// as we do not want this.maxRange to be incorrect.
|
|
123
|
+
logWithColor('Extracting rendering settings from props - no channel window set');
|
|
124
124
|
return {
|
|
125
125
|
valid: false,
|
|
126
126
|
};
|
|
@@ -185,10 +185,12 @@ export class VolumeRenderManager {
|
|
|
185
185
|
channelCoordination[cScope][CoordinationType.SPATIAL_MAX_RESOLUTION]
|
|
186
186
|
));
|
|
187
187
|
|
|
188
|
+
|
|
188
189
|
// Get slice planes
|
|
189
190
|
let xSlice = layerCoordination[CoordinationType.SPATIAL_SLICE_X];
|
|
190
191
|
let ySlice = layerCoordination[CoordinationType.SPATIAL_SLICE_Y];
|
|
191
192
|
let zSlice = layerCoordination[CoordinationType.SPATIAL_SLICE_Z];
|
|
193
|
+
const lodFactor = layerCoordination[CoordinationType.SPATIAL_LOD_FACTOR] ?? 1.0;
|
|
192
194
|
|
|
193
195
|
xSlice = xSlice !== null ? xSlice : new Vector2(-1, 100000);
|
|
194
196
|
ySlice = ySlice !== null ? ySlice : new Vector2(-1, 100000);
|
|
@@ -196,6 +198,8 @@ export class VolumeRenderManager {
|
|
|
196
198
|
|
|
197
199
|
const allChannels = images[layerScope].image.loaders[0].channels;
|
|
198
200
|
|
|
201
|
+
logWithColor('Extracting rendering settings from props - success');
|
|
202
|
+
|
|
199
203
|
return {
|
|
200
204
|
valid: true,
|
|
201
205
|
channelsVisible,
|
|
@@ -210,6 +214,7 @@ export class VolumeRenderManager {
|
|
|
210
214
|
xSlice,
|
|
211
215
|
ySlice,
|
|
212
216
|
zSlice,
|
|
217
|
+
lodFactor,
|
|
213
218
|
};
|
|
214
219
|
}
|
|
215
220
|
|
|
@@ -219,9 +224,10 @@ export class VolumeRenderManager {
|
|
|
219
224
|
* @returns {boolean} True if settings were successfully updated
|
|
220
225
|
*/
|
|
221
226
|
updateFromProps(props) {
|
|
222
|
-
logWithColor('Updating from props');
|
|
223
227
|
const settings = this.extractRenderingSettingsFromProps(props);
|
|
224
228
|
if (!settings.valid) {
|
|
229
|
+
logWithColor('Updating from props - invalid settings');
|
|
230
|
+
|
|
225
231
|
return false;
|
|
226
232
|
}
|
|
227
233
|
|
|
@@ -236,7 +242,13 @@ export class VolumeRenderManager {
|
|
|
236
242
|
this.xSlice = settings.xSlice;
|
|
237
243
|
this.ySlice = settings.ySlice;
|
|
238
244
|
this.zSlice = settings.zSlice;
|
|
245
|
+
this.uniforms.lodFactor.value = settings.lodFactor;
|
|
239
246
|
|
|
247
|
+
logWithColor(`lodFactor ${settings.lodFactor}`);
|
|
248
|
+
|
|
249
|
+
this.shader.uniforms.lodFactor.value = settings.lodFactor;
|
|
250
|
+
|
|
251
|
+
logWithColor('Updating from props - success');
|
|
240
252
|
return true;
|
|
241
253
|
}
|
|
242
254
|
|
|
@@ -532,7 +544,6 @@ export class VolumeRenderManager {
|
|
|
532
544
|
this.uniforms.channelMapping.value = channelMapping;
|
|
533
545
|
}
|
|
534
546
|
|
|
535
|
-
|
|
536
547
|
// Only called on initialization of the
|
|
537
548
|
// VolumeDataManager and VolumeRenderManager for a particular image.
|
|
538
549
|
setZarrUniforms(
|
package/src/VolumeShader.js
CHANGED
|
@@ -18,6 +18,7 @@ export const VolumeShader = {
|
|
|
18
18
|
zClip: { value: new Vector2(0, 1000000.0) },
|
|
19
19
|
u_window_size: { value: new Vector2(1, 1) },
|
|
20
20
|
u_vol_scale: { value: new Vector3(1, 1, 1) },
|
|
21
|
+
// Set renderstyle to 3 or 4 for debugging modes
|
|
21
22
|
u_renderstyle: { value: 2 },
|
|
22
23
|
brickCacheTex: { type: 'sampler3D', value: null },
|
|
23
24
|
pageTableTex: { type: 'usampler3D', value: null },
|
package/src/VolumeView.js
CHANGED
|
@@ -6,14 +6,15 @@
|
|
|
6
6
|
* VolumeView.js
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React, { useRef, useState, useEffect,
|
|
9
|
+
import React, { useRef, useState, useEffect, useMemo } from 'react';
|
|
10
10
|
import { useThree, useFrame } from '@react-three/fiber';
|
|
11
11
|
import { OrbitControls } from '@react-three/drei';
|
|
12
12
|
import * as THREE from 'three';
|
|
13
13
|
import { WebGLMultipleRenderTargets } from 'three';
|
|
14
14
|
import { log, atLeastLogLevel, LogLevel } from '@vitessce/globals';
|
|
15
|
+
import { useEventCallback } from '@vitessce/styles';
|
|
15
16
|
|
|
16
|
-
import { VolumeDataManager } from './VolumeDataManager.js';
|
|
17
|
+
import { VolumeDataManager, INIT_STATUS } from './VolumeDataManager.js';
|
|
17
18
|
import { VolumeRenderManager } from './VolumeRenderManager.js';
|
|
18
19
|
|
|
19
20
|
import { gaussianVertexShader } from './shaders/GaussianVertexShader.js';
|
|
@@ -35,14 +36,14 @@ function logWithColor(msg) {
|
|
|
35
36
|
|
|
36
37
|
// Functions called with useFrame
|
|
37
38
|
function performGeometryPass(_gl, _camera, _scene, { mrtRef }) {
|
|
38
|
-
// logWithColor('performGeometryPass');
|
|
39
|
+
// logWithColor('performGeometryPass', 'blue');
|
|
39
40
|
_gl.setRenderTarget(mrtRef.current);
|
|
40
41
|
_gl.clear(true, true, true);
|
|
41
42
|
_gl.render(_scene, _camera);
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
function performBlitPass(_gl, { screenSceneRef, screenCameraRef }) {
|
|
45
|
-
// logWithColor('performBlitPass');
|
|
46
|
+
// logWithColor('performBlitPass', 'red');
|
|
46
47
|
_gl.setRenderTarget(null);
|
|
47
48
|
_gl.clear(true, true, true);
|
|
48
49
|
_gl.render(screenSceneRef.current, screenCameraRef.current);
|
|
@@ -54,7 +55,8 @@ function handleRequests(_gl, { frameRef, dataManager, mrtRef, bufRequest, bufUsa
|
|
|
54
55
|
const ctx = _gl.getContext();
|
|
55
56
|
const f = frameRef.current;
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
// Only allow automatic restart if not manually stopped by user
|
|
59
|
+
if (dataManager.noNewRequests === true && !dataManager.manuallyStopped && f % 100 === 0 && f < 500) {
|
|
58
60
|
// Every 100th frame of the first 500 frames.
|
|
59
61
|
// eslint-disable-next-line no-param-reassign
|
|
60
62
|
dataManager.noNewRequests = false;
|
|
@@ -114,8 +116,11 @@ function handleAdaptiveQuality(clock, params) {
|
|
|
114
116
|
// While the user is interacting, we want to ensure that the next frame's handleRequests call
|
|
115
117
|
// will trigger the processUsageData call, and subsequently the processRequestData call
|
|
116
118
|
// (since processUsageData will set triggerRequest to true its conclusion).
|
|
117
|
-
|
|
118
|
-
dataManager.
|
|
119
|
+
// But only if not manually stopped by the user
|
|
120
|
+
if (!dataManager.manuallyStopped) {
|
|
121
|
+
dataManager.noNewRequests = false;
|
|
122
|
+
dataManager.triggerUsage = true;
|
|
123
|
+
}
|
|
119
124
|
return;
|
|
120
125
|
}
|
|
121
126
|
if (spatialRenderingModeChanging) return;
|
|
@@ -186,6 +191,7 @@ export function VolumeView(props) {
|
|
|
186
191
|
// onInitComplete,
|
|
187
192
|
spatialRenderingMode,
|
|
188
193
|
spatialRenderingModeChanging,
|
|
194
|
+
onVolumeLoadingUpdate,
|
|
189
195
|
} = props;
|
|
190
196
|
const {
|
|
191
197
|
gl,
|
|
@@ -211,7 +217,7 @@ export function VolumeView(props) {
|
|
|
211
217
|
});
|
|
212
218
|
// const [lastRes, setLastRes] = useState(null);
|
|
213
219
|
// const [lastChannels, setLastChannels] = useState([]);
|
|
214
|
-
const
|
|
220
|
+
const is3D = spatialRenderingMode === '3D';
|
|
215
221
|
// const [loading, setLoading] = useState(false);
|
|
216
222
|
|
|
217
223
|
// Add new refs for screen quad setup
|
|
@@ -219,9 +225,14 @@ export function VolumeView(props) {
|
|
|
219
225
|
const screenCameraRef = useRef(null);
|
|
220
226
|
const screenQuadRef = useRef(null);
|
|
221
227
|
|
|
222
|
-
|
|
228
|
+
// The `_setIsInteracting` function is wrapped by the `setIsInteracting` useEventCallback below
|
|
229
|
+
// This allows us to apply the effects related to interaction state synchronously,
|
|
230
|
+
// rather than relying waiting for a React state update with `useEffect`.
|
|
231
|
+
// Updates to `isInteracting` should be done via `setIsInteracting`.
|
|
232
|
+
const [isInteracting, _setIsInteracting] = useState(false);
|
|
223
233
|
const interactionTimeoutRef = useRef(null);
|
|
224
234
|
|
|
235
|
+
|
|
225
236
|
// Track interaction-triggering props
|
|
226
237
|
// const prevInteractionChannels = useRef();
|
|
227
238
|
|
|
@@ -311,8 +322,10 @@ export function VolumeView(props) {
|
|
|
311
322
|
const firstImageChannelScope = imageChannelScopesByLayer?.[firstImageLayerScope];
|
|
312
323
|
const firstImageLayerChannelCoordination = imageChannelCoordination?.[0]?.[firstImageLayerScope];
|
|
313
324
|
|
|
325
|
+
|
|
314
326
|
useEffect(() => {
|
|
315
327
|
logWithColor('useEffect INIT');
|
|
328
|
+
let hasRerun = false;
|
|
316
329
|
|
|
317
330
|
if (!dataManager || !renderManager) {
|
|
318
331
|
log.debug('dataManager or renderManager not initialized yet');
|
|
@@ -329,12 +342,20 @@ export function VolumeView(props) {
|
|
|
329
342
|
return;
|
|
330
343
|
}
|
|
331
344
|
|
|
332
|
-
|
|
333
|
-
|
|
345
|
+
const initializeDataManager = async () => {
|
|
346
|
+
if (dataManager.initStatus === INIT_STATUS.COMPLETE) {
|
|
347
|
+
log.debug('dataManager already initialized, skipping');
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
334
350
|
// TODO(mark): separate the initialization which depends on gl, from the initialization which depends on images, from the dm.init(firstImageLayer)
|
|
335
351
|
dataManager.initImages(images, imageLayerScopes);
|
|
336
352
|
// TODO: generalize to more than one image layer.
|
|
337
353
|
await dataManager.init(firstImageLayerChannelCoordination); // device limits, zarr meta
|
|
354
|
+
if (hasRerun) {
|
|
355
|
+
log.debug('Initialization useEffect has rerun, aborting remaining initialization');
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
log.debug('dataManager initialized');
|
|
338
359
|
|
|
339
360
|
// log.debug('dm.physicalScale', dataManager.physicalScale);
|
|
340
361
|
|
|
@@ -347,7 +368,16 @@ export function VolumeView(props) {
|
|
|
347
368
|
// if (onInitComplete) {
|
|
348
369
|
// onInitComplete({ zarrStoreInfo: dm.zarrStore, deviceLimits: dm.deviceLimits });
|
|
349
370
|
// }
|
|
350
|
-
|
|
371
|
+
|
|
372
|
+
invalidate();
|
|
373
|
+
};
|
|
374
|
+
initializeDataManager();
|
|
375
|
+
|
|
376
|
+
// avoid setting state on unmounted component
|
|
377
|
+
// eslint-disable-next-line consistent-return
|
|
378
|
+
return () => {
|
|
379
|
+
hasRerun = true;
|
|
380
|
+
};
|
|
351
381
|
|
|
352
382
|
// The data manager does not have a clearCache method, should it?
|
|
353
383
|
// return () => {
|
|
@@ -355,9 +385,7 @@ export function VolumeView(props) {
|
|
|
355
385
|
// dataManager?.clearCache?.();
|
|
356
386
|
// };
|
|
357
387
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
358
|
-
}, [dataManager, renderManager,
|
|
359
|
-
images, imageLayerScopes,
|
|
360
|
-
]);
|
|
388
|
+
}, [dataManager, renderManager, images, imageLayerScopes]);
|
|
361
389
|
|
|
362
390
|
/*
|
|
363
391
|
useEffect(() => {
|
|
@@ -396,11 +424,10 @@ export function VolumeView(props) {
|
|
|
396
424
|
|
|
397
425
|
// TODO(mark): convert to a useMemo?
|
|
398
426
|
useEffect(() => {
|
|
399
|
-
logWithColor('useEffect spatialRenderingMode');
|
|
400
427
|
const on3D = spatialRenderingMode === '3D';
|
|
401
|
-
setIs3D(on3D);
|
|
402
428
|
|
|
403
429
|
if (on3D && dataManager && renderManager) {
|
|
430
|
+
logWithColor('useEffect spatialRenderingMode');
|
|
404
431
|
// Direct call, no callbacks needed
|
|
405
432
|
const propsForRenderManager = {
|
|
406
433
|
images,
|
|
@@ -444,44 +471,8 @@ export function VolumeView(props) {
|
|
|
444
471
|
}
|
|
445
472
|
}
|
|
446
473
|
}
|
|
447
|
-
}, [dataManager, renderManager, images, imageLayerScopes, imageLayerCoordination,
|
|
448
|
-
imageChannelScopesByLayer, imageChannelCoordination, spatialRenderingMode]);
|
|
474
|
+
}, [dataManager, renderManager, images, imageLayerScopes, imageLayerCoordination, imageChannelScopesByLayer, imageChannelCoordination, spatialRenderingMode]);
|
|
449
475
|
|
|
450
|
-
// TODO(mark): can this logic be moved into useFrame?
|
|
451
|
-
/*
|
|
452
|
-
useEffect(() => {
|
|
453
|
-
logWithColor('useEffect stillRef');
|
|
454
|
-
// log.debug('useEffect stillRef');
|
|
455
|
-
// log.debug('stillRef', stillRef.current);
|
|
456
|
-
if (!screenQuadRef.current) {
|
|
457
|
-
log.debug('no screen quad yet');
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
if (!stillRef.current) {
|
|
461
|
-
// log.debug('stillRef is false');
|
|
462
|
-
screenQuadRef.current.material.uniforms.gaussian.value = 7;
|
|
463
|
-
} else {
|
|
464
|
-
//log.debug('stillRef is true');
|
|
465
|
-
screenQuadRef.current.material.uniforms.gaussian.value = 0;
|
|
466
|
-
}
|
|
467
|
-
}, [stillRef.current]);
|
|
468
|
-
*/
|
|
469
|
-
|
|
470
|
-
// TODO(mark): can this logic be moved into useFrame?
|
|
471
|
-
// But need to be careful, since currently, it only runs upon change of isInteracting,
|
|
472
|
-
// and we do not want to execute more `invalidate` calls than necessary/currently.
|
|
473
|
-
useEffect(() => {
|
|
474
|
-
logWithColor('useEffect isInteracting');
|
|
475
|
-
if (isInteracting) {
|
|
476
|
-
const meshRefUniforms = meshRef.current?.material?.uniforms;
|
|
477
|
-
if (meshRefUniforms) {
|
|
478
|
-
meshRefUniforms.renderRes.value = dataManager.PT.lowestDataRes;
|
|
479
|
-
}
|
|
480
|
-
// setRenderSpeed(dataManager.PT.lowestDataRes);
|
|
481
|
-
stillRef.current = false;
|
|
482
|
-
invalidate();
|
|
483
|
-
}
|
|
484
|
-
}, [invalidate, isInteracting]);
|
|
485
476
|
|
|
486
477
|
// Execute code on every rendered frame.
|
|
487
478
|
// A numerical renderPriority value will cause React Three Fiber
|
|
@@ -493,11 +484,15 @@ export function VolumeView(props) {
|
|
|
493
484
|
useFrame((state, delta, xrFrame) => {
|
|
494
485
|
// NOTE: Do not update React state inside useFrame.
|
|
495
486
|
// Reference: https://r3f.docs.pmnd.rs/advanced/pitfalls#%E2%9D%8C-setstate-in-useframe-is-bad
|
|
496
|
-
if (!mrtRef.current || !dataManager || !renderManager)
|
|
487
|
+
if (!mrtRef.current || !dataManager || !renderManager) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
497
490
|
|
|
498
491
|
// Don't render to MRT if shader is not ready - this prevents WebGL warnings
|
|
499
492
|
// about missing fragment shader outputs for multiple render targets
|
|
500
|
-
if (!renderState.shader)
|
|
493
|
+
if (!renderState.shader) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
501
496
|
|
|
502
497
|
// Receive the same state as the useThree hook.
|
|
503
498
|
const {
|
|
@@ -510,6 +505,7 @@ export function VolumeView(props) {
|
|
|
510
505
|
if (!stillRef.current) {
|
|
511
506
|
performGeometryPass(frameGl, frameCamera, frameScene, { mrtRef });
|
|
512
507
|
}
|
|
508
|
+
|
|
513
509
|
performBlitPass(frameGl, { screenSceneRef, screenCameraRef });
|
|
514
510
|
handleRequests(frameGl, { frameRef, dataManager, mrtRef, bufRequest, bufUsage });
|
|
515
511
|
handleAdaptiveQuality(clock, {
|
|
@@ -587,17 +583,37 @@ export function VolumeView(props) {
|
|
|
587
583
|
}, [mainOrbitControlsRef.current, setIsInteracting]);
|
|
588
584
|
*/
|
|
589
585
|
|
|
590
|
-
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Handle interaction state changes in event callbacks.
|
|
589
|
+
* This avoids the need for a useEffect hook which
|
|
590
|
+
* would run on every render frame.
|
|
591
|
+
*/
|
|
592
|
+
const setIsInteracting = useEventCallback((interacting) => {
|
|
593
|
+
logWithColor('invalidateOnInteraction callback');
|
|
594
|
+
_setIsInteracting(interacting);
|
|
595
|
+
if (interacting) {
|
|
596
|
+
const meshRefUniforms = meshRef.current?.material?.uniforms;
|
|
597
|
+
if (meshRefUniforms) {
|
|
598
|
+
meshRefUniforms.renderRes.value = dataManager.PT.lowestDataRes;
|
|
599
|
+
}
|
|
600
|
+
// setRenderSpeed(dataManager.PT.lowestDataRes);
|
|
601
|
+
stillRef.current = false;
|
|
602
|
+
invalidate();
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
const onOrbitControlsStart = useEventCallback((e) => {
|
|
591
607
|
setIsInteracting(true);
|
|
592
|
-
}
|
|
608
|
+
});
|
|
593
609
|
|
|
594
610
|
|
|
595
|
-
const onOrbitControlsEnd =
|
|
611
|
+
const onOrbitControlsEnd = useEventCallback((e) => {
|
|
596
612
|
clearTimeout(interactionTimeoutRef.current);
|
|
597
613
|
interactionTimeoutRef.current = setTimeout(() => {
|
|
598
614
|
setIsInteracting(false);
|
|
599
615
|
}, 300);
|
|
600
|
-
}
|
|
616
|
+
});
|
|
601
617
|
|
|
602
618
|
// Set isInteracting while the user is sliding the channel slider
|
|
603
619
|
// for smooth updates.
|
|
@@ -613,7 +629,45 @@ export function VolumeView(props) {
|
|
|
613
629
|
interactionTimeoutRef.current = setTimeout(() => {
|
|
614
630
|
setIsInteracting(false);
|
|
615
631
|
}, 300);
|
|
616
|
-
}, [firstImageLayerChannelCoordination]);
|
|
632
|
+
}, [dataManager, firstImageLayerChannelCoordination, renderManager, setIsInteracting]);
|
|
633
|
+
|
|
634
|
+
const stopLoading = useEventCallback(() => {
|
|
635
|
+
if (dataManager) {
|
|
636
|
+
dataManager.stopLoading();
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
const restartLoading = useEventCallback(() => {
|
|
641
|
+
if (dataManager) {
|
|
642
|
+
dataManager.restartLoading();
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const getLoadingProgress = useEventCallback(() => {
|
|
647
|
+
if (dataManager) {
|
|
648
|
+
return dataManager.getLoadingProgress();
|
|
649
|
+
}
|
|
650
|
+
return null;
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// Notify parent component about loading updates
|
|
654
|
+
useEffect(() => {
|
|
655
|
+
if (!onVolumeLoadingUpdate) return undefined;
|
|
656
|
+
|
|
657
|
+
const intervalId = setInterval(() => {
|
|
658
|
+
const loadingProgress = getLoadingProgress();
|
|
659
|
+
onVolumeLoadingUpdate({
|
|
660
|
+
loadingProgress,
|
|
661
|
+
stillRef,
|
|
662
|
+
onStopLoading: stopLoading,
|
|
663
|
+
onRestartLoading: restartLoading,
|
|
664
|
+
});
|
|
665
|
+
}, 1000); // Update every 1000ms (1s)
|
|
666
|
+
|
|
667
|
+
return () => clearInterval(intervalId);
|
|
668
|
+
// `useEventCallback` ensures stable references
|
|
669
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
670
|
+
}, [onVolumeLoadingUpdate, stillRef]);
|
|
617
671
|
|
|
618
672
|
// Render nothing during initialization.
|
|
619
673
|
if (!is3D || !dataManager || !renderManager) return null;
|