@vitessce/neuroglancer 3.6.18 → 3.7.1
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/{ReactNeuroglancer-Crquekcy.js → ReactNeuroglancer-C0i-a6Cw.js} +717 -1268
- package/dist/{index-BNCfoEHv.js → index-w8xI9TWU.js} +2211 -478
- package/dist/index.js +1 -1
- package/dist-tsc/Neuroglancer.d.ts +5 -3
- package/dist-tsc/Neuroglancer.d.ts.map +1 -1
- package/dist-tsc/Neuroglancer.js +31 -72
- package/dist-tsc/NeuroglancerSubscriber.d.ts.map +1 -1
- package/dist-tsc/NeuroglancerSubscriber.js +329 -93
- package/dist-tsc/ReactNeuroglancer.d.ts +147 -2
- package/dist-tsc/ReactNeuroglancer.d.ts.map +1 -1
- package/dist-tsc/ReactNeuroglancer.js +819 -5
- package/dist-tsc/styles.d.ts.map +1 -1
- package/dist-tsc/styles.js +3 -1
- package/dist-tsc/utils.d.ts +41 -0
- package/dist-tsc/utils.d.ts.map +1 -0
- package/dist-tsc/utils.js +117 -0
- package/dist-tsc/utils.test.d.ts +2 -0
- package/dist-tsc/utils.test.d.ts.map +1 -0
- package/dist-tsc/utils.test.js +34 -0
- package/package.json +12 -12
- package/src/Neuroglancer.js +32 -91
- package/src/NeuroglancerSubscriber.js +400 -108
- package/src/ReactNeuroglancer.js +912 -6
- package/src/styles.js +3 -1
- package/src/utils.js +156 -0
- package/src/utils.test.js +44 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable no-unused-vars */
|
|
2
|
-
import React, { useCallback, useMemo } from 'react';
|
|
2
|
+
import React, { useCallback, useMemo, useRef, useEffect, useState } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
TitleInfo,
|
|
5
5
|
useCoordination,
|
|
@@ -14,53 +14,37 @@ import {
|
|
|
14
14
|
COMPONENT_COORDINATION_TYPES,
|
|
15
15
|
} from '@vitessce/constants-internal';
|
|
16
16
|
import { mergeObsSets, getCellColors, setObsSelection } from '@vitessce/sets-utils';
|
|
17
|
-
import {
|
|
17
|
+
import { NeuroglancerComp } from './Neuroglancer.js';
|
|
18
18
|
import { useStyles } from './styles.js';
|
|
19
|
+
import {
|
|
20
|
+
quaternionToEuler,
|
|
21
|
+
eulerToQuaternion,
|
|
22
|
+
valueGreaterThanEpsilon,
|
|
23
|
+
nearEq,
|
|
24
|
+
makeVitNgZoomCalibrator,
|
|
25
|
+
conjQuat,
|
|
26
|
+
multiplyQuat,
|
|
27
|
+
rad2deg,
|
|
28
|
+
deg2rad,
|
|
29
|
+
Q_Y_UP,
|
|
30
|
+
} from './utils.js';
|
|
19
31
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
function mapNeuroglancerToVitessce(projectionScale) {
|
|
27
|
-
return -Math.log2(projectionScale / NEUROGLANCER_ZOOM_BASIS);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function quaternionToEuler([x, y, z, w]) {
|
|
31
|
-
// X-axis rotation (Roll)
|
|
32
|
-
const thetaX = Math.atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y));
|
|
33
|
-
|
|
34
|
-
// Y-axis rotation (Pitch)
|
|
35
|
-
const sinp = 2 * (w * y - z * x);
|
|
36
|
-
const thetaY = Math.abs(sinp) >= 1 ? Math.sign(sinp) * (Math.PI / 2) : Math.asin(sinp);
|
|
37
|
-
|
|
38
|
-
// Convert to degrees as Vitessce expects degrees?
|
|
39
|
-
return [thetaX * (180 / Math.PI), thetaY * (180 / Math.PI)];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
function eulerToQuaternion(thetaX, thetaY) {
|
|
44
|
-
// Convert Euler angles (X, Y rotations) to quaternion
|
|
45
|
-
const halfThetaX = thetaX / 2;
|
|
46
|
-
const halfThetaY = thetaY / 2;
|
|
47
|
-
|
|
48
|
-
const sinX = Math.sin(halfThetaX);
|
|
49
|
-
const cosX = Math.cos(halfThetaX);
|
|
50
|
-
const sinY = Math.sin(halfThetaY);
|
|
51
|
-
const cosY = Math.cos(halfThetaY);
|
|
32
|
+
const VITESSCE_INTERACTION_DELAY = 50;
|
|
33
|
+
const INIT_VIT_ZOOM = -3.6;
|
|
34
|
+
const ZOOM_EPS = 1e-2;
|
|
35
|
+
const ROTATION_EPS = 1e-3;
|
|
36
|
+
const TARGET_EPS = 0.5;
|
|
37
|
+
const NG_ROT_COOLDOWN_MS = 120;
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
cosX * cosY,
|
|
58
|
-
];
|
|
59
|
-
}
|
|
39
|
+
const LAST_INTERACTION_SOURCE = {
|
|
40
|
+
vitessce: 'vitessce',
|
|
41
|
+
neuroglancer: 'neuroglancer',
|
|
42
|
+
};
|
|
60
43
|
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
44
|
+
function rgbToHex(rgb) {
|
|
45
|
+
return (typeof rgb === 'string'
|
|
46
|
+
? rgb
|
|
47
|
+
: `#${rgb.map(c => c.toString(16).padStart(2, '0')).join('')}`);
|
|
64
48
|
}
|
|
65
49
|
|
|
66
50
|
export function NeuroglancerSubscriber(props) {
|
|
@@ -86,13 +70,13 @@ export function NeuroglancerSubscriber(props) {
|
|
|
86
70
|
spatialTargetY,
|
|
87
71
|
spatialRotationX,
|
|
88
72
|
spatialRotationY,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// spatialOrbitAxis,
|
|
73
|
+
spatialRotationZ,
|
|
74
|
+
spatialRotationOrbit,
|
|
75
|
+
// spatialOrbitAxis, // always along Y-axis - not used in conversion
|
|
92
76
|
embeddingType: mapping,
|
|
77
|
+
obsSetColor: cellSetColor,
|
|
93
78
|
obsSetSelection: cellSetSelection,
|
|
94
79
|
additionalObsSets: additionalCellSets,
|
|
95
|
-
obsSetColor: cellSetColor,
|
|
96
80
|
}, {
|
|
97
81
|
setAdditionalObsSets: setAdditionalCellSets,
|
|
98
82
|
setObsSetColor: setCellSetColor,
|
|
@@ -102,13 +86,18 @@ export function NeuroglancerSubscriber(props) {
|
|
|
102
86
|
setSpatialTargetX: setTargetX,
|
|
103
87
|
setSpatialTargetY: setTargetY,
|
|
104
88
|
setSpatialRotationX: setRotationX,
|
|
105
|
-
setSpatialRotationY: setRotationY,
|
|
89
|
+
// setSpatialRotationY: setRotationY,
|
|
106
90
|
// setSpatialRotationZ: setRotationZ,
|
|
107
|
-
|
|
108
|
-
|
|
91
|
+
setSpatialRotationOrbit: setRotationOrbit,
|
|
109
92
|
setSpatialZoom: setZoom,
|
|
110
93
|
}] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.NEUROGLANCER], coordinationScopes);
|
|
111
94
|
|
|
95
|
+
|
|
96
|
+
const latestViewerStateRef = useRef(initialViewerState);
|
|
97
|
+
const initialRotationPushedRef = useRef(false);
|
|
98
|
+
|
|
99
|
+
// console.log("NG Subs Render orbit", spatialRotationX, spatialRotationY, spatialRotationOrbit);
|
|
100
|
+
|
|
112
101
|
const { classes } = useStyles();
|
|
113
102
|
|
|
114
103
|
const [{ obsSets: cellSets }] = useObsSetsData(
|
|
@@ -123,23 +112,174 @@ export function NeuroglancerSubscriber(props) {
|
|
|
123
112
|
{ obsType, embeddingType: mapping },
|
|
124
113
|
);
|
|
125
114
|
|
|
115
|
+
const ngRotPushAtRef = useRef(0);
|
|
116
|
+
const lastInteractionSource = useRef(null);
|
|
117
|
+
const applyNgUpdateTimeoutRef = useRef(null);
|
|
118
|
+
const lastNgPushOrientationRef = useRef(null);
|
|
119
|
+
const initialRenderCalibratorRef = useRef(null);
|
|
120
|
+
const translationOffsetRef = useRef([0, 0, 0]);
|
|
121
|
+
const zoomRafRef = useRef(null);
|
|
122
|
+
const lastNgQuatRef = useRef([0, 0, 0, 1]);
|
|
123
|
+
const lastNgScaleRef = useRef(null);
|
|
124
|
+
const lastVitessceRotationRef = useRef({
|
|
125
|
+
x: spatialRotationX,
|
|
126
|
+
y: spatialRotationY,
|
|
127
|
+
z: spatialRotationZ,
|
|
128
|
+
orbit: spatialRotationOrbit,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Track the last coord values we saw, and only mark "vitessce"
|
|
132
|
+
// when *those* actually change. This prevents cell set renders
|
|
133
|
+
// from spoofing the source.
|
|
134
|
+
const prevCoordsRef = useRef({
|
|
135
|
+
zoom: spatialZoom,
|
|
136
|
+
rx: spatialRotationX,
|
|
137
|
+
ry: spatialRotationY,
|
|
138
|
+
rz: spatialRotationZ,
|
|
139
|
+
orbit: spatialRotationOrbit,
|
|
140
|
+
tx: spatialTargetX,
|
|
141
|
+
ty: spatialTargetY,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const mergedCellSets = useMemo(() => mergeObsSets(
|
|
145
|
+
cellSets, additionalCellSets,
|
|
146
|
+
), [cellSets, additionalCellSets]);
|
|
147
|
+
|
|
148
|
+
const cellColors = useMemo(() => getCellColors({
|
|
149
|
+
cellSets: mergedCellSets,
|
|
150
|
+
cellSetSelection,
|
|
151
|
+
cellSetColor,
|
|
152
|
+
obsIndex,
|
|
153
|
+
theme,
|
|
154
|
+
}), [mergedCellSets, theme,
|
|
155
|
+
cellSetColor, cellSetSelection, obsIndex]);
|
|
156
|
+
|
|
157
|
+
/*
|
|
158
|
+
* handleStateUpdate - Interactions from NG to Vitessce are pushed here
|
|
159
|
+
*/
|
|
126
160
|
const handleStateUpdate = useCallback((newState) => {
|
|
161
|
+
lastInteractionSource.current = LAST_INTERACTION_SOURCE.neuroglancer;
|
|
127
162
|
const { projectionScale, projectionOrientation, position } = newState;
|
|
128
|
-
setZoom(mapNeuroglancerToVitessce(projectionScale));
|
|
129
|
-
const vitessceEularMapping = quaternionToEuler(projectionOrientation);
|
|
130
163
|
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
164
|
+
// Set the views on first mount
|
|
165
|
+
if (!initialRenderCalibratorRef.current) {
|
|
166
|
+
// wait for a real scale
|
|
167
|
+
if (!Number.isFinite(projectionScale) || projectionScale <= 0) return;
|
|
168
|
+
|
|
169
|
+
// anchor to current Vitessce zoom
|
|
170
|
+
const zRef = Number.isFinite(spatialZoom) ? spatialZoom : 0;
|
|
171
|
+
initialRenderCalibratorRef.current = makeVitNgZoomCalibrator(projectionScale, zRef);
|
|
172
|
+
|
|
173
|
+
const [px = 0, py = 0, pz = 0] = Array.isArray(position) ? position : [0, 0, 0];
|
|
174
|
+
const tX = Number.isFinite(spatialTargetX) ? spatialTargetX : 0;
|
|
175
|
+
const tY = Number.isFinite(spatialTargetY) ? spatialTargetY : 0;
|
|
176
|
+
// TODO: translation off in the first render - turn pz to 0 if z-axis needs to be avoided
|
|
177
|
+
translationOffsetRef.current = [px - tX, py - tY, pz];
|
|
178
|
+
// console.log(" translationOffsetRef.current", translationOffsetRef.current)
|
|
179
|
+
const syncedZoom = initialRenderCalibratorRef.current.vitToNgZoom(INIT_VIT_ZOOM);
|
|
180
|
+
latestViewerStateRef.current = {
|
|
181
|
+
...latestViewerStateRef.current,
|
|
182
|
+
projectionScale: syncedZoom,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (!Number.isFinite(spatialZoom) || Math.abs(spatialZoom - INIT_VIT_ZOOM) > ZOOM_EPS) {
|
|
186
|
+
setZoom(INIT_VIT_ZOOM);
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ZOOM (NG → Vitessce) — do this only after calibrator exists
|
|
192
|
+
if (Number.isFinite(projectionScale) && projectionScale > 0) {
|
|
193
|
+
const vitZoomFromNg = initialRenderCalibratorRef.current.ngToVitZoom(projectionScale);
|
|
194
|
+
const scaleChanged = lastNgScaleRef.current == null
|
|
195
|
+
|| (Math.abs(projectionScale - lastNgScaleRef.current)
|
|
196
|
+
> 1e-6 * Math.max(1, projectionScale));
|
|
197
|
+
if (scaleChanged && Number.isFinite(vitZoomFromNg)
|
|
198
|
+
&& Math.abs(vitZoomFromNg - (spatialZoom ?? 0)) > ZOOM_EPS) {
|
|
199
|
+
if (zoomRafRef.current) cancelAnimationFrame(zoomRafRef.current);
|
|
200
|
+
zoomRafRef.current = requestAnimationFrame(() => {
|
|
201
|
+
setZoom(vitZoomFromNg);
|
|
202
|
+
zoomRafRef.current = null;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// remember last NG scale
|
|
206
|
+
lastNgScaleRef.current = projectionScale;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// TRANSLATION
|
|
210
|
+
if (Array.isArray(position) && position.length >= 2) {
|
|
211
|
+
const [px, py] = position;
|
|
212
|
+
const [ox, oy] = translationOffsetRef.current;
|
|
213
|
+
const tx = px - ox; // map NG → Vitessce
|
|
214
|
+
const ty = py - oy;
|
|
215
|
+
if (Number.isFinite(tx) && Math.abs(tx - (spatialTargetX ?? tx)) > TARGET_EPS) setTargetX(tx);
|
|
216
|
+
if (Number.isFinite(ty) && Math.abs(ty - (spatialTargetY ?? ty)) > TARGET_EPS) setTargetY(ty);
|
|
217
|
+
}
|
|
218
|
+
// ROTATION — only when NG quat actually changes
|
|
219
|
+
const quatChanged = valueGreaterThanEpsilon(
|
|
220
|
+
projectionOrientation, lastNgQuatRef.current, ROTATION_EPS,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
if (quatChanged) {
|
|
224
|
+
if (applyNgUpdateTimeoutRef.current) clearTimeout(applyNgUpdateTimeoutRef.current);
|
|
225
|
+
lastNgPushOrientationRef.current = projectionOrientation;
|
|
226
|
+
|
|
227
|
+
applyNgUpdateTimeoutRef.current = setTimeout(() => {
|
|
228
|
+
// Remove the Y-up correction before converting to Euler for Vitessce
|
|
229
|
+
const qVit = multiplyQuat(conjQuat(Q_Y_UP), projectionOrientation);
|
|
230
|
+
const [pitchRad, yawRad] = quaternionToEuler(qVit); // radians
|
|
231
|
+
const currPitchRad = deg2rad(spatialRotationX ?? 0);
|
|
232
|
+
const currYawRad = deg2rad(spatialRotationOrbit ?? 0);
|
|
233
|
+
|
|
234
|
+
if (Math.abs(pitchRad - currPitchRad) > ROTATION_EPS
|
|
235
|
+
|| Math.abs(yawRad - currYawRad) > ROTATION_EPS) {
|
|
236
|
+
const pitchDeg = rad2deg(pitchRad);
|
|
237
|
+
const yawDeg = rad2deg(yawRad);
|
|
238
|
+
|
|
239
|
+
// Mark Vitessce as the source for the next derived pass
|
|
240
|
+
lastInteractionSource.current = LAST_INTERACTION_SOURCE.vitessce;
|
|
241
|
+
setRotationX(pitchDeg);
|
|
242
|
+
setRotationOrbit(yawDeg);
|
|
243
|
+
ngRotPushAtRef.current = performance.now();
|
|
244
|
+
|
|
245
|
+
// // Test to verify rotation from NG to Vitessce and back to NG
|
|
246
|
+
// requestAnimationFrame(() => {
|
|
247
|
+
// requestAnimationFrame(() => {
|
|
248
|
+
// // Recreate the Vitessce quaternion from the angles we *just set*
|
|
249
|
+
// const qVitJustSet = eulerToQuaternion(deg2rad(pitchDeg), deg2rad(yawDeg), 0);
|
|
250
|
+
// // Convert to NG frame (apply Y-up)
|
|
251
|
+
// const qNgExpected = multiplyQuat(Q_Y_UP, qVitJustSet);
|
|
252
|
+
// // What NG is currently holding (latest from ref, fallback to local)
|
|
253
|
+
// const qNgCurrent = latestViewerStateRef.current?.projectionOrientation
|
|
254
|
+
// || projectionOrientation;
|
|
255
|
+
|
|
256
|
+
// const dot = quatdotAbs(qNgExpected, qNgCurrent);
|
|
257
|
+
// console.log('[POST-APPLY] |dot| =', dot.toFixed(6));
|
|
258
|
+
// });
|
|
259
|
+
// });
|
|
260
|
+
}
|
|
261
|
+
}, VITESSCE_INTERACTION_DELAY);
|
|
262
|
+
|
|
263
|
+
lastNgQuatRef.current = projectionOrientation;
|
|
264
|
+
}
|
|
134
265
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
266
|
+
latestViewerStateRef.current = {
|
|
267
|
+
...latestViewerStateRef.current,
|
|
268
|
+
projectionOrientation,
|
|
269
|
+
projectionScale,
|
|
270
|
+
position,
|
|
271
|
+
};
|
|
272
|
+
}, []);
|
|
139
273
|
|
|
140
274
|
const onSegmentClick = useCallback((value) => {
|
|
141
275
|
if (value) {
|
|
142
|
-
const
|
|
276
|
+
const id = String(value);
|
|
277
|
+
const selectedCellIds = [id];
|
|
278
|
+
const alreadySelectedId = cellSetSelection?.flat()?.some(sel => sel.includes(id));
|
|
279
|
+
// Don't create new selection from same ids
|
|
280
|
+
if (alreadySelectedId) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
143
283
|
setObsSelection(
|
|
144
284
|
selectedCellIds, additionalCellSets, cellSetColor,
|
|
145
285
|
setCellSetSelection, setAdditionalCellSets, setCellSetColor,
|
|
@@ -152,63 +292,214 @@ export function NeuroglancerSubscriber(props) {
|
|
|
152
292
|
setCellColorEncoding, setCellSetColor, setCellSetSelection,
|
|
153
293
|
]);
|
|
154
294
|
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
), [cellSets, additionalCellSets]);
|
|
158
|
-
|
|
159
|
-
const cellColors = useMemo(() => getCellColors({
|
|
160
|
-
cellSets: mergedCellSets,
|
|
161
|
-
cellSetSelection,
|
|
162
|
-
cellSetColor,
|
|
163
|
-
obsIndex,
|
|
164
|
-
theme,
|
|
165
|
-
}), [mergedCellSets, theme,
|
|
166
|
-
cellSetColor, cellSetSelection, obsIndex]);
|
|
295
|
+
const batchedUpdateTimeoutRef = useRef(null);
|
|
296
|
+
const [batchedCellColors, setBatchedCellColors] = useState(cellColors);
|
|
167
297
|
|
|
168
|
-
|
|
169
|
-
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
if (batchedUpdateTimeoutRef.current) {
|
|
300
|
+
clearTimeout(batchedUpdateTimeoutRef.current);
|
|
301
|
+
}
|
|
302
|
+
batchedUpdateTimeoutRef.current = setTimeout(() => {
|
|
303
|
+
setBatchedCellColors(cellColors);
|
|
304
|
+
}, 100);
|
|
170
305
|
|
|
306
|
+
// TODO: look into deferredValue from React
|
|
307
|
+
// startTransition(() => {
|
|
308
|
+
// setBatchedCellColors(cellColors);
|
|
309
|
+
// });
|
|
310
|
+
}, [cellColors]);
|
|
311
|
+
// TODO use a ref if slow - see prev commits
|
|
171
312
|
const cellColorMapping = useMemo(() => {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
313
|
+
const colorMapping = {};
|
|
314
|
+
batchedCellColors.forEach((color, cell) => {
|
|
315
|
+
colorMapping[cell] = rgbToHex(color);
|
|
175
316
|
});
|
|
176
|
-
return
|
|
177
|
-
}, [
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
317
|
+
return colorMapping;
|
|
318
|
+
}, [batchedCellColors]);
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
const derivedViewerState = useMemo(() => {
|
|
322
|
+
const { current } = latestViewerStateRef;
|
|
323
|
+
const nextSegments = Object.keys(cellColorMapping);
|
|
324
|
+
const prevLayer = current?.layers?.[0] || {};
|
|
325
|
+
const prevSegments = prevLayer.segments || [];
|
|
326
|
+
const { projectionScale, projectionOrientation, position } = current;
|
|
327
|
+
|
|
328
|
+
// Did Vitessce coords change vs the *previous* render?
|
|
329
|
+
const rotChangedNow = !nearEq(spatialRotationX, prevCoordsRef.current.rx, ROTATION_EPS)
|
|
330
|
+
|| !nearEq(spatialRotationY, prevCoordsRef.current.ry, ROTATION_EPS)
|
|
331
|
+
|| !nearEq(spatialRotationZ, prevCoordsRef.current.rz, ROTATION_EPS)
|
|
332
|
+
|| !nearEq(spatialRotationOrbit, prevCoordsRef.current.orbit, ROTATION_EPS);
|
|
333
|
+
|
|
334
|
+
const zoomChangedNow = !nearEq(spatialZoom, prevCoordsRef.current.zoom, ROTATION_EPS);
|
|
335
|
+
|
|
336
|
+
const transChangedNow = !nearEq(spatialTargetX, prevCoordsRef.current.tx, ROTATION_EPS)
|
|
337
|
+
|| !nearEq(spatialTargetY, prevCoordsRef.current.ty, ROTATION_EPS);
|
|
338
|
+
|
|
339
|
+
let nextProjectionScale = projectionScale;
|
|
340
|
+
let nextPosition = position;
|
|
341
|
+
|
|
342
|
+
// ** --- Zoom handling --- ** //
|
|
343
|
+
if (typeof spatialZoom === 'number'
|
|
344
|
+
&& initialRenderCalibratorRef.current
|
|
345
|
+
&& lastInteractionSource.current !== LAST_INTERACTION_SOURCE.neuroglancer
|
|
346
|
+
&& zoomChangedNow) {
|
|
347
|
+
const s = initialRenderCalibratorRef.current.vitToNgZoom(spatialZoom);
|
|
348
|
+
if (Number.isFinite(s) && s > 0) {
|
|
349
|
+
nextProjectionScale = s;
|
|
186
350
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ** --- Translation handling --- ** //
|
|
354
|
+
const [ox, oy, oz] = translationOffsetRef.current;
|
|
355
|
+
const [px = 0, py = 0, pz = (current.position?.[2] ?? oz)] = current.position || [];
|
|
356
|
+
const hasVitessceSpatialTarget = Number.isFinite(spatialTargetX)
|
|
357
|
+
&& Number.isFinite(spatialTargetY);
|
|
358
|
+
if (hasVitessceSpatialTarget
|
|
359
|
+
&& lastInteractionSource.current !== LAST_INTERACTION_SOURCE.neuroglancer
|
|
360
|
+
&& transChangedNow) {
|
|
361
|
+
const nx = spatialTargetX + ox; // Vitessce → NG
|
|
362
|
+
const ny = spatialTargetY + oy;
|
|
363
|
+
if (Math.abs(nx - px) > TARGET_EPS || Math.abs(ny - py) > TARGET_EPS) {
|
|
364
|
+
nextPosition = [nx, ny, pz];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ** --- Orientation/Rotation handling --- ** //
|
|
369
|
+
const vitessceRotationRaw = eulerToQuaternion(
|
|
370
|
+
deg2rad(spatialRotationX ?? 0),
|
|
371
|
+
deg2rad(spatialRotationOrbit ?? 0),
|
|
372
|
+
deg2rad(spatialRotationZ ?? 0),
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// Apply Y-up to have both views with same axis-direction (xy)
|
|
376
|
+
const vitessceRotation = multiplyQuat(Q_Y_UP, vitessceRotationRaw);
|
|
377
|
+
|
|
378
|
+
// // Round-trip check: NG -> Vit (remove Y-UP)
|
|
379
|
+
// const qVitBack = multiplyQuat(conjQuat(Q_Y_UP), vitessceRotation);
|
|
380
|
+
// const dotVitLoop = quatdotAbs(qVitBack, vitessceRotationRaw);
|
|
381
|
+
|
|
382
|
+
// // Expect ~1 (± sign OK)
|
|
383
|
+
// const fmt = (v) => Array.isArray(v) ? v.map(n => Number(n).toFixed(6)) : v;
|
|
384
|
+
// console.log('[CHK Vit→NG→Vit] |dot| =', dotVitLoop.toFixed(6),
|
|
385
|
+
// ' qVitRaw=', fmt(vitessceRotationRaw),
|
|
386
|
+
// ' qVitBack=', fmt(qVitBack));
|
|
387
|
+
|
|
388
|
+
// // Cross-view check: does the NG orientation we're about to send match our Vit -> NG?
|
|
389
|
+
// const dotVsNg = quatdotAbs(vitessceRotation, projectionOrientation);
|
|
390
|
+
// console.log('[CHK Vit→NG vs current NG] |dot| =', dotVsNg.toFixed(6));
|
|
391
|
+
|
|
392
|
+
// If NG quat != Vitessce quat on first render, push Vitessce once.
|
|
393
|
+
const shouldForceInitialVitPush = !initialRotationPushedRef.current
|
|
394
|
+
&& valueGreaterThanEpsilon(vitessceRotation, projectionOrientation, ROTATION_EPS);
|
|
395
|
+
|
|
396
|
+
// Use explicit source if set; otherwise infer Vitessce when coords changed.
|
|
397
|
+
const ngFresh = (performance.now() - (ngRotPushAtRef.current || 0)) < NG_ROT_COOLDOWN_MS;
|
|
398
|
+
|
|
399
|
+
const changedNowOrIInitialVitPush = rotChangedNow
|
|
400
|
+
|| zoomChangedNow || transChangedNow || shouldForceInitialVitPush;
|
|
401
|
+
|
|
402
|
+
const src = ngFresh ? LAST_INTERACTION_SOURCE.neuroglancer
|
|
403
|
+
: (lastInteractionSource.current
|
|
404
|
+
?? (changedNowOrIInitialVitPush ? LAST_INTERACTION_SOURCE.vitessce : null));
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
let nextOrientation = projectionOrientation; // start from NG's current quat
|
|
408
|
+
|
|
409
|
+
// console.log('[ORIENT]',
|
|
410
|
+
// 'srcResolved=', src,
|
|
411
|
+
// 'lastSource=', lastInteractionSource.current,
|
|
412
|
+
// 'dotLoop=', dotVitLoop.toFixed(6),
|
|
413
|
+
// 'dotCross=', dotVsNg.toFixed(6)
|
|
414
|
+
// );
|
|
415
|
+
|
|
416
|
+
// console.log('[ORIENT Q]',
|
|
417
|
+
// 'qVitRaw=', fmt(vitessceRotationRaw), // Vit frame (pre Y-up)
|
|
418
|
+
// 'qVitToNg=', fmt(vitessceRotation), // NG frame (post Y-up)
|
|
419
|
+
// 'qNgCurr=', fmt(projectionOrientation),
|
|
420
|
+
// );
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
if (src === LAST_INTERACTION_SOURCE.vitessce) {
|
|
424
|
+
// Only push if Vitessce rotation actually changed since last time.
|
|
425
|
+
const rotDiffers = valueGreaterThanEpsilon(
|
|
426
|
+
vitessceRotation,
|
|
201
427
|
projectionOrientation,
|
|
202
|
-
|
|
428
|
+
ROTATION_EPS,
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
if (rotDiffers) {
|
|
432
|
+
nextOrientation = vitessceRotation;
|
|
433
|
+
lastVitessceRotationRef.current = {
|
|
434
|
+
x: spatialRotationX,
|
|
435
|
+
y: spatialRotationY,
|
|
436
|
+
z: spatialRotationZ,
|
|
437
|
+
orbit: spatialRotationOrbit,
|
|
438
|
+
};
|
|
439
|
+
initialRotationPushedRef.current = true;
|
|
440
|
+
// Re-anchor NG -> Vitessce translation once we commit the initial orientation,
|
|
441
|
+
// the center shows a right translated image
|
|
442
|
+
const [cx = 0, cy = 0,
|
|
443
|
+
cz = (nextPosition?.[2] ?? current.position?.[2] ?? 0),
|
|
444
|
+
] = nextPosition
|
|
445
|
+
|| current.position || [];
|
|
446
|
+
const tX = Number.isFinite(spatialTargetX) ? spatialTargetX : 0;
|
|
447
|
+
const tY = Number.isFinite(spatialTargetY) ? spatialTargetY : 0;
|
|
448
|
+
translationOffsetRef.current = [cx - tX, cy - tY, cz];
|
|
449
|
+
}
|
|
450
|
+
// else {
|
|
451
|
+
// // No real Vitessce rotation change → do not overwrite NG's quat.
|
|
452
|
+
// console.log('Vitessce → NG: no rotation change, keep NG quat');
|
|
453
|
+
// }
|
|
454
|
+
if (lastInteractionSource.current === LAST_INTERACTION_SOURCE.vitessce) {
|
|
455
|
+
lastInteractionSource.current = null;
|
|
456
|
+
}
|
|
457
|
+
} else if (src === LAST_INTERACTION_SOURCE.neuroglancer) {
|
|
458
|
+
nextOrientation = lastNgPushOrientationRef.current ?? projectionOrientation;
|
|
459
|
+
lastInteractionSource.current = null;
|
|
203
460
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
461
|
+
|
|
462
|
+
const newLayer0 = {
|
|
463
|
+
...prevLayer,
|
|
464
|
+
segments: nextSegments,
|
|
465
|
+
segmentColors: cellColorMapping,
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
const updated = {
|
|
470
|
+
...current,
|
|
471
|
+
projectionScale: nextProjectionScale,
|
|
472
|
+
projectionOrientation: nextOrientation,
|
|
473
|
+
position: nextPosition,
|
|
474
|
+
layers: prevSegments.length === 0 ? [newLayer0, ...(current?.layers?.slice(1)
|
|
475
|
+
|| [])] : current?.layers,
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
latestViewerStateRef.current = updated;
|
|
479
|
+
|
|
480
|
+
prevCoordsRef.current = {
|
|
481
|
+
zoom: spatialZoom,
|
|
482
|
+
rx: spatialRotationX,
|
|
483
|
+
ry: spatialRotationY,
|
|
484
|
+
rz: spatialRotationZ,
|
|
485
|
+
orbit: spatialRotationOrbit,
|
|
486
|
+
tx: spatialTargetX,
|
|
487
|
+
ty: spatialTargetY,
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
return updated;
|
|
491
|
+
}, [cellColorMapping, spatialZoom, spatialRotationX, spatialRotationY,
|
|
492
|
+
spatialRotationZ, spatialTargetX, spatialTargetY]);
|
|
207
493
|
|
|
208
494
|
const onSegmentHighlight = useCallback((obsId) => {
|
|
209
495
|
setCellHighlight(String(obsId));
|
|
210
496
|
}, [obsIndex, setCellHighlight]);
|
|
211
497
|
|
|
498
|
+
// TODO: if all cells are deselected, a black view is shown, rather we want to show empty NG view?
|
|
499
|
+
// if (!cellColorMapping || Object.keys(cellColorMapping).length === 0) {
|
|
500
|
+
// return;
|
|
501
|
+
// }
|
|
502
|
+
|
|
212
503
|
return (
|
|
213
504
|
<TitleInfo
|
|
214
505
|
title={title}
|
|
@@ -221,11 +512,12 @@ export function NeuroglancerSubscriber(props) {
|
|
|
221
512
|
isReady
|
|
222
513
|
withPadding={false}
|
|
223
514
|
>
|
|
224
|
-
<
|
|
515
|
+
<NeuroglancerComp
|
|
225
516
|
classes={classes}
|
|
226
517
|
onSegmentClick={onSegmentClick}
|
|
227
518
|
onSelectHoveredCoords={onSegmentHighlight}
|
|
228
|
-
viewerState={
|
|
519
|
+
viewerState={derivedViewerState}
|
|
520
|
+
cellColorMapping={cellColorMapping}
|
|
229
521
|
setViewerState={handleStateUpdate}
|
|
230
522
|
/>
|
|
231
523
|
</TitleInfo>
|