@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.
@@ -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 { Neuroglancer } from './Neuroglancer.js';
7
+ import { NeuroglancerComp } from './Neuroglancer.js';
8
8
  import { useStyles } from './styles.js';
9
- const NEUROGLANCER_ZOOM_BASIS = 16;
10
- function mapVitessceToNeuroglancer(zoom) {
11
- return NEUROGLANCER_ZOOM_BASIS * (2 ** -zoom);
12
- }
13
- function mapNeuroglancerToVitessce(projectionScale) {
14
- return -Math.log2(projectionScale / NEUROGLANCER_ZOOM_BASIS);
15
- }
16
- function quaternionToEuler([x, y, z, w]) {
17
- // X-axis rotation (Roll)
18
- const thetaX = Math.atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y));
19
- // Y-axis rotation (Pitch)
20
- const sinp = 2 * (w * y - z * x);
21
- const thetaY = Math.abs(sinp) >= 1 ? Math.sign(sinp) * (Math.PI / 2) : Math.asin(sinp);
22
- // Convert to degrees as Vitessce expects degrees?
23
- return [thetaX * (180 / Math.PI), thetaY * (180 / Math.PI)];
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
- // spatialRotationZ,
50
- // spatialRotationOrbit,
51
- // spatialOrbitAxis,
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
- // setSpatialRotationOrbit: setRotationOrbit,
55
- setSpatialZoom: setZoom, }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.NEUROGLANCER], coordinationScopes);
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 handleStateUpdate = useCallback((newState) => {
60
- const { projectionScale, projectionOrientation, position } = newState;
61
- setZoom(mapNeuroglancerToVitessce(projectionScale));
62
- const vitessceEularMapping = quaternionToEuler(projectionOrientation);
63
- // TODO: support z rotation on SpatialView?
64
- setRotationX(vitessceEularMapping[0]);
65
- setRotationY(vitessceEularMapping[1]);
66
- // Note: To pan in Neuroglancer, use shift+leftKey+drag
67
- setTargetX(position[0]);
68
- setTargetY(position[1]);
69
- }, [setZoom, setTargetX, setTargetY, setRotationX, setRotationY]);
70
- const onSegmentClick = useCallback((value) => {
71
- if (value) {
72
- const selectedCellIds = [String(value)];
73
- setObsSelection(selectedCellIds, additionalCellSets, cellSetColor, setCellSetSelection, setAdditionalCellSets, setCellSetColor, setCellColorEncoding, 'Selection ', `: based on selected segments ${value}`);
74
- }
75
- }, [additionalCellSets, cellSetColor, setAdditionalCellSets,
76
- setCellColorEncoding, setCellSetColor, setCellSetSelection,
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
- const rgbToHex = useCallback(rgb => (typeof rgb === 'string' ? rgb
88
- : `#${rgb.map(c => c.toString(16).padStart(2, '0')).join('')}`), []);
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 colorCellMapping = {};
91
- cellColors.forEach((color, cell) => {
92
- colorCellMapping[cell] = rgbToHex(color);
212
+ const colorMapping = {};
213
+ batchedCellColors.forEach((color, cell) => {
214
+ colorMapping[cell] = rgbToHex(color);
93
215
  });
94
- return colorCellMapping;
95
- }, [cellColors, rgbToHex]);
96
- const derivedViewerState = useMemo(() => ({
97
- ...initialViewerState,
98
- layers: initialViewerState.layers.map((layer, index) => (index === 0
99
- ? {
100
- ...layer,
101
- segments: Object.keys(cellColorMapping).map(String),
102
- segmentColors: cellColorMapping,
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
- return derivedViewerState;
119
- }, [derivedViewerState, spatialZoom, spatialTargetX,
120
- spatialTargetY, spatialRotationX, spatialRotationY]);
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
- return (_jsx(TitleInfo, { title: title, helpText: helpText, isSpatial: true, theme: theme, closeButtonVisible: closeButtonVisible, downloadButtonVisible: downloadButtonVisible, removeGridComponent: removeGridComponent, isReady: true, withPadding: false, children: _jsx(Neuroglancer, { classes: classes, onSegmentClick: onSegmentClick, onSelectHoveredCoords: onSegmentHighlight, viewerState: derivedViewerState2, setViewerState: handleStateUpdate }) }));
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
- export default ReactNeuroglancerWrapper;
2
- declare const ReactNeuroglancerWrapper: any;
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":";AAKA,4CAEG"}
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"}