@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,80 +1,70 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
/* eslint-disable no-unused-vars */
|
|
3
|
-
import React, { useCallback, useMemo } from 'react';
|
|
3
|
+
import React, { useCallback, useMemo, useRef, useEffect, useState } from 'react';
|
|
4
4
|
import { TitleInfo, useCoordination, useObsSetsData, useLoaders, useObsEmbeddingData, useCoordinationScopes, } from '@vitessce/vit-s';
|
|
5
5
|
import { ViewHelpMapping, ViewType, COMPONENT_COORDINATION_TYPES, } from '@vitessce/constants-internal';
|
|
6
6
|
import { mergeObsSets, getCellColors, setObsSelection } from '@vitessce/sets-utils';
|
|
7
|
-
import {
|
|
7
|
+
import { NeuroglancerComp } from './Neuroglancer.js';
|
|
8
8
|
import { useStyles } from './styles.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
function eulerToQuaternion(thetaX, thetaY) {
|
|
26
|
-
// Convert Euler angles (X, Y rotations) to quaternion
|
|
27
|
-
const halfThetaX = thetaX / 2;
|
|
28
|
-
const halfThetaY = thetaY / 2;
|
|
29
|
-
const sinX = Math.sin(halfThetaX);
|
|
30
|
-
const cosX = Math.cos(halfThetaX);
|
|
31
|
-
const sinY = Math.sin(halfThetaY);
|
|
32
|
-
const cosY = Math.cos(halfThetaY);
|
|
33
|
-
return [
|
|
34
|
-
sinX * cosY,
|
|
35
|
-
cosX * sinY,
|
|
36
|
-
sinX * sinY,
|
|
37
|
-
cosX * cosY,
|
|
38
|
-
];
|
|
39
|
-
}
|
|
40
|
-
function normalizeQuaternion(q) {
|
|
41
|
-
const length = Math.sqrt((q[0] ** 2) + (q[1] ** 2) + (q[2] ** 2) + (q[3] ** 2));
|
|
42
|
-
return q.map(value => value / length);
|
|
9
|
+
import { quaternionToEuler, eulerToQuaternion, valueGreaterThanEpsilon, nearEq, makeVitNgZoomCalibrator, conjQuat, multiplyQuat, rad2deg, deg2rad, Q_Y_UP, } from './utils.js';
|
|
10
|
+
const VITESSCE_INTERACTION_DELAY = 50;
|
|
11
|
+
const INIT_VIT_ZOOM = -3.6;
|
|
12
|
+
const ZOOM_EPS = 1e-2;
|
|
13
|
+
const ROTATION_EPS = 1e-3;
|
|
14
|
+
const TARGET_EPS = 0.5;
|
|
15
|
+
const NG_ROT_COOLDOWN_MS = 120;
|
|
16
|
+
const LAST_INTERACTION_SOURCE = {
|
|
17
|
+
vitessce: 'vitessce',
|
|
18
|
+
neuroglancer: 'neuroglancer',
|
|
19
|
+
};
|
|
20
|
+
function rgbToHex(rgb) {
|
|
21
|
+
return (typeof rgb === 'string'
|
|
22
|
+
? rgb
|
|
23
|
+
: `#${rgb.map(c => c.toString(16).padStart(2, '0')).join('')}`);
|
|
43
24
|
}
|
|
44
25
|
export function NeuroglancerSubscriber(props) {
|
|
45
26
|
const { coordinationScopes: coordinationScopesRaw, closeButtonVisible, downloadButtonVisible, removeGridComponent, theme, title = 'Neuroglancer', helpText = ViewHelpMapping.NEUROGLANCER, viewerState: initialViewerState, } = props;
|
|
46
27
|
const loaders = useLoaders();
|
|
47
28
|
const coordinationScopes = useCoordinationScopes(coordinationScopesRaw);
|
|
48
|
-
const [{ dataset, obsType, spatialZoom, spatialTargetX, spatialTargetY, spatialRotationX, spatialRotationY,
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
embeddingType: mapping, obsSetSelection: cellSetSelection, additionalObsSets: additionalCellSets, obsSetColor: cellSetColor, }, { setAdditionalObsSets: setAdditionalCellSets, setObsSetColor: setCellSetColor, setObsColorEncoding: setCellColorEncoding, setObsSetSelection: setCellSetSelection, setObsHighlight: setCellHighlight, setSpatialTargetX: setTargetX, setSpatialTargetY: setTargetY, setSpatialRotationX: setRotationX, setSpatialRotationY: setRotationY,
|
|
29
|
+
const [{ dataset, obsType, spatialZoom, spatialTargetX, spatialTargetY, spatialRotationX, spatialRotationY, spatialRotationZ, spatialRotationOrbit,
|
|
30
|
+
// spatialOrbitAxis, // always along Y-axis - not used in conversion
|
|
31
|
+
embeddingType: mapping, obsSetColor: cellSetColor, obsSetSelection: cellSetSelection, additionalObsSets: additionalCellSets, }, { setAdditionalObsSets: setAdditionalCellSets, setObsSetColor: setCellSetColor, setObsColorEncoding: setCellColorEncoding, setObsSetSelection: setCellSetSelection, setObsHighlight: setCellHighlight, setSpatialTargetX: setTargetX, setSpatialTargetY: setTargetY, setSpatialRotationX: setRotationX,
|
|
32
|
+
// setSpatialRotationY: setRotationY,
|
|
53
33
|
// setSpatialRotationZ: setRotationZ,
|
|
54
|
-
|
|
55
|
-
|
|
34
|
+
setSpatialRotationOrbit: setRotationOrbit, setSpatialZoom: setZoom, }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.NEUROGLANCER], coordinationScopes);
|
|
35
|
+
const latestViewerStateRef = useRef(initialViewerState);
|
|
36
|
+
const initialRotationPushedRef = useRef(false);
|
|
37
|
+
// console.log("NG Subs Render orbit", spatialRotationX, spatialRotationY, spatialRotationOrbit);
|
|
56
38
|
const { classes } = useStyles();
|
|
57
39
|
const [{ obsSets: cellSets }] = useObsSetsData(loaders, dataset, false, { setObsSetSelection: setCellSetSelection, setObsSetColor: setCellSetColor }, { cellSetSelection, obsSetColor: cellSetColor }, { obsType });
|
|
58
40
|
const [{ obsIndex }] = useObsEmbeddingData(loaders, dataset, true, {}, {}, { obsType, embeddingType: mapping });
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
41
|
+
const ngRotPushAtRef = useRef(0);
|
|
42
|
+
const lastInteractionSource = useRef(null);
|
|
43
|
+
const applyNgUpdateTimeoutRef = useRef(null);
|
|
44
|
+
const lastNgPushOrientationRef = useRef(null);
|
|
45
|
+
const initialRenderCalibratorRef = useRef(null);
|
|
46
|
+
const translationOffsetRef = useRef([0, 0, 0]);
|
|
47
|
+
const zoomRafRef = useRef(null);
|
|
48
|
+
const lastNgQuatRef = useRef([0, 0, 0, 1]);
|
|
49
|
+
const lastNgScaleRef = useRef(null);
|
|
50
|
+
const lastVitessceRotationRef = useRef({
|
|
51
|
+
x: spatialRotationX,
|
|
52
|
+
y: spatialRotationY,
|
|
53
|
+
z: spatialRotationZ,
|
|
54
|
+
orbit: spatialRotationOrbit,
|
|
55
|
+
});
|
|
56
|
+
// Track the last coord values we saw, and only mark "vitessce"
|
|
57
|
+
// when *those* actually change. This prevents cell set renders
|
|
58
|
+
// from spoofing the source.
|
|
59
|
+
const prevCoordsRef = useRef({
|
|
60
|
+
zoom: spatialZoom,
|
|
61
|
+
rx: spatialRotationX,
|
|
62
|
+
ry: spatialRotationY,
|
|
63
|
+
rz: spatialRotationZ,
|
|
64
|
+
orbit: spatialRotationOrbit,
|
|
65
|
+
tx: spatialTargetX,
|
|
66
|
+
ty: spatialTargetY,
|
|
67
|
+
});
|
|
78
68
|
const mergedCellSets = useMemo(() => mergeObsSets(cellSets, additionalCellSets), [cellSets, additionalCellSets]);
|
|
79
69
|
const cellColors = useMemo(() => getCellColors({
|
|
80
70
|
cellSets: mergedCellSets,
|
|
@@ -84,42 +74,288 @@ export function NeuroglancerSubscriber(props) {
|
|
|
84
74
|
theme,
|
|
85
75
|
}), [mergedCellSets, theme,
|
|
86
76
|
cellSetColor, cellSetSelection, obsIndex]);
|
|
87
|
-
|
|
88
|
-
|
|
77
|
+
/*
|
|
78
|
+
* handleStateUpdate - Interactions from NG to Vitessce are pushed here
|
|
79
|
+
*/
|
|
80
|
+
const handleStateUpdate = useCallback((newState) => {
|
|
81
|
+
lastInteractionSource.current = LAST_INTERACTION_SOURCE.neuroglancer;
|
|
82
|
+
const { projectionScale, projectionOrientation, position } = newState;
|
|
83
|
+
// Set the views on first mount
|
|
84
|
+
if (!initialRenderCalibratorRef.current) {
|
|
85
|
+
// wait for a real scale
|
|
86
|
+
if (!Number.isFinite(projectionScale) || projectionScale <= 0)
|
|
87
|
+
return;
|
|
88
|
+
// anchor to current Vitessce zoom
|
|
89
|
+
const zRef = Number.isFinite(spatialZoom) ? spatialZoom : 0;
|
|
90
|
+
initialRenderCalibratorRef.current = makeVitNgZoomCalibrator(projectionScale, zRef);
|
|
91
|
+
const [px = 0, py = 0, pz = 0] = Array.isArray(position) ? position : [0, 0, 0];
|
|
92
|
+
const tX = Number.isFinite(spatialTargetX) ? spatialTargetX : 0;
|
|
93
|
+
const tY = Number.isFinite(spatialTargetY) ? spatialTargetY : 0;
|
|
94
|
+
// TODO: translation off in the first render - turn pz to 0 if z-axis needs to be avoided
|
|
95
|
+
translationOffsetRef.current = [px - tX, py - tY, pz];
|
|
96
|
+
// console.log(" translationOffsetRef.current", translationOffsetRef.current)
|
|
97
|
+
const syncedZoom = initialRenderCalibratorRef.current.vitToNgZoom(INIT_VIT_ZOOM);
|
|
98
|
+
latestViewerStateRef.current = {
|
|
99
|
+
...latestViewerStateRef.current,
|
|
100
|
+
projectionScale: syncedZoom,
|
|
101
|
+
};
|
|
102
|
+
if (!Number.isFinite(spatialZoom) || Math.abs(spatialZoom - INIT_VIT_ZOOM) > ZOOM_EPS) {
|
|
103
|
+
setZoom(INIT_VIT_ZOOM);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// ZOOM (NG → Vitessce) — do this only after calibrator exists
|
|
108
|
+
if (Number.isFinite(projectionScale) && projectionScale > 0) {
|
|
109
|
+
const vitZoomFromNg = initialRenderCalibratorRef.current.ngToVitZoom(projectionScale);
|
|
110
|
+
const scaleChanged = lastNgScaleRef.current == null
|
|
111
|
+
|| (Math.abs(projectionScale - lastNgScaleRef.current)
|
|
112
|
+
> 1e-6 * Math.max(1, projectionScale));
|
|
113
|
+
if (scaleChanged && Number.isFinite(vitZoomFromNg)
|
|
114
|
+
&& Math.abs(vitZoomFromNg - (spatialZoom ?? 0)) > ZOOM_EPS) {
|
|
115
|
+
if (zoomRafRef.current)
|
|
116
|
+
cancelAnimationFrame(zoomRafRef.current);
|
|
117
|
+
zoomRafRef.current = requestAnimationFrame(() => {
|
|
118
|
+
setZoom(vitZoomFromNg);
|
|
119
|
+
zoomRafRef.current = null;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// remember last NG scale
|
|
123
|
+
lastNgScaleRef.current = projectionScale;
|
|
124
|
+
}
|
|
125
|
+
// TRANSLATION
|
|
126
|
+
if (Array.isArray(position) && position.length >= 2) {
|
|
127
|
+
const [px, py] = position;
|
|
128
|
+
const [ox, oy] = translationOffsetRef.current;
|
|
129
|
+
const tx = px - ox; // map NG → Vitessce
|
|
130
|
+
const ty = py - oy;
|
|
131
|
+
if (Number.isFinite(tx) && Math.abs(tx - (spatialTargetX ?? tx)) > TARGET_EPS)
|
|
132
|
+
setTargetX(tx);
|
|
133
|
+
if (Number.isFinite(ty) && Math.abs(ty - (spatialTargetY ?? ty)) > TARGET_EPS)
|
|
134
|
+
setTargetY(ty);
|
|
135
|
+
}
|
|
136
|
+
// ROTATION — only when NG quat actually changes
|
|
137
|
+
const quatChanged = valueGreaterThanEpsilon(projectionOrientation, lastNgQuatRef.current, ROTATION_EPS);
|
|
138
|
+
if (quatChanged) {
|
|
139
|
+
if (applyNgUpdateTimeoutRef.current)
|
|
140
|
+
clearTimeout(applyNgUpdateTimeoutRef.current);
|
|
141
|
+
lastNgPushOrientationRef.current = projectionOrientation;
|
|
142
|
+
applyNgUpdateTimeoutRef.current = setTimeout(() => {
|
|
143
|
+
// Remove the Y-up correction before converting to Euler for Vitessce
|
|
144
|
+
const qVit = multiplyQuat(conjQuat(Q_Y_UP), projectionOrientation);
|
|
145
|
+
const [pitchRad, yawRad] = quaternionToEuler(qVit); // radians
|
|
146
|
+
const currPitchRad = deg2rad(spatialRotationX ?? 0);
|
|
147
|
+
const currYawRad = deg2rad(spatialRotationOrbit ?? 0);
|
|
148
|
+
if (Math.abs(pitchRad - currPitchRad) > ROTATION_EPS
|
|
149
|
+
|| Math.abs(yawRad - currYawRad) > ROTATION_EPS) {
|
|
150
|
+
const pitchDeg = rad2deg(pitchRad);
|
|
151
|
+
const yawDeg = rad2deg(yawRad);
|
|
152
|
+
// Mark Vitessce as the source for the next derived pass
|
|
153
|
+
lastInteractionSource.current = LAST_INTERACTION_SOURCE.vitessce;
|
|
154
|
+
setRotationX(pitchDeg);
|
|
155
|
+
setRotationOrbit(yawDeg);
|
|
156
|
+
ngRotPushAtRef.current = performance.now();
|
|
157
|
+
// // Test to verify rotation from NG to Vitessce and back to NG
|
|
158
|
+
// requestAnimationFrame(() => {
|
|
159
|
+
// requestAnimationFrame(() => {
|
|
160
|
+
// // Recreate the Vitessce quaternion from the angles we *just set*
|
|
161
|
+
// const qVitJustSet = eulerToQuaternion(deg2rad(pitchDeg), deg2rad(yawDeg), 0);
|
|
162
|
+
// // Convert to NG frame (apply Y-up)
|
|
163
|
+
// const qNgExpected = multiplyQuat(Q_Y_UP, qVitJustSet);
|
|
164
|
+
// // What NG is currently holding (latest from ref, fallback to local)
|
|
165
|
+
// const qNgCurrent = latestViewerStateRef.current?.projectionOrientation
|
|
166
|
+
// || projectionOrientation;
|
|
167
|
+
// const dot = quatdotAbs(qNgExpected, qNgCurrent);
|
|
168
|
+
// console.log('[POST-APPLY] |dot| =', dot.toFixed(6));
|
|
169
|
+
// });
|
|
170
|
+
// });
|
|
171
|
+
}
|
|
172
|
+
}, VITESSCE_INTERACTION_DELAY);
|
|
173
|
+
lastNgQuatRef.current = projectionOrientation;
|
|
174
|
+
}
|
|
175
|
+
latestViewerStateRef.current = {
|
|
176
|
+
...latestViewerStateRef.current,
|
|
177
|
+
projectionOrientation,
|
|
178
|
+
projectionScale,
|
|
179
|
+
position,
|
|
180
|
+
};
|
|
181
|
+
}, []);
|
|
182
|
+
const onSegmentClick = useCallback((value) => {
|
|
183
|
+
if (value) {
|
|
184
|
+
const id = String(value);
|
|
185
|
+
const selectedCellIds = [id];
|
|
186
|
+
const alreadySelectedId = cellSetSelection?.flat()?.some(sel => sel.includes(id));
|
|
187
|
+
// Don't create new selection from same ids
|
|
188
|
+
if (alreadySelectedId) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
setObsSelection(selectedCellIds, additionalCellSets, cellSetColor, setCellSetSelection, setAdditionalCellSets, setCellSetColor, setCellColorEncoding, 'Selection ', `: based on selected segments ${value}`);
|
|
192
|
+
}
|
|
193
|
+
}, [additionalCellSets, cellSetColor, setAdditionalCellSets,
|
|
194
|
+
setCellColorEncoding, setCellSetColor, setCellSetSelection,
|
|
195
|
+
]);
|
|
196
|
+
const batchedUpdateTimeoutRef = useRef(null);
|
|
197
|
+
const [batchedCellColors, setBatchedCellColors] = useState(cellColors);
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (batchedUpdateTimeoutRef.current) {
|
|
200
|
+
clearTimeout(batchedUpdateTimeoutRef.current);
|
|
201
|
+
}
|
|
202
|
+
batchedUpdateTimeoutRef.current = setTimeout(() => {
|
|
203
|
+
setBatchedCellColors(cellColors);
|
|
204
|
+
}, 100);
|
|
205
|
+
// TODO: look into deferredValue from React
|
|
206
|
+
// startTransition(() => {
|
|
207
|
+
// setBatchedCellColors(cellColors);
|
|
208
|
+
// });
|
|
209
|
+
}, [cellColors]);
|
|
210
|
+
// TODO use a ref if slow - see prev commits
|
|
89
211
|
const cellColorMapping = useMemo(() => {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
212
|
+
const colorMapping = {};
|
|
213
|
+
batchedCellColors.forEach((color, cell) => {
|
|
214
|
+
colorMapping[cell] = rgbToHex(color);
|
|
93
215
|
});
|
|
94
|
-
return
|
|
95
|
-
}, [
|
|
96
|
-
const derivedViewerState = useMemo(() =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
216
|
+
return colorMapping;
|
|
217
|
+
}, [batchedCellColors]);
|
|
218
|
+
const derivedViewerState = useMemo(() => {
|
|
219
|
+
const { current } = latestViewerStateRef;
|
|
220
|
+
const nextSegments = Object.keys(cellColorMapping);
|
|
221
|
+
const prevLayer = current?.layers?.[0] || {};
|
|
222
|
+
const prevSegments = prevLayer.segments || [];
|
|
223
|
+
const { projectionScale, projectionOrientation, position } = current;
|
|
224
|
+
// Did Vitessce coords change vs the *previous* render?
|
|
225
|
+
const rotChangedNow = !nearEq(spatialRotationX, prevCoordsRef.current.rx, ROTATION_EPS)
|
|
226
|
+
|| !nearEq(spatialRotationY, prevCoordsRef.current.ry, ROTATION_EPS)
|
|
227
|
+
|| !nearEq(spatialRotationZ, prevCoordsRef.current.rz, ROTATION_EPS)
|
|
228
|
+
|| !nearEq(spatialRotationOrbit, prevCoordsRef.current.orbit, ROTATION_EPS);
|
|
229
|
+
const zoomChangedNow = !nearEq(spatialZoom, prevCoordsRef.current.zoom, ROTATION_EPS);
|
|
230
|
+
const transChangedNow = !nearEq(spatialTargetX, prevCoordsRef.current.tx, ROTATION_EPS)
|
|
231
|
+
|| !nearEq(spatialTargetY, prevCoordsRef.current.ty, ROTATION_EPS);
|
|
232
|
+
let nextProjectionScale = projectionScale;
|
|
233
|
+
let nextPosition = position;
|
|
234
|
+
// ** --- Zoom handling --- ** //
|
|
235
|
+
if (typeof spatialZoom === 'number'
|
|
236
|
+
&& initialRenderCalibratorRef.current
|
|
237
|
+
&& lastInteractionSource.current !== LAST_INTERACTION_SOURCE.neuroglancer
|
|
238
|
+
&& zoomChangedNow) {
|
|
239
|
+
const s = initialRenderCalibratorRef.current.vitToNgZoom(spatialZoom);
|
|
240
|
+
if (Number.isFinite(s) && s > 0) {
|
|
241
|
+
nextProjectionScale = s;
|
|
103
242
|
}
|
|
104
|
-
: layer)),
|
|
105
|
-
}), [cellColorMapping, initialViewerState]);
|
|
106
|
-
const derivedViewerState2 = useMemo(() => {
|
|
107
|
-
if (typeof spatialZoom === 'number' && typeof spatialTargetX === 'number') {
|
|
108
|
-
const projectionScale = mapVitessceToNeuroglancer(spatialZoom);
|
|
109
|
-
const position = [spatialTargetX, spatialTargetY, derivedViewerState.position[2]];
|
|
110
|
-
const projectionOrientation = normalizeQuaternion(eulerToQuaternion(spatialRotationX, spatialRotationY));
|
|
111
|
-
return {
|
|
112
|
-
...derivedViewerState,
|
|
113
|
-
projectionScale,
|
|
114
|
-
position,
|
|
115
|
-
projectionOrientation,
|
|
116
|
-
};
|
|
117
243
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
244
|
+
// ** --- Translation handling --- ** //
|
|
245
|
+
const [ox, oy, oz] = translationOffsetRef.current;
|
|
246
|
+
const [px = 0, py = 0, pz = (current.position?.[2] ?? oz)] = current.position || [];
|
|
247
|
+
const hasVitessceSpatialTarget = Number.isFinite(spatialTargetX)
|
|
248
|
+
&& Number.isFinite(spatialTargetY);
|
|
249
|
+
if (hasVitessceSpatialTarget
|
|
250
|
+
&& lastInteractionSource.current !== LAST_INTERACTION_SOURCE.neuroglancer
|
|
251
|
+
&& transChangedNow) {
|
|
252
|
+
const nx = spatialTargetX + ox; // Vitessce → NG
|
|
253
|
+
const ny = spatialTargetY + oy;
|
|
254
|
+
if (Math.abs(nx - px) > TARGET_EPS || Math.abs(ny - py) > TARGET_EPS) {
|
|
255
|
+
nextPosition = [nx, ny, pz];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// ** --- Orientation/Rotation handling --- ** //
|
|
259
|
+
const vitessceRotationRaw = eulerToQuaternion(deg2rad(spatialRotationX ?? 0), deg2rad(spatialRotationOrbit ?? 0), deg2rad(spatialRotationZ ?? 0));
|
|
260
|
+
// Apply Y-up to have both views with same axis-direction (xy)
|
|
261
|
+
const vitessceRotation = multiplyQuat(Q_Y_UP, vitessceRotationRaw);
|
|
262
|
+
// // Round-trip check: NG -> Vit (remove Y-UP)
|
|
263
|
+
// const qVitBack = multiplyQuat(conjQuat(Q_Y_UP), vitessceRotation);
|
|
264
|
+
// const dotVitLoop = quatdotAbs(qVitBack, vitessceRotationRaw);
|
|
265
|
+
// // Expect ~1 (± sign OK)
|
|
266
|
+
// const fmt = (v) => Array.isArray(v) ? v.map(n => Number(n).toFixed(6)) : v;
|
|
267
|
+
// console.log('[CHK Vit→NG→Vit] |dot| =', dotVitLoop.toFixed(6),
|
|
268
|
+
// ' qVitRaw=', fmt(vitessceRotationRaw),
|
|
269
|
+
// ' qVitBack=', fmt(qVitBack));
|
|
270
|
+
// // Cross-view check: does the NG orientation we're about to send match our Vit -> NG?
|
|
271
|
+
// const dotVsNg = quatdotAbs(vitessceRotation, projectionOrientation);
|
|
272
|
+
// console.log('[CHK Vit→NG vs current NG] |dot| =', dotVsNg.toFixed(6));
|
|
273
|
+
// If NG quat != Vitessce quat on first render, push Vitessce once.
|
|
274
|
+
const shouldForceInitialVitPush = !initialRotationPushedRef.current
|
|
275
|
+
&& valueGreaterThanEpsilon(vitessceRotation, projectionOrientation, ROTATION_EPS);
|
|
276
|
+
// Use explicit source if set; otherwise infer Vitessce when coords changed.
|
|
277
|
+
const ngFresh = (performance.now() - (ngRotPushAtRef.current || 0)) < NG_ROT_COOLDOWN_MS;
|
|
278
|
+
const changedNowOrIInitialVitPush = rotChangedNow
|
|
279
|
+
|| zoomChangedNow || transChangedNow || shouldForceInitialVitPush;
|
|
280
|
+
const src = ngFresh ? LAST_INTERACTION_SOURCE.neuroglancer
|
|
281
|
+
: (lastInteractionSource.current
|
|
282
|
+
?? (changedNowOrIInitialVitPush ? LAST_INTERACTION_SOURCE.vitessce : null));
|
|
283
|
+
let nextOrientation = projectionOrientation; // start from NG's current quat
|
|
284
|
+
// console.log('[ORIENT]',
|
|
285
|
+
// 'srcResolved=', src,
|
|
286
|
+
// 'lastSource=', lastInteractionSource.current,
|
|
287
|
+
// 'dotLoop=', dotVitLoop.toFixed(6),
|
|
288
|
+
// 'dotCross=', dotVsNg.toFixed(6)
|
|
289
|
+
// );
|
|
290
|
+
// console.log('[ORIENT Q]',
|
|
291
|
+
// 'qVitRaw=', fmt(vitessceRotationRaw), // Vit frame (pre Y-up)
|
|
292
|
+
// 'qVitToNg=', fmt(vitessceRotation), // NG frame (post Y-up)
|
|
293
|
+
// 'qNgCurr=', fmt(projectionOrientation),
|
|
294
|
+
// );
|
|
295
|
+
if (src === LAST_INTERACTION_SOURCE.vitessce) {
|
|
296
|
+
// Only push if Vitessce rotation actually changed since last time.
|
|
297
|
+
const rotDiffers = valueGreaterThanEpsilon(vitessceRotation, projectionOrientation, ROTATION_EPS);
|
|
298
|
+
if (rotDiffers) {
|
|
299
|
+
nextOrientation = vitessceRotation;
|
|
300
|
+
lastVitessceRotationRef.current = {
|
|
301
|
+
x: spatialRotationX,
|
|
302
|
+
y: spatialRotationY,
|
|
303
|
+
z: spatialRotationZ,
|
|
304
|
+
orbit: spatialRotationOrbit,
|
|
305
|
+
};
|
|
306
|
+
initialRotationPushedRef.current = true;
|
|
307
|
+
// Re-anchor NG -> Vitessce translation once we commit the initial orientation,
|
|
308
|
+
// the center shows a right translated image
|
|
309
|
+
const [cx = 0, cy = 0, cz = (nextPosition?.[2] ?? current.position?.[2] ?? 0),] = nextPosition
|
|
310
|
+
|| current.position || [];
|
|
311
|
+
const tX = Number.isFinite(spatialTargetX) ? spatialTargetX : 0;
|
|
312
|
+
const tY = Number.isFinite(spatialTargetY) ? spatialTargetY : 0;
|
|
313
|
+
translationOffsetRef.current = [cx - tX, cy - tY, cz];
|
|
314
|
+
}
|
|
315
|
+
// else {
|
|
316
|
+
// // No real Vitessce rotation change → do not overwrite NG's quat.
|
|
317
|
+
// console.log('Vitessce → NG: no rotation change, keep NG quat');
|
|
318
|
+
// }
|
|
319
|
+
if (lastInteractionSource.current === LAST_INTERACTION_SOURCE.vitessce) {
|
|
320
|
+
lastInteractionSource.current = null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else if (src === LAST_INTERACTION_SOURCE.neuroglancer) {
|
|
324
|
+
nextOrientation = lastNgPushOrientationRef.current ?? projectionOrientation;
|
|
325
|
+
lastInteractionSource.current = null;
|
|
326
|
+
}
|
|
327
|
+
const newLayer0 = {
|
|
328
|
+
...prevLayer,
|
|
329
|
+
segments: nextSegments,
|
|
330
|
+
segmentColors: cellColorMapping,
|
|
331
|
+
};
|
|
332
|
+
const updated = {
|
|
333
|
+
...current,
|
|
334
|
+
projectionScale: nextProjectionScale,
|
|
335
|
+
projectionOrientation: nextOrientation,
|
|
336
|
+
position: nextPosition,
|
|
337
|
+
layers: prevSegments.length === 0 ? [newLayer0, ...(current?.layers?.slice(1)
|
|
338
|
+
|| [])] : current?.layers,
|
|
339
|
+
};
|
|
340
|
+
latestViewerStateRef.current = updated;
|
|
341
|
+
prevCoordsRef.current = {
|
|
342
|
+
zoom: spatialZoom,
|
|
343
|
+
rx: spatialRotationX,
|
|
344
|
+
ry: spatialRotationY,
|
|
345
|
+
rz: spatialRotationZ,
|
|
346
|
+
orbit: spatialRotationOrbit,
|
|
347
|
+
tx: spatialTargetX,
|
|
348
|
+
ty: spatialTargetY,
|
|
349
|
+
};
|
|
350
|
+
return updated;
|
|
351
|
+
}, [cellColorMapping, spatialZoom, spatialRotationX, spatialRotationY,
|
|
352
|
+
spatialRotationZ, spatialTargetX, spatialTargetY]);
|
|
121
353
|
const onSegmentHighlight = useCallback((obsId) => {
|
|
122
354
|
setCellHighlight(String(obsId));
|
|
123
355
|
}, [obsIndex, setCellHighlight]);
|
|
124
|
-
|
|
356
|
+
// TODO: if all cells are deselected, a black view is shown, rather we want to show empty NG view?
|
|
357
|
+
// if (!cellColorMapping || Object.keys(cellColorMapping).length === 0) {
|
|
358
|
+
// return;
|
|
359
|
+
// }
|
|
360
|
+
return (_jsx(TitleInfo, { title: title, helpText: helpText, isSpatial: true, theme: theme, closeButtonVisible: closeButtonVisible, downloadButtonVisible: downloadButtonVisible, removeGridComponent: removeGridComponent, isReady: true, withPadding: false, children: _jsx(NeuroglancerComp, { classes: classes, onSegmentClick: onSegmentClick, onSelectHoveredCoords: onSegmentHighlight, viewerState: derivedViewerState, cellColorMapping: cellColorMapping, setViewerState: handleStateUpdate }) }));
|
|
125
361
|
}
|
|
@@ -1,3 +1,148 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} NgProps
|
|
3
|
+
* @property {number} perspectiveZoom
|
|
4
|
+
* @property {Object} viewerState
|
|
5
|
+
* @property {string} brainMapsClientId
|
|
6
|
+
* @property {string} key
|
|
7
|
+
* @property {Object<string, string>} cellColorMapping
|
|
8
|
+
*
|
|
9
|
+
* @property {Array} eventBindingsToUpdate
|
|
10
|
+
* An array of event bindings to change in Neuroglancer. The array format is as follows:
|
|
11
|
+
* [[old-event1, new-event1], [old-event2], old-event3]
|
|
12
|
+
* Here, `old-event1`'s will be unbound and its action will be re-bound to `new-event1`.
|
|
13
|
+
* The bindings for `old-event2` and `old-event3` will be removed.
|
|
14
|
+
* Neuroglancer has its own syntax for event descriptors, and here are some examples:
|
|
15
|
+
* 'keya', 'shift+keyb' 'control+keyc', 'digit4', 'space', 'arrowleft', 'comma', 'period',
|
|
16
|
+
* 'minus', 'equal', 'bracketleft'.
|
|
17
|
+
*
|
|
18
|
+
* @property {(segment:any|null, layer:any) => void} onSelectedChanged
|
|
19
|
+
* A function of the form `(segment, layer) => {}`, called each time there is a change to
|
|
20
|
+
* the segment the user has 'selected' (i.e., hovered over) in Neuroglancer.
|
|
21
|
+
* The `segment` argument will be a Neuroglancer `Uint64` with the ID of the now-selected
|
|
22
|
+
* segment, or `null` if no segment is now selected.
|
|
23
|
+
* The `layer` argument will be a Neuroglaner `ManagedUserLayer`, whose `layer` property
|
|
24
|
+
* will be a Neuroglancer `SegmentationUserLayer`.
|
|
25
|
+
*
|
|
26
|
+
* @property {(segments:any, layer:any) => void} onVisibleChanged
|
|
27
|
+
* A function of the form `(segments, layer) => {}`, called each time there is a change to
|
|
28
|
+
* the segments the user has designated as 'visible' (i.e., double-clicked on) in Neuroglancer.
|
|
29
|
+
* The `segments` argument will be a Neuroglancer `Uint64Set` whose elements are `Uint64`
|
|
30
|
+
* instances for the IDs of the now-visible segments.
|
|
31
|
+
* The `layer` argument will be a Neuroglaner `ManagedUserLayer`, whose `layer` property
|
|
32
|
+
* will be a Neuroglancer `SegmentationUserLayer`.
|
|
33
|
+
*
|
|
34
|
+
* @property {() => void} onSelectionDetailsStateChanged
|
|
35
|
+
* A function of the form `() => {}` to respond to selection changes in the viewer.
|
|
36
|
+
* @property {() => void} onViewerStateChanged
|
|
37
|
+
*
|
|
38
|
+
* @property {Array<Object>} callbacks
|
|
39
|
+
* // ngServer: string,
|
|
40
|
+
*/
|
|
41
|
+
export function parseUrlHash(url: any): any;
|
|
42
|
+
export function getNeuroglancerViewerState(key: any): any;
|
|
43
|
+
export function getNeuroglancerColor(idStr: any, key: any): any;
|
|
44
|
+
export function closeSelectionTab(key: any): void;
|
|
45
|
+
export function getLayerManager(key: any): any;
|
|
46
|
+
export function getManagedLayer(key: any, name: any): any;
|
|
47
|
+
export function getAnnotationLayer(key: any, name: any): any;
|
|
48
|
+
export function getAnnotationSource(key: any, name: any): any;
|
|
49
|
+
export function addLayerSignalRemover(key: any, name: any, remover: any): void;
|
|
50
|
+
export function unsubscribeLayersChangedSignals(layerManager: any, signalKey: any): void;
|
|
51
|
+
export function configureLayersChangedSignals(key: any, layerConfig: any): any;
|
|
52
|
+
export function configureAnnotationLayer(layer: any, props: any, recordRemover: any): void;
|
|
53
|
+
export function configureAnnotationLayerChanged(layer: any, props: any, recordRemover: any): void;
|
|
54
|
+
export function getAnnotationSelectionHost(key: any): "viewer" | "layer" | null;
|
|
55
|
+
export function getSelectedAnnotationId(key: any, layerName: any): any;
|
|
56
|
+
/** @extends {React.Component<NgProps>} */
|
|
57
|
+
export default class Neuroglancer {
|
|
58
|
+
static defaultProps: {
|
|
59
|
+
perspectiveZoom: number;
|
|
60
|
+
eventBindingsToUpdate: null;
|
|
61
|
+
brainMapsClientId: string;
|
|
62
|
+
viewerState: null;
|
|
63
|
+
onSelectedChanged: null;
|
|
64
|
+
onVisibleChanged: null;
|
|
65
|
+
onSelectionDetailsStateChanged: null;
|
|
66
|
+
onViewerStateChanged: null;
|
|
67
|
+
key: null;
|
|
68
|
+
callbacks: never[];
|
|
69
|
+
ngServer: string;
|
|
70
|
+
};
|
|
71
|
+
constructor(props: any);
|
|
72
|
+
ngContainer: any;
|
|
73
|
+
viewer: any;
|
|
74
|
+
muteViewerChanged: boolean;
|
|
75
|
+
prevVisibleIds: Set<any>;
|
|
76
|
+
prevColorMap: any;
|
|
77
|
+
disposers: any[];
|
|
78
|
+
prevColorOverrides: Set<any>;
|
|
79
|
+
overrideColorsById: any;
|
|
80
|
+
allKnownIds: Set<any>;
|
|
81
|
+
minimalPoseSnapshot: () => {
|
|
82
|
+
position: any[];
|
|
83
|
+
projectionScale: any;
|
|
84
|
+
projectionOrientation: any[];
|
|
85
|
+
};
|
|
86
|
+
scheduleEmit: () => () => void;
|
|
87
|
+
withoutEmitting: (fn: any) => void;
|
|
88
|
+
didLayersChange: (prevVS: any, nextVS: any) => boolean;
|
|
89
|
+
applyColorsAndVisibility: (cellColorMapping: any) => void;
|
|
90
|
+
componentDidMount(): void;
|
|
91
|
+
componentDidUpdate(prevProps: any, prevState: any): void;
|
|
92
|
+
componentWillUnmount(): void;
|
|
93
|
+
setCallbacks(callbacks: any): void;
|
|
94
|
+
updateEventBindings: (eventBindingsToUpdate: any) => void;
|
|
95
|
+
selectionDetailsStateChanged: () => void;
|
|
96
|
+
layersChanged: () => void;
|
|
97
|
+
handlerRemovers: any[] | undefined;
|
|
98
|
+
selectedChanged: (layer: any) => void;
|
|
99
|
+
visibleChanged: (layer: any) => void;
|
|
100
|
+
render(): JSX.Element;
|
|
101
|
+
}
|
|
102
|
+
export type NgProps = {
|
|
103
|
+
perspectiveZoom: number;
|
|
104
|
+
viewerState: Object;
|
|
105
|
+
brainMapsClientId: string;
|
|
106
|
+
key: string;
|
|
107
|
+
cellColorMapping: {
|
|
108
|
+
[x: string]: string;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* An array of event bindings to change in Neuroglancer. The array format is as follows:
|
|
112
|
+
* [[old-event1, new-event1], [old-event2], old-event3]
|
|
113
|
+
* Here, `old-event1`'s will be unbound and its action will be re-bound to `new-event1`.
|
|
114
|
+
* The bindings for `old-event2` and `old-event3` will be removed.
|
|
115
|
+
* Neuroglancer has its own syntax for event descriptors, and here are some examples:
|
|
116
|
+
* 'keya', 'shift+keyb' 'control+keyc', 'digit4', 'space', 'arrowleft', 'comma', 'period',
|
|
117
|
+
* 'minus', 'equal', 'bracketleft'.
|
|
118
|
+
*/
|
|
119
|
+
eventBindingsToUpdate: any[];
|
|
120
|
+
/**
|
|
121
|
+
* A function of the form `(segment, layer) => {}`, called each time there is a change to
|
|
122
|
+
* the segment the user has 'selected' (i.e., hovered over) in Neuroglancer.
|
|
123
|
+
* The `segment` argument will be a Neuroglancer `Uint64` with the ID of the now-selected
|
|
124
|
+
* segment, or `null` if no segment is now selected.
|
|
125
|
+
* The `layer` argument will be a Neuroglaner `ManagedUserLayer`, whose `layer` property
|
|
126
|
+
* will be a Neuroglancer `SegmentationUserLayer`.
|
|
127
|
+
*/
|
|
128
|
+
onSelectedChanged: (segment: any | null, layer: any) => void;
|
|
129
|
+
/**
|
|
130
|
+
* A function of the form `(segments, layer) => {}`, called each time there is a change to
|
|
131
|
+
* the segments the user has designated as 'visible' (i.e., double-clicked on) in Neuroglancer.
|
|
132
|
+
* The `segments` argument will be a Neuroglancer `Uint64Set` whose elements are `Uint64`
|
|
133
|
+
* instances for the IDs of the now-visible segments.
|
|
134
|
+
* The `layer` argument will be a Neuroglaner `ManagedUserLayer`, whose `layer` property
|
|
135
|
+
* will be a Neuroglancer `SegmentationUserLayer`.
|
|
136
|
+
*/
|
|
137
|
+
onVisibleChanged: (segments: any, layer: any) => void;
|
|
138
|
+
/**
|
|
139
|
+
* A function of the form `() => {}` to respond to selection changes in the viewer.
|
|
140
|
+
*/
|
|
141
|
+
onSelectionDetailsStateChanged: () => void;
|
|
142
|
+
onViewerStateChanged: () => void;
|
|
143
|
+
/**
|
|
144
|
+
* // ngServer: string,
|
|
145
|
+
*/
|
|
146
|
+
callbacks: Array<Object>;
|
|
147
|
+
};
|
|
3
148
|
//# sourceMappingURL=ReactNeuroglancer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactNeuroglancer.d.ts","sourceRoot":"","sources":["../src/ReactNeuroglancer.js"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"ReactNeuroglancer.d.ts","sourceRoot":"","sources":["../src/ReactNeuroglancer.js"],"names":[],"mappings":"AAiCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAGH,4CAsBC;AAED,0DAGC;AAED,gEA2BC;AAED,kDAKC;AAED,+CAMC;AAED,0DAMC;AAED,6DAMC;AAED,8DAWC;AAED,+EAYC;AAED,yFAcC;AAED,+EA6BC;AAmFD,2FAwBC;AAED,kGAeC;AAED,gFAUC;AAED,uEA0BC;AAED,0CAA0C;AAC1C;IACE;;;;;;;;;;;;MAYE;IAEF,wBAYC;IAVC,iBAAoC;IACpC,YAAkB;IAElB,2BAA8B;IAC9B,yBAA+B;IAC/B,kBAAwB;IACxB,iBAAmB;IACnB,6BAAmC;IACnC,wBAA6C;IAC7C,sBAA4B;IAG9B;;;;MASE;IAGF,+BAWE;IAGF,mCAKE;IAGF,uDASE;IAGF,0DAgCE;IAEF,0BA+GC;IAED,yDAgIC;IAED,6BAUC;IAcD,mCAQC;IAED,0DAyCE;IAEF,yCAOE;IAEF,0BAsCE;IA5BI,mCAAyB;IA+B/B,sCAWE;IAEF,qCAUE;IAEF,sBAOC;CACF;;qBAh3Ba,MAAM;iBACN,MAAM;uBACN,MAAM;SACN,MAAM;sBACN;YAAO,MAAM,GAAE,MAAM;KAAC;;;;;;;;;;;;;;;;;;;uBAWtB,CAAC,OAAO,EAAC,GAAG,GAAC,IAAI,EAAE,KAAK,EAAC,GAAG,KAAK,IAAI;;;;;;;;;sBAQrC,CAAC,QAAQ,EAAC,GAAG,EAAE,KAAK,EAAC,GAAG,KAAK,IAAI;;;;oCAQjC,MAAM,IAAI;0BAEV,MAAM,IAAI;;;;eAEV,KAAK,CAAC,MAAM,CAAC"}
|