@vitessce/spatial-accelerated 3.8.0
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/LICENSE +21 -0
- package/dist/index.js +52925 -0
- package/dist-tsc/SpatialWrapper.d.ts +2 -0
- package/dist-tsc/SpatialWrapper.d.ts.map +1 -0
- package/dist-tsc/SpatialWrapper.js +45 -0
- package/dist-tsc/VolumeDataManager.d.ts +170 -0
- package/dist-tsc/VolumeDataManager.d.ts.map +1 -0
- package/dist-tsc/VolumeDataManager.js +1273 -0
- package/dist-tsc/VolumeDataManager.test.d.ts +2 -0
- package/dist-tsc/VolumeDataManager.test.d.ts.map +1 -0
- package/dist-tsc/VolumeDataManager.test.js +164 -0
- package/dist-tsc/VolumeRenderManager.d.ts +265 -0
- package/dist-tsc/VolumeRenderManager.d.ts.map +1 -0
- package/dist-tsc/VolumeRenderManager.js +396 -0
- package/dist-tsc/VolumeShader.d.ts +262 -0
- package/dist-tsc/VolumeShader.d.ts.map +1 -0
- package/dist-tsc/VolumeShader.js +73 -0
- package/dist-tsc/VolumeView.d.ts +2 -0
- package/dist-tsc/VolumeView.d.ts.map +1 -0
- package/dist-tsc/VolumeView.js +521 -0
- package/dist-tsc/index.d.ts +2 -0
- package/dist-tsc/index.d.ts.map +1 -0
- package/dist-tsc/index.js +1 -0
- package/dist-tsc/shaders/GaussianFragmentShader.d.ts +2 -0
- package/dist-tsc/shaders/GaussianFragmentShader.d.ts.map +1 -0
- package/dist-tsc/shaders/GaussianFragmentShader.js +242 -0
- package/dist-tsc/shaders/GaussianVertexShader.d.ts +2 -0
- package/dist-tsc/shaders/GaussianVertexShader.d.ts.map +1 -0
- package/dist-tsc/shaders/GaussianVertexShader.js +51 -0
- package/dist-tsc/shaders/VolumeFragmentShader.d.ts +2 -0
- package/dist-tsc/shaders/VolumeFragmentShader.d.ts.map +1 -0
- package/dist-tsc/shaders/VolumeFragmentShader.js +1817 -0
- package/dist-tsc/shaders/VolumeVertexShader.d.ts +2 -0
- package/dist-tsc/shaders/VolumeVertexShader.d.ts.map +1 -0
- package/dist-tsc/shaders/VolumeVertexShader.js +110 -0
- package/package.json +63 -0
- package/src/SpatialWrapper.js +60 -0
- package/src/VolumeDataManager.js +1515 -0
- package/src/VolumeDataManager.test.js +180 -0
- package/src/VolumeRenderManager.js +589 -0
- package/src/VolumeShader.js +74 -0
- package/src/VolumeView.js +651 -0
- package/src/index.js +1 -0
- package/src/shaders/GaussianFragmentShader.js +243 -0
- package/src/shaders/GaussianVertexShader.js +52 -0
- package/src/shaders/VolumeFragmentShader.js +1818 -0
- package/src/shaders/VolumeVertexShader.js +111 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
/* eslint-disable max-len */
|
|
2
|
+
/* eslint-disable no-underscore-dangle */
|
|
3
|
+
/* eslint-disable no-unused-vars */
|
|
4
|
+
/* eslint-disable react/no-unknown-property */
|
|
5
|
+
/**
|
|
6
|
+
* VolumeView.js
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useRef, useState, useEffect, useCallback, useMemo } from 'react';
|
|
10
|
+
import { useThree, useFrame } from '@react-three/fiber';
|
|
11
|
+
import { OrbitControls } from '@react-three/drei';
|
|
12
|
+
import * as THREE from 'three';
|
|
13
|
+
import { WebGLMultipleRenderTargets } from 'three';
|
|
14
|
+
import { log, atLeastLogLevel, LogLevel } from '@vitessce/globals';
|
|
15
|
+
|
|
16
|
+
import { VolumeDataManager } from './VolumeDataManager.js';
|
|
17
|
+
import { VolumeRenderManager } from './VolumeRenderManager.js';
|
|
18
|
+
|
|
19
|
+
import { gaussianVertexShader } from './shaders/GaussianVertexShader.js';
|
|
20
|
+
import { gaussianFragmentShader } from './shaders/GaussianFragmentShader.js';
|
|
21
|
+
|
|
22
|
+
function framebufferFor(renderer, rt) {
|
|
23
|
+
const p = renderer.properties?.get(rt);
|
|
24
|
+
return p?.framebuffer || p?.__webglFramebuffer || rt.__webglFramebuffer;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function logWithColor(msg) {
|
|
28
|
+
if (atLeastLogLevel(LogLevel.DEBUG)) {
|
|
29
|
+
console.warn(
|
|
30
|
+
`%cV: ${msg}`,
|
|
31
|
+
'background: deeppink; color: white; padding: 2px; border-radius: 3px;',
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Functions called with useFrame
|
|
37
|
+
function performGeometryPass(_gl, _camera, _scene, { mrtRef }) {
|
|
38
|
+
// logWithColor('performGeometryPass');
|
|
39
|
+
_gl.setRenderTarget(mrtRef.current);
|
|
40
|
+
_gl.clear(true, true, true);
|
|
41
|
+
_gl.render(_scene, _camera);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function performBlitPass(_gl, { screenSceneRef, screenCameraRef }) {
|
|
45
|
+
// logWithColor('performBlitPass');
|
|
46
|
+
_gl.setRenderTarget(null);
|
|
47
|
+
_gl.clear(true, true, true);
|
|
48
|
+
_gl.render(screenSceneRef.current, screenCameraRef.current);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handleRequests(_gl, { frameRef, dataManager, mrtRef, bufRequest, bufUsage }) {
|
|
52
|
+
// logWithColor('handleRequests');
|
|
53
|
+
// log.debug('handleRequests', frameRef.current);
|
|
54
|
+
const ctx = _gl.getContext();
|
|
55
|
+
const f = frameRef.current;
|
|
56
|
+
|
|
57
|
+
if (dataManager.noNewRequests === true && f % 100 === 0 && f < 500) {
|
|
58
|
+
// Every 100th frame of the first 500 frames.
|
|
59
|
+
// eslint-disable-next-line no-param-reassign
|
|
60
|
+
dataManager.noNewRequests = false;
|
|
61
|
+
}
|
|
62
|
+
if (dataManager.triggerRequest === true && dataManager.noNewRequests === false) {
|
|
63
|
+
// Read the pixels of the request buffer into the width*height*RGBA bufRequest.current array.
|
|
64
|
+
ctx.bindFramebuffer(ctx.READ_FRAMEBUFFER, framebufferFor(_gl, mrtRef.current));
|
|
65
|
+
ctx.readBuffer(ctx.COLOR_ATTACHMENT1);
|
|
66
|
+
ctx.readPixels(0, 0, mrtRef.current.width, mrtRef.current.height,
|
|
67
|
+
ctx.RGBA, ctx.UNSIGNED_BYTE, bufRequest.current);
|
|
68
|
+
// Based on the request buffer contents, process the requests
|
|
69
|
+
// (e.g., start loading the brick data and upload to the brick cache).
|
|
70
|
+
// Finally, it will set triggerUsage to true.
|
|
71
|
+
dataManager.processRequestData(bufRequest.current);
|
|
72
|
+
} else if (dataManager.triggerUsage === true && dataManager.noNewRequests === false) {
|
|
73
|
+
// Read the pixels of the usage buffer into the width*height*RGBA bufUsage.current array.
|
|
74
|
+
ctx.bindFramebuffer(ctx.READ_FRAMEBUFFER, framebufferFor(_gl, mrtRef.current));
|
|
75
|
+
ctx.readBuffer(ctx.COLOR_ATTACHMENT2);
|
|
76
|
+
ctx.readPixels(0, 0, mrtRef.current.width, mrtRef.current.height,
|
|
77
|
+
ctx.RGBA, ctx.UNSIGNED_BYTE, bufUsage.current);
|
|
78
|
+
// Based on the usage buffer contents, process the usage data.
|
|
79
|
+
// This identifies used bricks and updates their timestamps.
|
|
80
|
+
// Timestamps are used to determine which bricks are most/least recently used for cache eviction.
|
|
81
|
+
// If the brick cache is full, it will create an LRU cache.
|
|
82
|
+
// Finally, it will set triggerRequest to true.
|
|
83
|
+
dataManager.processUsageData(bufUsage.current);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function handleAdaptiveQuality(clock, params) {
|
|
88
|
+
const {
|
|
89
|
+
invalidate,
|
|
90
|
+
isInteracting,
|
|
91
|
+
dataManager,
|
|
92
|
+
spatialRenderingModeChanging,
|
|
93
|
+
meshRef,
|
|
94
|
+
stillRef,
|
|
95
|
+
screenQuadRef,
|
|
96
|
+
lastSampleRef,
|
|
97
|
+
frameRef,
|
|
98
|
+
lastFrameCountRef,
|
|
99
|
+
} = params;
|
|
100
|
+
|
|
101
|
+
// Moved out of useEffect
|
|
102
|
+
if (screenQuadRef.current) {
|
|
103
|
+
if (!stillRef.current) {
|
|
104
|
+
// log.debug('stillRef is false');
|
|
105
|
+
screenQuadRef.current.material.uniforms.gaussian.value = 7;
|
|
106
|
+
} else {
|
|
107
|
+
// log.debug('stillRef is true');
|
|
108
|
+
screenQuadRef.current.material.uniforms.gaussian.value = 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// End moved out of useEffect
|
|
112
|
+
|
|
113
|
+
if (isInteracting) {
|
|
114
|
+
// While the user is interacting, we want to ensure that the next frame's handleRequests call
|
|
115
|
+
// will trigger the processUsageData call, and subsequently the processRequestData call
|
|
116
|
+
// (since processUsageData will set triggerRequest to true its conclusion).
|
|
117
|
+
dataManager.noNewRequests = false;
|
|
118
|
+
dataManager.triggerUsage = true;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (spatialRenderingModeChanging) return;
|
|
122
|
+
|
|
123
|
+
const meshRefUniforms = meshRef.current?.material?.uniforms;
|
|
124
|
+
const renderSpeed = meshRefUniforms?.renderRes?.value ?? dataManager?.PT?.lowestDataRes;
|
|
125
|
+
|
|
126
|
+
if (dataManager.noNewRequests) {
|
|
127
|
+
// If there are no more new requests, we can render in higher resolution.
|
|
128
|
+
if (renderSpeed !== 0) {
|
|
129
|
+
if (meshRefUniforms) {
|
|
130
|
+
meshRefUniforms.renderRes.value = 0; // Set to best quality
|
|
131
|
+
}
|
|
132
|
+
log.debug('Adaptive Quality: No new requests. Setting renderSpeed to 0 (best quality).');
|
|
133
|
+
stillRef.current = false;
|
|
134
|
+
screenQuadRef.current.material.uniforms.gaussian.value = 7;
|
|
135
|
+
invalidate();
|
|
136
|
+
} else if (!stillRef.current) {
|
|
137
|
+
log.debug('Adaptive Quality: No new requests and already at best quality. Setting stillRef to true.');
|
|
138
|
+
stillRef.current = true;
|
|
139
|
+
screenQuadRef.current.material.uniforms.gaussian.value = 0;
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (stillRef.current) {
|
|
145
|
+
stillRef.current = false;
|
|
146
|
+
invalidate();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const t = clock.getElapsedTime();
|
|
150
|
+
if (t - lastSampleRef.current < 1) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const timeElapsedDuringSample = t - lastSampleRef.current;
|
|
155
|
+
const framesRenderedDuringSample = frameRef.current - lastFrameCountRef.current;
|
|
156
|
+
let fps = 0;
|
|
157
|
+
if (timeElapsedDuringSample > 0) {
|
|
158
|
+
fps = framesRenderedDuringSample / timeElapsedDuringSample;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
lastSampleRef.current = t;
|
|
162
|
+
lastFrameCountRef.current = frameRef.current;
|
|
163
|
+
|
|
164
|
+
const upscale = fps > 100 && renderSpeed > 0; // Can't go below 0
|
|
165
|
+
const downscale = fps < 30 && renderSpeed < dataManager.PT.lowestDataRes; // Can't go above
|
|
166
|
+
|
|
167
|
+
if (upscale || downscale) {
|
|
168
|
+
const newSpeed = renderSpeed + (downscale ? 1 : -1);
|
|
169
|
+
if (meshRefUniforms) {
|
|
170
|
+
meshRefUniforms.renderRes.value = newSpeed;
|
|
171
|
+
}
|
|
172
|
+
invalidate(); // Ensure the change takes effect
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// END Functions called with useFrame
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
export function VolumeView(props) {
|
|
180
|
+
const {
|
|
181
|
+
images,
|
|
182
|
+
imageLayerScopes,
|
|
183
|
+
imageLayerCoordination,
|
|
184
|
+
imageChannelScopesByLayer,
|
|
185
|
+
imageChannelCoordination,
|
|
186
|
+
// onInitComplete,
|
|
187
|
+
spatialRenderingMode,
|
|
188
|
+
spatialRenderingModeChanging,
|
|
189
|
+
} = props;
|
|
190
|
+
const {
|
|
191
|
+
gl,
|
|
192
|
+
// scene,
|
|
193
|
+
// camera
|
|
194
|
+
} = useThree();
|
|
195
|
+
|
|
196
|
+
// Request a new render.
|
|
197
|
+
const invalidate = useThree(state => state.invalidate);
|
|
198
|
+
|
|
199
|
+
const orbitRef = useRef(null);
|
|
200
|
+
const meshRef = useRef(null);
|
|
201
|
+
const bufRequest = useRef(null);
|
|
202
|
+
const bufUsage = useRef(null);
|
|
203
|
+
const mrtRef = useRef(null);
|
|
204
|
+
|
|
205
|
+
// const [managers, setManagers] = useState(null);
|
|
206
|
+
const [renderState, setRenderState] = useState({
|
|
207
|
+
uniforms: null,
|
|
208
|
+
shader: null,
|
|
209
|
+
meshScale: [1, 1, 1],
|
|
210
|
+
geometrySize: [1, 1, 1],
|
|
211
|
+
});
|
|
212
|
+
// const [lastRes, setLastRes] = useState(null);
|
|
213
|
+
// const [lastChannels, setLastChannels] = useState([]);
|
|
214
|
+
const [is3D, setIs3D] = useState(false);
|
|
215
|
+
// const [loading, setLoading] = useState(false);
|
|
216
|
+
|
|
217
|
+
// Add new refs for screen quad setup
|
|
218
|
+
const screenSceneRef = useRef(null);
|
|
219
|
+
const screenCameraRef = useRef(null);
|
|
220
|
+
const screenQuadRef = useRef(null);
|
|
221
|
+
|
|
222
|
+
const [isInteracting, setIsInteracting] = useState(false);
|
|
223
|
+
const interactionTimeoutRef = useRef(null);
|
|
224
|
+
|
|
225
|
+
// Track interaction-triggering props
|
|
226
|
+
// const prevInteractionChannels = useRef();
|
|
227
|
+
|
|
228
|
+
// new refs from step 2
|
|
229
|
+
// const [renderSpeed, setRenderSpeed] = useState(dataManager.PT.lowestDataRes);
|
|
230
|
+
const stillRef = useRef(false); // skip geometry pass when true
|
|
231
|
+
const frameRef = useRef(0); // frame counter
|
|
232
|
+
const lastSampleRef = useRef(0);
|
|
233
|
+
const lastFrameCountRef = useRef(0); // For more stable FPS calculation
|
|
234
|
+
|
|
235
|
+
// const mainOrbitControlsRef = useRef(null); // Added for main view OrbitControls
|
|
236
|
+
|
|
237
|
+
// const sameArray = (a, b) => a && b && a.length === b.length && a.every((v, i) => v === b[i]);
|
|
238
|
+
|
|
239
|
+
// INITIALIZATION (GL-DEPENDENT ONLY)
|
|
240
|
+
|
|
241
|
+
const dataManager = useMemo(() => new VolumeDataManager(gl), [gl]);
|
|
242
|
+
|
|
243
|
+
const renderManager = useMemo(() => new VolumeRenderManager(), []);
|
|
244
|
+
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
logWithColor('useEffect MRT target matching canvas');
|
|
247
|
+
|
|
248
|
+
const { width, height } = gl.domElement;
|
|
249
|
+
// We use three render targets:
|
|
250
|
+
// layout(location = 0) out vec4 gColor: Final rendered color (sRGB)
|
|
251
|
+
// layout(location = 1) out vec4 gRequest: Brick loading requests (packed coordinates)
|
|
252
|
+
// layout(location = 2) out vec4 gUsage: Brick usage tracking (for cache management)
|
|
253
|
+
const mrt = new WebGLMultipleRenderTargets(width, height, 3);
|
|
254
|
+
mrt.texture.forEach((tex) => {
|
|
255
|
+
// eslint-disable-next-line no-param-reassign
|
|
256
|
+
tex.format = THREE.RGBAFormat;
|
|
257
|
+
// eslint-disable-next-line no-param-reassign
|
|
258
|
+
tex.type = THREE.UnsignedByteType;
|
|
259
|
+
// eslint-disable-next-line no-param-reassign
|
|
260
|
+
// tex.encoding = THREE.LinearEncoding;
|
|
261
|
+
// eslint-disable-next-line no-param-reassign
|
|
262
|
+
tex.minFilter = THREE.NearestFilter;
|
|
263
|
+
// eslint-disable-next-line no-param-reassign
|
|
264
|
+
tex.magFilter = THREE.NearestFilter;
|
|
265
|
+
// eslint-disable-next-line no-param-reassign
|
|
266
|
+
tex.generateMipmaps = false;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Create screen quad setup
|
|
270
|
+
const screenScene = new THREE.Scene();
|
|
271
|
+
const screenCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
|
|
272
|
+
screenCamera.position.z = 1;
|
|
273
|
+
|
|
274
|
+
const screenMaterial = new THREE.ShaderMaterial({
|
|
275
|
+
uniforms: {
|
|
276
|
+
// Bind the first render target texture as the input of the gaussian blur shader.
|
|
277
|
+
tDiffuse: { value: mrt.texture[0] },
|
|
278
|
+
resolution: { value: new THREE.Vector2(width, height) },
|
|
279
|
+
gaussian: { value: 7 },
|
|
280
|
+
},
|
|
281
|
+
vertexShader: gaussianVertexShader,
|
|
282
|
+
fragmentShader: gaussianFragmentShader,
|
|
283
|
+
transparent: true,
|
|
284
|
+
});
|
|
285
|
+
const screenQuad = new THREE.Mesh(
|
|
286
|
+
new THREE.PlaneGeometry(2, 2),
|
|
287
|
+
screenMaterial,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
screenScene.add(screenQuad);
|
|
291
|
+
|
|
292
|
+
screenSceneRef.current = screenScene;
|
|
293
|
+
screenCameraRef.current = screenCamera;
|
|
294
|
+
screenQuadRef.current = screenQuad;
|
|
295
|
+
|
|
296
|
+
bufRequest.current = new Uint8Array(width * height * 4);
|
|
297
|
+
bufUsage.current = new Uint8Array(width * height * 4);
|
|
298
|
+
mrtRef.current = mrt;
|
|
299
|
+
|
|
300
|
+
return () => {
|
|
301
|
+
mrt.dispose();
|
|
302
|
+
screenMaterial.dispose();
|
|
303
|
+
screenQuad.geometry.dispose();
|
|
304
|
+
};
|
|
305
|
+
}, [gl]);
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
// INITIALIZATION (IMAGE-DEPENDENT)
|
|
309
|
+
const firstImageLayerScope = imageLayerScopes?.[0];
|
|
310
|
+
const firstImage = images?.[firstImageLayerScope];
|
|
311
|
+
const firstImageChannelScope = imageChannelScopesByLayer?.[firstImageLayerScope];
|
|
312
|
+
const firstImageLayerChannelCoordination = imageChannelCoordination?.[0]?.[firstImageLayerScope];
|
|
313
|
+
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
logWithColor('useEffect INIT');
|
|
316
|
+
|
|
317
|
+
if (!dataManager || !renderManager) {
|
|
318
|
+
log.debug('dataManager or renderManager not initialized yet');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!firstImage) {
|
|
323
|
+
log.debug('no first image layer yet');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!firstImageLayerChannelCoordination) {
|
|
328
|
+
log.debug('no firstImageLayerChannelCoordination yet');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// TODO(mark): prevent dataManager.init from being called more than once.
|
|
333
|
+
(async () => {
|
|
334
|
+
// TODO(mark): separate the initialization which depends on gl, from the initialization which depends on images, from the dm.init(firstImageLayer)
|
|
335
|
+
dataManager.initImages(images, imageLayerScopes);
|
|
336
|
+
// TODO: generalize to more than one image layer.
|
|
337
|
+
await dataManager.init(firstImageLayerChannelCoordination); // device limits, zarr meta
|
|
338
|
+
|
|
339
|
+
// log.debug('dm.physicalScale', dataManager.physicalScale);
|
|
340
|
+
|
|
341
|
+
renderManager.setZarrUniforms(dataManager.zarrStore, dataManager.PT);
|
|
342
|
+
renderManager.setChannelMapping(dataManager.channels.colorMappings);
|
|
343
|
+
|
|
344
|
+
log.debug('rm.uniforms', renderManager.uniforms);
|
|
345
|
+
|
|
346
|
+
// setManagers({ dataManager: dm, renderManager: rm });
|
|
347
|
+
// if (onInitComplete) {
|
|
348
|
+
// onInitComplete({ zarrStoreInfo: dm.zarrStore, deviceLimits: dm.deviceLimits });
|
|
349
|
+
// }
|
|
350
|
+
})();
|
|
351
|
+
|
|
352
|
+
// The data manager does not have a clearCache method, should it?
|
|
353
|
+
// return () => {
|
|
354
|
+
// log.debug('VolumeView useEffect cleanup: running dataManager.clearCache()');
|
|
355
|
+
// dataManager?.clearCache?.();
|
|
356
|
+
// };
|
|
357
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
358
|
+
}, [dataManager, renderManager,
|
|
359
|
+
images, imageLayerScopes,
|
|
360
|
+
]);
|
|
361
|
+
|
|
362
|
+
/*
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
logWithColor('useEffect GL');
|
|
365
|
+
gl.autoClear = false;
|
|
366
|
+
}, [gl]);
|
|
367
|
+
*/
|
|
368
|
+
|
|
369
|
+
/*
|
|
370
|
+
const extractSettings = useCallback(() => {
|
|
371
|
+
logWithColor('callback extractSettings');
|
|
372
|
+
if (!dataManager || !renderManager) return null;
|
|
373
|
+
const ok = renderManager.updateFromProps(props);
|
|
374
|
+
return ok ? renderManager.extractRenderingSettingsFromProps(props) : null;
|
|
375
|
+
}, [props, dataManager, renderManager]);
|
|
376
|
+
*/
|
|
377
|
+
|
|
378
|
+
/*
|
|
379
|
+
const loadIfNeeded = useCallback(async (settings) => {
|
|
380
|
+
if (!settings || !dataManager || !renderManager) return;
|
|
381
|
+
|
|
382
|
+
const resolutionChanged = settings.resolution !== lastRes;
|
|
383
|
+
const channelsChanged = !sameArray(settings.channelTargetC, lastChannels);
|
|
384
|
+
|
|
385
|
+
// Always update rendering state
|
|
386
|
+
const renderManagerState = renderManager.updateRendering(dataManager);
|
|
387
|
+
if (renderManagerState) setRenderState(renderManagerState);
|
|
388
|
+
|
|
389
|
+
// Update tracking state
|
|
390
|
+
if (resolutionChanged) setLastRes(settings.resolution);
|
|
391
|
+
if (channelsChanged) setLastChannels([...settings.channelTargetC]);
|
|
392
|
+
|
|
393
|
+
stillRef.current = false;
|
|
394
|
+
}, [dataManager, renderManager, lastRes, lastChannels]);
|
|
395
|
+
*/
|
|
396
|
+
|
|
397
|
+
// TODO(mark): convert to a useMemo?
|
|
398
|
+
useEffect(() => {
|
|
399
|
+
logWithColor('useEffect spatialRenderingMode');
|
|
400
|
+
const on3D = spatialRenderingMode === '3D';
|
|
401
|
+
setIs3D(on3D);
|
|
402
|
+
|
|
403
|
+
if (on3D && dataManager && renderManager) {
|
|
404
|
+
// Direct call, no callbacks needed
|
|
405
|
+
const propsForRenderManager = {
|
|
406
|
+
images,
|
|
407
|
+
imageLayerScopes,
|
|
408
|
+
imageLayerCoordination,
|
|
409
|
+
imageChannelScopesByLayer,
|
|
410
|
+
imageChannelCoordination,
|
|
411
|
+
spatialRenderingMode,
|
|
412
|
+
};
|
|
413
|
+
if (renderManager.updateFromProps(propsForRenderManager)) {
|
|
414
|
+
const { zarrInit } = renderManager;
|
|
415
|
+
if (!zarrInit) {
|
|
416
|
+
// If textures have not yet been initialized, do so prior to calling .updateRendering,
|
|
417
|
+
// as this internally calls renderManager.updateUniforms,
|
|
418
|
+
// which (I anticipate) expects the textures to be initialized.
|
|
419
|
+
|
|
420
|
+
// Initialize textures without warnings
|
|
421
|
+
dataManager.ptTHREE.needsUpdate = false;
|
|
422
|
+
dataManager.bcTHREE.needsUpdate = false;
|
|
423
|
+
|
|
424
|
+
// Initialize textures if needed
|
|
425
|
+
dataManager.renderer.initTexture(dataManager.bcTHREE);
|
|
426
|
+
dataManager.renderer.initTexture(dataManager.ptTHREE);
|
|
427
|
+
|
|
428
|
+
// Handle the first brick request.
|
|
429
|
+
dataManager.initTexture();
|
|
430
|
+
}
|
|
431
|
+
// Render manager needs some things from the data manager.
|
|
432
|
+
const nextRenderState = renderManager.updateRendering({
|
|
433
|
+
zarrStoreShapes: dataManager.zarrStore.shapes,
|
|
434
|
+
originalScaleXYZ: dataManager.getOriginalScaleXYZ(),
|
|
435
|
+
physicalDimensionsXYZ: dataManager.getPhysicalDimensionsXYZ(),
|
|
436
|
+
maxResolutionXYZ: dataManager.getMaxResolutionXYZ(),
|
|
437
|
+
boxDimensionsXYZ: dataManager.getBoxDimensionsXYZ(),
|
|
438
|
+
normalizedScaleXYZ: dataManager.getNormalizedScaleXYZ(),
|
|
439
|
+
bcTHREE: dataManager.bcTHREE,
|
|
440
|
+
ptTHREE: dataManager.ptTHREE,
|
|
441
|
+
});
|
|
442
|
+
if (nextRenderState) {
|
|
443
|
+
setRenderState(nextRenderState);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}, [dataManager, renderManager, images, imageLayerScopes, imageLayerCoordination,
|
|
448
|
+
imageChannelScopesByLayer, imageChannelCoordination, spatialRenderingMode]);
|
|
449
|
+
|
|
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
|
+
|
|
486
|
+
// Execute code on every rendered frame.
|
|
487
|
+
// A numerical renderPriority value will cause React Three Fiber
|
|
488
|
+
// to disable automatic rendering altogether.
|
|
489
|
+
// It will now be your responsibility to render
|
|
490
|
+
// (by calling invalidate()).
|
|
491
|
+
// Reference: https://r3f.docs.pmnd.rs/api/hooks#useframe
|
|
492
|
+
const RENDER_PRIORITY = 1;
|
|
493
|
+
useFrame((state, delta, xrFrame) => {
|
|
494
|
+
// NOTE: Do not update React state inside useFrame.
|
|
495
|
+
// Reference: https://r3f.docs.pmnd.rs/advanced/pitfalls#%E2%9D%8C-setstate-in-useframe-is-bad
|
|
496
|
+
if (!mrtRef.current || !dataManager || !renderManager) return;
|
|
497
|
+
|
|
498
|
+
// Receive the same state as the useThree hook.
|
|
499
|
+
const {
|
|
500
|
+
gl: frameGl,
|
|
501
|
+
camera: frameCamera,
|
|
502
|
+
scene: frameScene,
|
|
503
|
+
clock,
|
|
504
|
+
} = state;
|
|
505
|
+
|
|
506
|
+
if (!stillRef.current) {
|
|
507
|
+
performGeometryPass(frameGl, frameCamera, frameScene, { mrtRef });
|
|
508
|
+
}
|
|
509
|
+
performBlitPass(frameGl, { screenSceneRef, screenCameraRef });
|
|
510
|
+
handleRequests(frameGl, { frameRef, dataManager, mrtRef, bufRequest, bufUsage });
|
|
511
|
+
handleAdaptiveQuality(clock, {
|
|
512
|
+
invalidate,
|
|
513
|
+
isInteracting,
|
|
514
|
+
dataManager,
|
|
515
|
+
spatialRenderingModeChanging,
|
|
516
|
+
meshRef,
|
|
517
|
+
stillRef,
|
|
518
|
+
screenQuadRef,
|
|
519
|
+
lastSampleRef,
|
|
520
|
+
frameRef,
|
|
521
|
+
lastFrameCountRef,
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
frameRef.current += 1;
|
|
525
|
+
}, RENDER_PRIORITY);
|
|
526
|
+
|
|
527
|
+
/*
|
|
528
|
+
useEffect(() => {
|
|
529
|
+
logWithColor('useEffect setProcessingTargets');
|
|
530
|
+
renderManager.setProcessingTargets(mrtRef.current);
|
|
531
|
+
}, [renderManager, mrtRef]);
|
|
532
|
+
*/
|
|
533
|
+
|
|
534
|
+
/*
|
|
535
|
+
useEffect(() => {
|
|
536
|
+
logWithColor('useEffect renderState');
|
|
537
|
+
const u = meshRef.current?.material?.uniforms;
|
|
538
|
+
if (!u) return;
|
|
539
|
+
u.renderRes.value = renderSpeed;
|
|
540
|
+
}, [renderSpeed]);
|
|
541
|
+
*/
|
|
542
|
+
|
|
543
|
+
/*
|
|
544
|
+
useEffect(() => {
|
|
545
|
+
logWithColor('useEffect mainOrbitControlsRef');
|
|
546
|
+
const controlsInstance = mainOrbitControlsRef.current; // Renamed to avoid conflict with controls in event handlers
|
|
547
|
+
if (!controlsInstance) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const handleStart = () => setIsInteracting(true);
|
|
552
|
+
|
|
553
|
+
const handleEnd = () => {
|
|
554
|
+
clearTimeout(interactionTimeoutRef.current);
|
|
555
|
+
interactionTimeoutRef.current = setTimeout(() => setIsInteracting(false), 300);
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const handleWheel = () => {
|
|
559
|
+
setIsInteracting(true);
|
|
560
|
+
clearTimeout(interactionTimeoutRef.current);
|
|
561
|
+
interactionTimeoutRef.current = setTimeout(() => setIsInteracting(false), 300);
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
controlsInstance.addEventListener('start', handleStart);
|
|
565
|
+
controlsInstance.addEventListener('end', handleEnd);
|
|
566
|
+
|
|
567
|
+
const { domElement } = controlsInstance;
|
|
568
|
+
// Ensure domElement is available
|
|
569
|
+
if (domElement) {
|
|
570
|
+
domElement.addEventListener('wheel', handleWheel, { passive: true });
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return () => {
|
|
574
|
+
if (controlsInstance) {
|
|
575
|
+
controlsInstance.removeEventListener('start', handleStart);
|
|
576
|
+
controlsInstance.removeEventListener('end', handleEnd);
|
|
577
|
+
if (domElement) {
|
|
578
|
+
domElement.removeEventListener('wheel', handleWheel);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
clearTimeout(interactionTimeoutRef.current);
|
|
582
|
+
};
|
|
583
|
+
}, [mainOrbitControlsRef.current, setIsInteracting]);
|
|
584
|
+
*/
|
|
585
|
+
|
|
586
|
+
const onOrbitControlsStart = useCallback((e) => {
|
|
587
|
+
setIsInteracting(true);
|
|
588
|
+
}, []);
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
const onOrbitControlsEnd = useCallback((e) => {
|
|
592
|
+
clearTimeout(interactionTimeoutRef.current);
|
|
593
|
+
interactionTimeoutRef.current = setTimeout(() => {
|
|
594
|
+
setIsInteracting(false);
|
|
595
|
+
}, 300);
|
|
596
|
+
}, []);
|
|
597
|
+
|
|
598
|
+
// Set isInteracting while the user is sliding the channel slider
|
|
599
|
+
// for smooth updates.
|
|
600
|
+
useEffect(() => {
|
|
601
|
+
logWithColor('useEffect firstImageLayerChannelCoordination');
|
|
602
|
+
setIsInteracting(true);
|
|
603
|
+
log.debug('something about channels changed');
|
|
604
|
+
|
|
605
|
+
dataManager.updateChannels(firstImageLayerChannelCoordination);
|
|
606
|
+
renderManager.setChannelMapping(dataManager.channels.colorMappings);
|
|
607
|
+
|
|
608
|
+
clearTimeout(interactionTimeoutRef.current);
|
|
609
|
+
interactionTimeoutRef.current = setTimeout(() => {
|
|
610
|
+
setIsInteracting(false);
|
|
611
|
+
}, 300);
|
|
612
|
+
}, [firstImageLayerChannelCoordination]);
|
|
613
|
+
|
|
614
|
+
// Render nothing during initialization.
|
|
615
|
+
if (!is3D || !dataManager || !renderManager) return null;
|
|
616
|
+
|
|
617
|
+
if (!renderState.shader) {
|
|
618
|
+
return (
|
|
619
|
+
<group>
|
|
620
|
+
<mesh>
|
|
621
|
+
<boxGeometry args={[1, 1, 1]} />
|
|
622
|
+
<meshBasicMaterial color="#444" wireframe />
|
|
623
|
+
</mesh>
|
|
624
|
+
<OrbitControls ref={orbitRef} />
|
|
625
|
+
</group>
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Render the volume.
|
|
630
|
+
return (
|
|
631
|
+
<group>
|
|
632
|
+
<OrbitControls
|
|
633
|
+
// ref={mainOrbitControlsRef}
|
|
634
|
+
enableDamping={false}
|
|
635
|
+
onStart={onOrbitControlsStart}
|
|
636
|
+
onEnd={onOrbitControlsEnd}
|
|
637
|
+
/>
|
|
638
|
+
<mesh ref={meshRef} scale={renderState.meshScale}>
|
|
639
|
+
<boxGeometry args={renderState.geometrySize} />
|
|
640
|
+
<shaderMaterial
|
|
641
|
+
uniforms={renderState.uniforms}
|
|
642
|
+
vertexShader={renderState.shader.vertexShader}
|
|
643
|
+
fragmentShader={renderState.shader.fragmentShader}
|
|
644
|
+
side={THREE.BackSide}
|
|
645
|
+
transparent={false}
|
|
646
|
+
glslVersion={THREE.GLSL3}
|
|
647
|
+
/>
|
|
648
|
+
</mesh>
|
|
649
|
+
</group>
|
|
650
|
+
);
|
|
651
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SpatialWrapper } from './SpatialWrapper.js';
|