@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.
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import React from 'react';
2
+ import React, { Suspense } from 'react';
3
3
  // Note: need to be careful about versions of RTF, Three, React, and React DOM in package.json
4
4
  // to avoid multiple copies of RTF, since the Canvas depends on a React Context.
5
5
  // Otherwise you may see errors such as "useThree can only be used inside the Canvas component!".
@@ -41,5 +41,5 @@ export function SpatialWrapper(props) {
41
41
  logarithmicDepthBuffer: false,
42
42
  preserveDrawingBuffer: false,
43
43
  autoClear: false,
44
- }, children: _jsx(VolumeView, { ...props }) }));
44
+ }, children: _jsx(Suspense, { fallback: "Initializing Volume View...", children: _jsx(VolumeView, { ...props }) }) }));
45
45
  }
@@ -35,6 +35,12 @@ export function _requestBufferToRequestObjects(buffer: any, k: any): {
35
35
  }[];
36
36
  origRequestCount: number;
37
37
  };
38
+ export namespace INIT_STATUS {
39
+ let NOT_STARTED: string;
40
+ let IN_PROGRESS: string;
41
+ let COMPLETE: string;
42
+ let FAILED: string;
43
+ }
38
44
  export class VolumeDataManager {
39
45
  constructor(glParam: any);
40
46
  gl: any;
@@ -93,6 +99,7 @@ export class VolumeDataManager {
93
99
  timeStamp: number;
94
100
  k: number;
95
101
  noNewRequests: boolean;
102
+ manuallyStopped: boolean;
96
103
  needsBailout: boolean;
97
104
  initStatus: string;
98
105
  initError: any;
@@ -107,6 +114,21 @@ export class VolumeDataManager {
107
114
  downsampleMin: never[];
108
115
  downsampleMax: never[];
109
116
  } | null;
117
+ currentRequestCount: number;
118
+ totalBricksRequested: number;
119
+ /**
120
+ * Get loading progress information
121
+ * @returns {Object} Loading progress with bricksLoaded, totalBricks, isLoading, and percentage
122
+ */
123
+ getLoadingProgress(): Object;
124
+ /**
125
+ * Manually stop loading and render at highest resolution
126
+ */
127
+ stopLoading(): void;
128
+ /**
129
+ * Manually restart loading
130
+ */
131
+ restartLoading(): void;
110
132
  /**
111
133
  * Handle WebGL context loss
112
134
  */
@@ -1 +1 @@
1
- {"version":3,"file":"VolumeDataManager.d.ts","sourceRoot":"","sources":["../src/VolumeDataManager.js"],"names":[],"mappings":"AA8DA,yEAaC;AACD,8EAeC;AAED;;;;GAIG;AACH,kDAHW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,8BAC1B,MAAM;;;;;;;;;;;;;EAyHhB;AAMD,kFAaC;AAqBD;;;;;;EA2DC;AAED;;;;;;;EA0BC;AAID;IACE,0BA2KC;IA5JG,QAAyB;IAa3B,cAAwB;IAyBxB,sBAA+B;IAC/B,wBAA2B;IAC3B,iCAAmC;IAenC;;;;;MAKC;IAED;;;;;;;;;;;;MAcC;IAED,8BAAmB;IACnB,8BAAmB;IAEnB;;;;;;MAMC;IAED;;;;;;;;;MAiBC;IAQD,2BAAiC;IAYjC,gBAAmB;IAGnB,oBAAuD;IACvD,gBAAwD;IACxD,gBAAmB;IACnB,sBAAsB;IAGtB,aAAmD;IAGnD,gBAAkB;IAElB,sBAAwB;IACxB,wBAA2B;IAC3B,kBAAkB;IAClB,UAAW;IACX,uBAA0B;IAE1B,sBAAyB;IAGzB,mBAAyC;IACzC,eAAqB;IAGrB;;;;;;;;;;aAA8B;IAKhC;;OAEG;IACH,qCAeC;IAED;;OAEG;IACH,yCAqCC;IAED;;OAEG;IACH,qBAMC;IAED;;OAEG;IACH,uCAIC;IAED,qDAIC;IAFC,YAAoB;IACpB,sBAAwC;IAG1C;;;;;OAKG;IACH,mBAFa,OAAO,CAAC,MAAM,CAAC,CA8S3B;IAjQG,kBAAmD;IAmQvD;;;;;;;;OAQG;IACH,mBAqBC;IASD,6BAKC;IAED,wCAqHC;IAED;;;;;OAKG;IAqBH;;;OAGG;IACH,kCAQC;IAED;;;OAGG;IACH,uBAFa,MAAM,CAUlB;IAED,+BAQC;IAED,kCASC;IAED,gCASC;IAED;;;;;;;;;OASG;IACH,iBARW,MAAM,iBACN,MAAM,iBACN,MAAM,KACN,MAAM,KACN,MAAM,cACN,MAAM,GACJ,OAAO,CAAC,UAAU,CAAC,CAiC/B;IAED,+CA0BC;IAGD,6CA0DC;IAGD,gCAcC;IAED,yCAmDC;IAGD,+DAqBC;IAKD;;;;OAIG;IACH,oBAHW,MAAM,GACJ;QAAE,OAAO,MAAC;QAAC,CAAC,MAAC;QAAC,CAAC,MAAC;QAAC,CAAC,MAAA;KAAE,EAAE,CAyClC;IAKD,uDAqLC;IAKD,oDA2CC;IAKD,kBAWC;CACF;8BAhpDM,OAAO"}
1
+ {"version":3,"file":"VolumeDataManager.d.ts","sourceRoot":"","sources":["../src/VolumeDataManager.js"],"names":[],"mappings":"AA8DA,yEAaC;AACD,8EAeC;AAED;;;;GAIG;AACH,kDAHW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,8BAC1B,MAAM;;;;;;;;;;;;;EAyHhB;AAMD,kFAaC;AAqBD;;;;;;EA2DC;AAED;;;;;;;EA0BC;;;;;;;AAID;IACE,0BAgLC;IAjKG,QAAyB;IAa3B,cAAwB;IAyBxB,sBAA+B;IAC/B,wBAA2B;IAC3B,iCAAmC;IAenC;;;;;MAKC;IAED;;;;;;;;;;;;MAcC;IAED,8BAAmB;IACnB,8BAAmB;IAEnB;;;;;;MAMC;IAED;;;;;;;;;MAiBC;IAQD,2BAAiC;IAYjC,gBAAmB;IAGnB,oBAAuD;IACvD,gBAAwD;IACxD,gBAAmB;IACnB,sBAAsB;IAGtB,aAAmD;IAGnD,gBAAkB;IAElB,sBAAwB;IACxB,wBAA2B;IAC3B,kBAAkB;IAClB,UAAW;IACX,uBAA0B;IAC1B,yBAA4B;IAE5B,sBAAyB;IAGzB,mBAAyC;IACzC,eAAqB;IAGrB;;;;;;;;;;aAA8B;IAG9B,4BAA4B;IAC5B,6BAA6B;IAK/B;;;OAGG;IACH,sBAFa,MAAM,CAiBlB;IAED;;OAEG;IACH,oBAQC;IAED;;OAEG;IACH,uBASC;IAED;;OAEG;IACH,qCAeC;IAED;;OAEG;IACH,yCAqCC;IAED;;OAEG;IACH,qBAMC;IAED;;OAEG;IACH,uCAIC;IAED,qDAIC;IAFC,YAAoB;IACpB,sBAAwC;IAG1C;;;;;OAKG;IACH,mBAFa,OAAO,CAAC,MAAM,CAAC,CA8S3B;IAjQG,kBAAmD;IAmQvD;;;;;;;;OAQG;IACH,mBAkBC;IAGD,6BAMC;IAED,wCAqHC;IAED;;;;;OAKG;IAqBH;;;OAGG;IACH,kCAQC;IAED;;;OAGG;IACH,uBAFa,MAAM,CAUlB;IAED,+BAQC;IAED,kCASC;IAED,gCASC;IAED;;;;;;;;;OASG;IACH,iBARW,MAAM,iBACN,MAAM,iBACN,MAAM,KACN,MAAM,KACN,MAAM,cACN,MAAM,GACJ,OAAO,CAAC,UAAU,CAAC,CA+B/B;IAED,+CAgCC;IAGD,6CA0DC;IAGD,gCAcC;IAED,yCAmDC;IAGD,+DAqBC;IAKD;;;;OAIG;IACH,oBAHW,MAAM,GACJ;QAAE,OAAO,MAAC;QAAC,CAAC,MAAC;QAAC,CAAC,MAAC;QAAC,CAAC,MAAA;KAAE,EAAE,CAyClC;IAKD,uDA6LC;IAKD,oDAwDC;IAKD,kBAWC;CACF;8BAttDM,OAAO"}
@@ -30,7 +30,7 @@ const MAX_RESOLUTION_LEVELS = 10; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
30
30
  const MAX_CHANNELS = 7; // [0, 1, 2, 3, 4, 5, 6]
31
31
  // const manualChannelSelection = 0;
32
32
  // Add a constant for initialization status
33
- const INIT_STATUS = {
33
+ export const INIT_STATUS = {
34
34
  NOT_STARTED: 'not_started',
35
35
  IN_PROGRESS: 'in_progress',
36
36
  COMPLETE: 'complete',
@@ -437,14 +437,62 @@ export class VolumeDataManager {
437
437
  this.timeStamp = 0;
438
438
  this.k = 40;
439
439
  this.noNewRequests = false;
440
+ this.manuallyStopped = false; // Flag to track if user manually stopped loading
440
441
  this.needsBailout = false; // Flag to indicate if we need to bail out of processing requests
441
442
  // Add initialization status
442
443
  this.initStatus = INIT_STATUS.NOT_STARTED;
443
444
  this.initError = null;
444
445
  // Store the last known configuration for context restoration
445
446
  this._lastChannelConfig = null;
447
+ // Track loading progress
448
+ this.currentRequestCount = 0; // Number of requests currently being processed
449
+ this.totalBricksRequested = 0; // Total number of bricks requested in current batch
446
450
  logWithColor('VolumeDataManager constructor complete');
447
451
  }
452
+ /**
453
+ * Get loading progress information
454
+ * @returns {Object} Loading progress with bricksLoaded, totalBricks, isLoading, and percentage
455
+ */
456
+ getLoadingProgress() {
457
+ const bricksLoaded = this.bricksEverLoaded.size;
458
+ const isLoading = !this.noNewRequests && this.currentRequestCount > 0;
459
+ const percentage = this.totalBricksRequested > 0
460
+ ? ((this.totalBricksRequested - this.currentRequestCount) / this.totalBricksRequested) * 100
461
+ : 0;
462
+ return {
463
+ bricksLoaded,
464
+ currentRequestCount: this.currentRequestCount,
465
+ totalBricksRequested: this.totalBricksRequested,
466
+ isLoading,
467
+ percentage,
468
+ noNewRequests: this.noNewRequests,
469
+ };
470
+ }
471
+ /**
472
+ * Manually stop loading and render at highest resolution
473
+ */
474
+ stopLoading() {
475
+ log.debug('Manually stopping data loading');
476
+ this.noNewRequests = true;
477
+ this.manuallyStopped = true;
478
+ this.needsBailout = true;
479
+ // Reset progress counters to reflect stopped state
480
+ this.currentRequestCount = 0;
481
+ this.totalBricksRequested = 0;
482
+ }
483
+ /**
484
+ * Manually restart loading
485
+ */
486
+ restartLoading() {
487
+ log.debug('Manually restarting data loading');
488
+ this.noNewRequests = false;
489
+ this.manuallyStopped = false;
490
+ this.needsBailout = false;
491
+ this.triggerUsage = true;
492
+ // Reset progress counters
493
+ this.currentRequestCount = 0;
494
+ this.totalBricksRequested = 0;
495
+ }
448
496
  /**
449
497
  * Handle WebGL context loss
450
498
  */
@@ -803,20 +851,13 @@ export class VolumeDataManager {
803
851
  this.PT = PT;
804
852
  this.ptTHREE = ptTHREE;
805
853
  this.bcTHREE = bcTHREE;
806
- // log.debug('gl', this.gl);
807
- // log.debug('renderer', this.renderer);
808
854
  logWithColor('initMRMCPT() COMPLETE');
809
855
  }
810
- /*
811
- testTexture() {
812
- log.debug('testTexture pt', this.ptTHREE);
813
- log.debug('testTexture bc', this.bcTHREE);
814
- }
815
- */
816
856
  async initTexture() {
817
857
  const requests = [
818
858
  { x: 0, y: 0, z: 1 },
819
859
  ];
860
+ logWithColor('initTexture - loading first brick');
820
861
  await this.handleBrickRequests(requests);
821
862
  }
822
863
  updateChannels(channelProps) {
@@ -1015,7 +1056,7 @@ export class VolumeDataManager {
1015
1056
  if (!this.zarrStore || !this.zarrStore.arrays[resolution]) {
1016
1057
  throw new Error('Zarr store or resolution not initialized');
1017
1058
  }
1018
- // log.debug('loadZarrChunk', t, c, z, y, x, resolution);
1059
+ log.debug('loadZarrChunk', { t, c, z, y, x, resolution });
1019
1060
  const array = this.zarrStore.arrays[resolution];
1020
1061
  // TODO: Use zarrita's get(arr, slice) method (rather than arr.getChunk),
1021
1062
  // which will enable reading from arrays with other chunk shapes
@@ -1024,7 +1065,6 @@ export class VolumeDataManager {
1024
1065
  // to each "assumed 32x32x32 chunk key".
1025
1066
  // Reference: https://zarrita.dev/slicing.html
1026
1067
  const chunkEntry = await array.getChunk([t, c, z, y, x]);
1027
- // log.debug('chunkEntry', chunkEntry);
1028
1068
  if (!chunkEntry) {
1029
1069
  throw new Error(`No chunk found at coordinates [${t},${c},${z},${y},${x}]`);
1030
1070
  }
@@ -1039,6 +1079,11 @@ export class VolumeDataManager {
1039
1079
  log.debug('processRequestData: already busy, skipping');
1040
1080
  return;
1041
1081
  }
1082
+ // Check if loading has been manually stopped
1083
+ if (this.noNewRequests) {
1084
+ log.debug('processRequestData: loading stopped by user, skipping');
1085
+ return;
1086
+ }
1042
1087
  // Check for context loss
1043
1088
  if (this.isContextLost()) {
1044
1089
  log.debug('processRequestData: WebGL context is lost, skipping');
@@ -1287,7 +1332,7 @@ export class VolumeDataManager {
1287
1332
  }
1288
1333
  log.debug('starting to load zarr chunk', { resolution, z, y, x, zarrChannel });
1289
1334
  let chunk = await this.loadZarrChunk(0, zarrChannel, z, y, x, resolution);
1290
- // log.debug('chunk', chunk);
1335
+ log.debug('chunk', chunk);
1291
1336
  // TODO(mark): is there a better way than iterating over all of the chunk values?
1292
1337
  if (chunk instanceof Uint16Array) {
1293
1338
  log.debug('chunk is Uint16Array, converting to Uint8Array');
@@ -1301,12 +1346,18 @@ export class VolumeDataManager {
1301
1346
  log.debug('this.channels.downsampleMin[channel]', this.channels.downsampleMin[channel]);
1302
1347
  log.debug('this.channels.downsampleMax[channel]', this.channels.downsampleMax[channel]);
1303
1348
  }
1304
- // TODO(mark): to reduce memory usage, create the Uint8Array as a view of the Uint16Array
1305
- // and use bitwise operations. Then scale the values.
1349
+ // Use TypedArray view and bitwise operations to reduce memory allocations
1350
+ // Shift right by 8 to convert uint16 to uint8 range
1351
+ const uint16View = new Uint16Array(chunk.buffer);
1306
1352
  const uint8Chunk = new Uint8Array(chunk.length);
1307
- for (let i = 0; i < chunk.length; i++) {
1308
- // Scale from 0-65535 to 0-255
1309
- uint8Chunk[i] = Math.floor((chunk[i] - this.channels.downsampleMin[channel]) / (this.channels.downsampleMax[channel] - this.channels.downsampleMin[channel]) * 255);
1353
+ const minVal = this.channels.downsampleMin[channel];
1354
+ const maxVal = this.channels.downsampleMax[channel];
1355
+ const range = maxVal - minVal;
1356
+ for (let i = 0; i < uint16View.length; i++) {
1357
+ // Clamp to min/max range, then scale to 0-255
1358
+ const val = uint16View[i];
1359
+ const clamped = Math.max(minVal, Math.min(maxVal, val));
1360
+ uint8Chunk[i] = ((clamped - minVal) * 255 / range) | 0; // Use bitwise OR for fast floor
1310
1361
  }
1311
1362
  chunk = uint8Chunk;
1312
1363
  }
@@ -1377,6 +1428,9 @@ export class VolumeDataManager {
1377
1428
  // log.debug('handleBrickRequests');
1378
1429
  if (ptRequests.length === 0)
1379
1430
  return;
1431
+ // Track loading progress
1432
+ this.totalBricksRequested = ptRequests.length;
1433
+ this.currentRequestCount = ptRequests.length;
1380
1434
  // log.debug('ptRequests', ptRequests);
1381
1435
  // log.debug('this.BCFull', this.BCFull);
1382
1436
  // log.debug('this.BCUnusedIndex', this.BCUnusedIndex);
@@ -1389,9 +1443,10 @@ export class VolumeDataManager {
1389
1443
  /* upload each (sequentially or Promise.all if you prefer IO overlap) */
1390
1444
  log.debug('handleBrickRequests: starting for loop');
1391
1445
  for (let i = 0; i < ptRequests.length; ++i) {
1392
- log.debug('uploading brick', ptRequests[i], slots[i]);
1393
1446
  // eslint-disable-next-line no-await-in-loop
1394
1447
  await this._uploadBrick(ptRequests[i], slots[i]);
1448
+ // Update progress
1449
+ this.currentRequestCount = ptRequests.length - i - 1;
1395
1450
  // this.bricksAllocated++;
1396
1451
  const rlength = this.bricksEverLoaded.size;
1397
1452
  this.bricksEverLoaded.add(`${ptRequests[i].x},${ptRequests[i].y},${ptRequests[i].z}`);
@@ -1401,10 +1456,13 @@ export class VolumeDataManager {
1401
1456
  if (this.needsBailout) {
1402
1457
  log.debug('Bailing out of handleBrickRequests early due to needsBailout flag');
1403
1458
  this.needsBailout = false; // Reset the flag
1459
+ this.currentRequestCount = 0; // Reset progress
1404
1460
  break; // Exit the loop early
1405
1461
  // TODO(mark): do something with the allocated slots that were not used?
1406
1462
  }
1407
1463
  }
1464
+ // Reset progress when complete
1465
+ this.currentRequestCount = 0;
1408
1466
  // log.debug('this.bricksAllocated', this.bricksAllocated);
1409
1467
  log.debug('this.bricksEverLoaded', this.bricksEverLoaded);
1410
1468
  // log.debug('uploaded bricks');
@@ -1 +1 @@
1
- {"version":3,"file":"VolumeRenderManager.d.ts","sourceRoot":"","sources":["../src/VolumeRenderManager.js"],"names":[],"mappings":"AA4BA;;;GAGG;AACH;IAII,cAAoB;IACpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAAkB;IAClB,oBAA0B;IAC1B,uBAA6B;IAC7B,kBAAwB;IACxB,kBAAqB;IAGrB,uBAAyB;IACzB,sBAAwB;IACxB,6BAAmC;IACnC,iCAAmC;IACnC,cAAgB;IAChB,sBAAwB;IACxB,0BAA4B;IAC5B,gBAAqC;IACrC,gBAAqC;IACrC,gBAAqC;IACrC,wBAA8B;IAC9B,6BAAmC;IACnC,wBAA8B;IAE9B,iBAAmB;IACnB,qBAAwB;IAS1B;;OAEG;IACH,yBAIC;IAED;;;;OAIG;IACH,yCAHW,MAAM,GACJ,MAAM,CAqIlB;IAED;;;;OAIG;IACH,uBAHW,MAAM,GACJ,OAAO,CAsBnB;IAPC,mBAA2C;IAS7C;;;;OAIG;IACH,yJAHW,MAAM,GACJ,MAAM,GAAC,IAAI,CAsIvB;IAzCG,qBAAyC;IA2C7C;;;;;;;;;;;OAWG;IACH,4CATW,MAAM,eACN,MAAM,2DAGN,MAAM,UACN,OAAO,UACP,OAAO,UACP,OAAO,uDA4HjB;IAED;;;OAGG;IAQH,6CAIC;IAKD,+CAkDC;CACF;wBAxkBM,OAAO"}
1
+ {"version":3,"file":"VolumeRenderManager.d.ts","sourceRoot":"","sources":["../src/VolumeRenderManager.js"],"names":[],"mappings":"AA4BA;;;GAGG;AACH;IAII,cAAoB;IACpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAAkB;IAClB,oBAA0B;IAC1B,uBAA6B;IAC7B,kBAAwB;IACxB,kBAAqB;IAGrB,uBAAyB;IACzB,sBAAwB;IACxB,6BAAmC;IACnC,iCAAmC;IACnC,cAAgB;IAChB,sBAAwB;IACxB,0BAA4B;IAC5B,gBAAqC;IACrC,gBAAqC;IACrC,gBAAqC;IACrC,wBAA8B;IAC9B,6BAAmC;IACnC,wBAA8B;IAE9B,iBAAmB;IACnB,qBAAwB;IAS1B;;OAEG;IACH,yBAIC;IAED;;;;OAIG;IACH,yCAHW,MAAM,GACJ,MAAM,CA0IlB;IAED;;;;OAIG;IACH,uBAHW,MAAM,GACJ,OAAO,CA6BnB;IAbC,mBAA2C;IAe7C;;;;OAIG;IACH,yJAHW,MAAM,GACJ,MAAM,GAAC,IAAI,CAsIvB;IAzCG,qBAAyC;IA2C7C;;;;;;;;;;;OAWG;IACH,4CATW,MAAM,eACN,MAAM,2DAGN,MAAM,UACN,OAAO,UACP,OAAO,UACP,OAAO,uDA4HjB;IAED;;;OAGG;IAQH,6CAIC;IAID,+CAkDC;CACF;wBAnlBM,OAAO"}
@@ -66,12 +66,10 @@ export class VolumeRenderManager {
66
66
  * @returns {Object} Extracted rendering settings
67
67
  */
68
68
  extractRenderingSettingsFromProps(props) {
69
- logWithColor('Extracting rendering settings from props');
70
- // log.debug('props', props);
71
- // log.debug('props.imageChannelCoordination', props.imageChannelCoordination[0]['A']);
72
69
  const { images = {}, imageLayerScopes = [], imageLayerCoordination = [{}], imageChannelScopesByLayer = {}, imageChannelCoordination = [{}], spatialRenderingMode, } = props;
73
70
  const layerScope = imageLayerScopes[0];
74
71
  if (!layerScope) {
72
+ logWithColor('Extracting rendering settings from props - no layer scope');
75
73
  return {
76
74
  valid: false,
77
75
  };
@@ -81,6 +79,7 @@ export class VolumeRenderManager {
81
79
  const channelCoordination = imageChannelCoordination[0][layerScope];
82
80
  const data = images[layerScope]?.image?.instance?.getData();
83
81
  if (!data) {
82
+ logWithColor('Extracting rendering settings from props - no image data');
84
83
  return {
85
84
  valid: false,
86
85
  };
@@ -93,6 +92,7 @@ export class VolumeRenderManager {
93
92
  // TODO(mark): the defaults should be set based on the image dtype.
94
93
  // TODO(mark): wait for all channel IQR ranges to be computed before initializing,
95
94
  // as we do not want this.maxRange to be incorrect.
95
+ logWithColor('Extracting rendering settings from props - no channel window set');
96
96
  return {
97
97
  valid: false,
98
98
  };
@@ -141,10 +141,12 @@ export class VolumeRenderManager {
141
141
  let xSlice = layerCoordination[CoordinationType.SPATIAL_SLICE_X];
142
142
  let ySlice = layerCoordination[CoordinationType.SPATIAL_SLICE_Y];
143
143
  let zSlice = layerCoordination[CoordinationType.SPATIAL_SLICE_Z];
144
+ const lodFactor = layerCoordination[CoordinationType.SPATIAL_LOD_FACTOR] ?? 1.0;
144
145
  xSlice = xSlice !== null ? xSlice : new Vector2(-1, 100000);
145
146
  ySlice = ySlice !== null ? ySlice : new Vector2(-1, 100000);
146
147
  zSlice = zSlice !== null ? zSlice : new Vector2(-1, 100000);
147
148
  const allChannels = images[layerScope].image.loaders[0].channels;
149
+ logWithColor('Extracting rendering settings from props - success');
148
150
  return {
149
151
  valid: true,
150
152
  channelsVisible,
@@ -159,6 +161,7 @@ export class VolumeRenderManager {
159
161
  xSlice,
160
162
  ySlice,
161
163
  zSlice,
164
+ lodFactor,
162
165
  };
163
166
  }
164
167
  /**
@@ -167,9 +170,9 @@ export class VolumeRenderManager {
167
170
  * @returns {boolean} True if settings were successfully updated
168
171
  */
169
172
  updateFromProps(props) {
170
- logWithColor('Updating from props');
171
173
  const settings = this.extractRenderingSettingsFromProps(props);
172
174
  if (!settings.valid) {
175
+ logWithColor('Updating from props - invalid settings');
173
176
  return false;
174
177
  }
175
178
  // Store the extracted settings
@@ -183,6 +186,10 @@ export class VolumeRenderManager {
183
186
  this.xSlice = settings.xSlice;
184
187
  this.ySlice = settings.ySlice;
185
188
  this.zSlice = settings.zSlice;
189
+ this.uniforms.lodFactor.value = settings.lodFactor;
190
+ logWithColor(`lodFactor ${settings.lodFactor}`);
191
+ this.shader.uniforms.lodFactor.value = settings.lodFactor;
192
+ logWithColor('Updating from props - success');
186
193
  return true;
187
194
  }
188
195
  /**
@@ -17,6 +17,7 @@ export const VolumeShader = {
17
17
  zClip: { value: new Vector2(0, 1000000.0) },
18
18
  u_window_size: { value: new Vector2(1, 1) },
19
19
  u_vol_scale: { value: new Vector3(1, 1, 1) },
20
+ // Set renderstyle to 3 or 4 for debugging modes
20
21
  u_renderstyle: { value: 2 },
21
22
  brickCacheTex: { type: 'sampler3D', value: null },
22
23
  pageTableTex: { type: 'usampler3D', value: null },
@@ -1 +1 @@
1
- {"version":3,"file":"VolumeView.d.ts","sourceRoot":"","sources":["../src/VolumeView.js"],"names":[],"mappings":"AAkLA,2DA4dC"}
1
+ {"version":3,"file":"VolumeView.d.ts","sourceRoot":"","sources":["../src/VolumeView.js"],"names":[],"mappings":"AAuLA,2DA6gBC"}
@@ -6,13 +6,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  /**
7
7
  * VolumeView.js
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 { VolumeDataManager } from './VolumeDataManager.js';
15
+ import { useEventCallback } from '@vitessce/styles';
16
+ import { VolumeDataManager, INIT_STATUS } from './VolumeDataManager.js';
16
17
  import { VolumeRenderManager } from './VolumeRenderManager.js';
17
18
  import { gaussianVertexShader } from './shaders/GaussianVertexShader.js';
18
19
  import { gaussianFragmentShader } from './shaders/GaussianFragmentShader.js';
@@ -27,13 +28,13 @@ function logWithColor(msg) {
27
28
  }
28
29
  // Functions called with useFrame
29
30
  function performGeometryPass(_gl, _camera, _scene, { mrtRef }) {
30
- // logWithColor('performGeometryPass');
31
+ // logWithColor('performGeometryPass', 'blue');
31
32
  _gl.setRenderTarget(mrtRef.current);
32
33
  _gl.clear(true, true, true);
33
34
  _gl.render(_scene, _camera);
34
35
  }
35
36
  function performBlitPass(_gl, { screenSceneRef, screenCameraRef }) {
36
- // logWithColor('performBlitPass');
37
+ // logWithColor('performBlitPass', 'red');
37
38
  _gl.setRenderTarget(null);
38
39
  _gl.clear(true, true, true);
39
40
  _gl.render(screenSceneRef.current, screenCameraRef.current);
@@ -43,7 +44,8 @@ function handleRequests(_gl, { frameRef, dataManager, mrtRef, bufRequest, bufUsa
43
44
  // log.debug('handleRequests', frameRef.current);
44
45
  const ctx = _gl.getContext();
45
46
  const f = frameRef.current;
46
- if (dataManager.noNewRequests === true && f % 100 === 0 && f < 500) {
47
+ // Only allow automatic restart if not manually stopped by user
48
+ if (dataManager.noNewRequests === true && !dataManager.manuallyStopped && f % 100 === 0 && f < 500) {
47
49
  // Every 100th frame of the first 500 frames.
48
50
  // eslint-disable-next-line no-param-reassign
49
51
  dataManager.noNewRequests = false;
@@ -89,8 +91,11 @@ function handleAdaptiveQuality(clock, params) {
89
91
  // While the user is interacting, we want to ensure that the next frame's handleRequests call
90
92
  // will trigger the processUsageData call, and subsequently the processRequestData call
91
93
  // (since processUsageData will set triggerRequest to true its conclusion).
92
- dataManager.noNewRequests = false;
93
- dataManager.triggerUsage = true;
94
+ // But only if not manually stopped by the user
95
+ if (!dataManager.manuallyStopped) {
96
+ dataManager.noNewRequests = false;
97
+ dataManager.triggerUsage = true;
98
+ }
94
99
  return;
95
100
  }
96
101
  if (spatialRenderingModeChanging)
@@ -145,7 +150,7 @@ function handleAdaptiveQuality(clock, params) {
145
150
  export function VolumeView(props) {
146
151
  const { images, imageLayerScopes, imageLayerCoordination, imageChannelScopesByLayer, imageChannelCoordination,
147
152
  // onInitComplete,
148
- spatialRenderingMode, spatialRenderingModeChanging, } = props;
153
+ spatialRenderingMode, spatialRenderingModeChanging, onVolumeLoadingUpdate, } = props;
149
154
  const { gl,
150
155
  // scene,
151
156
  // camera
@@ -166,13 +171,17 @@ export function VolumeView(props) {
166
171
  });
167
172
  // const [lastRes, setLastRes] = useState(null);
168
173
  // const [lastChannels, setLastChannels] = useState([]);
169
- const [is3D, setIs3D] = useState(false);
174
+ const is3D = spatialRenderingMode === '3D';
170
175
  // const [loading, setLoading] = useState(false);
171
176
  // Add new refs for screen quad setup
172
177
  const screenSceneRef = useRef(null);
173
178
  const screenCameraRef = useRef(null);
174
179
  const screenQuadRef = useRef(null);
175
- const [isInteracting, setIsInteracting] = useState(false);
180
+ // The `_setIsInteracting` function is wrapped by the `setIsInteracting` useEventCallback below
181
+ // This allows us to apply the effects related to interaction state synchronously,
182
+ // rather than relying waiting for a React state update with `useEffect`.
183
+ // Updates to `isInteracting` should be done via `setIsInteracting`.
184
+ const [isInteracting, _setIsInteracting] = useState(false);
176
185
  const interactionTimeoutRef = useRef(null);
177
186
  // Track interaction-triggering props
178
187
  // const prevInteractionChannels = useRef();
@@ -245,6 +254,7 @@ export function VolumeView(props) {
245
254
  const firstImageLayerChannelCoordination = imageChannelCoordination?.[0]?.[firstImageLayerScope];
246
255
  useEffect(() => {
247
256
  logWithColor('useEffect INIT');
257
+ let hasRerun = false;
248
258
  if (!dataManager || !renderManager) {
249
259
  log.debug('dataManager or renderManager not initialized yet');
250
260
  return;
@@ -257,12 +267,20 @@ export function VolumeView(props) {
257
267
  log.debug('no firstImageLayerChannelCoordination yet');
258
268
  return;
259
269
  }
260
- // TODO(mark): prevent dataManager.init from being called more than once.
261
- (async () => {
270
+ const initializeDataManager = async () => {
271
+ if (dataManager.initStatus === INIT_STATUS.COMPLETE) {
272
+ log.debug('dataManager already initialized, skipping');
273
+ return;
274
+ }
262
275
  // TODO(mark): separate the initialization which depends on gl, from the initialization which depends on images, from the dm.init(firstImageLayer)
263
276
  dataManager.initImages(images, imageLayerScopes);
264
277
  // TODO: generalize to more than one image layer.
265
278
  await dataManager.init(firstImageLayerChannelCoordination); // device limits, zarr meta
279
+ if (hasRerun) {
280
+ log.debug('Initialization useEffect has rerun, aborting remaining initialization');
281
+ return;
282
+ }
283
+ log.debug('dataManager initialized');
266
284
  // log.debug('dm.physicalScale', dataManager.physicalScale);
267
285
  renderManager.setZarrUniforms(dataManager.zarrStore, dataManager.PT);
268
286
  renderManager.setChannelMapping(dataManager.channels.colorMappings);
@@ -271,16 +289,21 @@ export function VolumeView(props) {
271
289
  // if (onInitComplete) {
272
290
  // onInitComplete({ zarrStoreInfo: dm.zarrStore, deviceLimits: dm.deviceLimits });
273
291
  // }
274
- })();
292
+ invalidate();
293
+ };
294
+ initializeDataManager();
295
+ // avoid setting state on unmounted component
296
+ // eslint-disable-next-line consistent-return
297
+ return () => {
298
+ hasRerun = true;
299
+ };
275
300
  // The data manager does not have a clearCache method, should it?
276
301
  // return () => {
277
302
  // log.debug('VolumeView useEffect cleanup: running dataManager.clearCache()');
278
303
  // dataManager?.clearCache?.();
279
304
  // };
280
305
  // eslint-disable-next-line react-hooks/exhaustive-deps
281
- }, [dataManager, renderManager,
282
- images, imageLayerScopes,
283
- ]);
306
+ }, [dataManager, renderManager, images, imageLayerScopes]);
284
307
  /*
285
308
  useEffect(() => {
286
309
  logWithColor('useEffect GL');
@@ -315,10 +338,9 @@ export function VolumeView(props) {
315
338
  */
316
339
  // TODO(mark): convert to a useMemo?
317
340
  useEffect(() => {
318
- logWithColor('useEffect spatialRenderingMode');
319
341
  const on3D = spatialRenderingMode === '3D';
320
- setIs3D(on3D);
321
342
  if (on3D && dataManager && renderManager) {
343
+ logWithColor('useEffect spatialRenderingMode');
322
344
  // Direct call, no callbacks needed
323
345
  const propsForRenderManager = {
324
346
  images,
@@ -359,42 +381,7 @@ export function VolumeView(props) {
359
381
  }
360
382
  }
361
383
  }
362
- }, [dataManager, renderManager, images, imageLayerScopes, imageLayerCoordination,
363
- imageChannelScopesByLayer, imageChannelCoordination, spatialRenderingMode]);
364
- // TODO(mark): can this logic be moved into useFrame?
365
- /*
366
- useEffect(() => {
367
- logWithColor('useEffect stillRef');
368
- // log.debug('useEffect stillRef');
369
- // log.debug('stillRef', stillRef.current);
370
- if (!screenQuadRef.current) {
371
- log.debug('no screen quad yet');
372
- return;
373
- }
374
- if (!stillRef.current) {
375
- // log.debug('stillRef is false');
376
- screenQuadRef.current.material.uniforms.gaussian.value = 7;
377
- } else {
378
- //log.debug('stillRef is true');
379
- screenQuadRef.current.material.uniforms.gaussian.value = 0;
380
- }
381
- }, [stillRef.current]);
382
- */
383
- // TODO(mark): can this logic be moved into useFrame?
384
- // But need to be careful, since currently, it only runs upon change of isInteracting,
385
- // and we do not want to execute more `invalidate` calls than necessary/currently.
386
- useEffect(() => {
387
- logWithColor('useEffect isInteracting');
388
- if (isInteracting) {
389
- const meshRefUniforms = meshRef.current?.material?.uniforms;
390
- if (meshRefUniforms) {
391
- meshRefUniforms.renderRes.value = dataManager.PT.lowestDataRes;
392
- }
393
- // setRenderSpeed(dataManager.PT.lowestDataRes);
394
- stillRef.current = false;
395
- invalidate();
396
- }
397
- }, [invalidate, isInteracting]);
384
+ }, [dataManager, renderManager, images, imageLayerScopes, imageLayerCoordination, imageChannelScopesByLayer, imageChannelCoordination, spatialRenderingMode]);
398
385
  // Execute code on every rendered frame.
399
386
  // A numerical renderPriority value will cause React Three Fiber
400
387
  // to disable automatic rendering altogether.
@@ -405,12 +392,14 @@ export function VolumeView(props) {
405
392
  useFrame((state, delta, xrFrame) => {
406
393
  // NOTE: Do not update React state inside useFrame.
407
394
  // Reference: https://r3f.docs.pmnd.rs/advanced/pitfalls#%E2%9D%8C-setstate-in-useframe-is-bad
408
- if (!mrtRef.current || !dataManager || !renderManager)
395
+ if (!mrtRef.current || !dataManager || !renderManager) {
409
396
  return;
397
+ }
410
398
  // Don't render to MRT if shader is not ready - this prevents WebGL warnings
411
399
  // about missing fragment shader outputs for multiple render targets
412
- if (!renderState.shader)
400
+ if (!renderState.shader) {
413
401
  return;
402
+ }
414
403
  // Receive the same state as the useThree hook.
415
404
  const { gl: frameGl, camera: frameCamera, scene: frameScene, clock, } = state;
416
405
  if (!stillRef.current) {
@@ -488,15 +477,33 @@ export function VolumeView(props) {
488
477
  };
489
478
  }, [mainOrbitControlsRef.current, setIsInteracting]);
490
479
  */
491
- const onOrbitControlsStart = useCallback((e) => {
480
+ /**
481
+ * Handle interaction state changes in event callbacks.
482
+ * This avoids the need for a useEffect hook which
483
+ * would run on every render frame.
484
+ */
485
+ const setIsInteracting = useEventCallback((interacting) => {
486
+ logWithColor('invalidateOnInteraction callback');
487
+ _setIsInteracting(interacting);
488
+ if (interacting) {
489
+ const meshRefUniforms = meshRef.current?.material?.uniforms;
490
+ if (meshRefUniforms) {
491
+ meshRefUniforms.renderRes.value = dataManager.PT.lowestDataRes;
492
+ }
493
+ // setRenderSpeed(dataManager.PT.lowestDataRes);
494
+ stillRef.current = false;
495
+ invalidate();
496
+ }
497
+ });
498
+ const onOrbitControlsStart = useEventCallback((e) => {
492
499
  setIsInteracting(true);
493
- }, []);
494
- const onOrbitControlsEnd = useCallback((e) => {
500
+ });
501
+ const onOrbitControlsEnd = useEventCallback((e) => {
495
502
  clearTimeout(interactionTimeoutRef.current);
496
503
  interactionTimeoutRef.current = setTimeout(() => {
497
504
  setIsInteracting(false);
498
505
  }, 300);
499
- }, []);
506
+ });
500
507
  // Set isInteracting while the user is sliding the channel slider
501
508
  // for smooth updates.
502
509
  useEffect(() => {
@@ -509,7 +516,40 @@ export function VolumeView(props) {
509
516
  interactionTimeoutRef.current = setTimeout(() => {
510
517
  setIsInteracting(false);
511
518
  }, 300);
512
- }, [firstImageLayerChannelCoordination]);
519
+ }, [dataManager, firstImageLayerChannelCoordination, renderManager, setIsInteracting]);
520
+ const stopLoading = useEventCallback(() => {
521
+ if (dataManager) {
522
+ dataManager.stopLoading();
523
+ }
524
+ });
525
+ const restartLoading = useEventCallback(() => {
526
+ if (dataManager) {
527
+ dataManager.restartLoading();
528
+ }
529
+ });
530
+ const getLoadingProgress = useEventCallback(() => {
531
+ if (dataManager) {
532
+ return dataManager.getLoadingProgress();
533
+ }
534
+ return null;
535
+ });
536
+ // Notify parent component about loading updates
537
+ useEffect(() => {
538
+ if (!onVolumeLoadingUpdate)
539
+ return undefined;
540
+ const intervalId = setInterval(() => {
541
+ const loadingProgress = getLoadingProgress();
542
+ onVolumeLoadingUpdate({
543
+ loadingProgress,
544
+ stillRef,
545
+ onStopLoading: stopLoading,
546
+ onRestartLoading: restartLoading,
547
+ });
548
+ }, 1000); // Update every 1000ms (1s)
549
+ return () => clearInterval(intervalId);
550
+ // `useEventCallback` ensures stable references
551
+ // eslint-disable-next-line react-hooks/exhaustive-deps
552
+ }, [onVolumeLoadingUpdate, stillRef]);
513
553
  // Render nothing during initialization.
514
554
  if (!is3D || !dataManager || !renderManager)
515
555
  return null;