@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.
@@ -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
- // log.debug('chunk', chunk);
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
- // TODO(mark): to reduce memory usage, create the Uint8Array as a view of the Uint16Array
1542
- // and use bitwise operations. Then scale the values.
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
- for (let i = 0; i < chunk.length; i++) {
1545
- // Scale from 0-65535 to 0-255
1546
- uint8Chunk[i] = Math.floor((chunk[i] - this.channels.downsampleMin[channel]) / (this.channels.downsampleMax[channel] - this.channels.downsampleMin[channel]) * 255);
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(
@@ -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, useCallback, useMemo } from 'react';
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
- if (dataManager.noNewRequests === true && f % 100 === 0 && f < 500) {
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
- dataManager.noNewRequests = false;
118
- dataManager.triggerUsage = true;
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 [is3D, setIs3D] = useState(false);
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
- const [isInteracting, setIsInteracting] = useState(false);
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
- // TODO(mark): prevent dataManager.init from being called more than once.
333
- (async () => {
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) return;
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) return;
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
- const onOrbitControlsStart = useCallback((e) => {
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 = useCallback((e) => {
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;