@vitessce/neuroglancer 3.6.0 → 3.6.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,21 +1,211 @@
1
- import React from 'react';
1
+ /* eslint-disable no-unused-vars */
2
+ import React, { useCallback, useMemo } from 'react';
2
3
  import {
3
4
  TitleInfo,
5
+ useCoordination,
6
+ useObsSetsData,
7
+ useLoaders,
8
+ useObsEmbeddingData,
4
9
  } from '@vitessce/vit-s';
5
- import { ViewHelpMapping } from '@vitessce/constants-internal';
10
+ import {
11
+ ViewHelpMapping,
12
+ ViewType,
13
+ COMPONENT_COORDINATION_TYPES,
14
+ } from '@vitessce/constants-internal';
15
+ import { mergeObsSets, getCellColors, setObsSelection } from '@vitessce/sets-utils';
6
16
  import { Neuroglancer } from './Neuroglancer.js';
17
+ import { useStyles } from './styles.js';
18
+
19
+ const NEUROGLANCER_ZOOM_BASIS = 16;
20
+
21
+ function mapVitessceToNeuroglancer(zoom) {
22
+ return NEUROGLANCER_ZOOM_BASIS * (2 ** -zoom);
23
+ }
24
+
25
+ function mapNeuroglancerToVitessce(projectionScale) {
26
+ return -Math.log2(projectionScale / NEUROGLANCER_ZOOM_BASIS);
27
+ }
28
+
29
+ function quaternionToEuler([x, y, z, w]) {
30
+ // X-axis rotation (Roll)
31
+ const thetaX = Math.atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y));
32
+
33
+ // Y-axis rotation (Pitch)
34
+ const sinp = 2 * (w * y - z * x);
35
+ const thetaY = Math.abs(sinp) >= 1 ? Math.sign(sinp) * (Math.PI / 2) : Math.asin(sinp);
36
+
37
+ // Convert to degrees as Vitessce expects degrees?
38
+ return [thetaX * (180 / Math.PI), thetaY * (180 / Math.PI)];
39
+ }
40
+
41
+
42
+ function eulerToQuaternion(thetaX, thetaY) {
43
+ // Convert Euler angles (X, Y rotations) to quaternion
44
+ const halfThetaX = thetaX / 2;
45
+ const halfThetaY = thetaY / 2;
46
+
47
+ const sinX = Math.sin(halfThetaX);
48
+ const cosX = Math.cos(halfThetaX);
49
+ const sinY = Math.sin(halfThetaY);
50
+ const cosY = Math.cos(halfThetaY);
51
+
52
+ return [
53
+ sinX * cosY,
54
+ cosX * sinY,
55
+ sinX * sinY,
56
+ cosX * cosY,
57
+ ];
58
+ }
59
+
60
+ function normalizeQuaternion(q) {
61
+ const length = Math.sqrt((q[0] ** 2) + (q[1] ** 2) + (q[2] ** 2) + (q[3] ** 2));
62
+ return q.map(value => value / length);
63
+ }
7
64
 
8
65
  export function NeuroglancerSubscriber(props) {
9
66
  const {
67
+ coordinationScopes,
10
68
  closeButtonVisible,
11
69
  downloadButtonVisible,
12
70
  removeGridComponent,
13
71
  theme,
14
72
  title = 'Neuroglancer',
15
- viewerState: viewerStateInitial = null,
16
73
  helpText = ViewHelpMapping.NEUROGLANCER,
74
+ viewerState: initialViewerState,
17
75
  } = props;
18
76
 
77
+ const [{
78
+ dataset,
79
+ obsType,
80
+ spatialZoom,
81
+ spatialTargetX,
82
+ spatialTargetY,
83
+ spatialRotationX,
84
+ spatialRotationY,
85
+ // spatialRotationZ,
86
+ // spatialRotationOrbit,
87
+ // spatialOrbitAxis,
88
+ embeddingType: mapping,
89
+ obsSetSelection: cellSetSelection,
90
+ additionalObsSets: additionalCellSets,
91
+ obsSetColor: cellSetColor,
92
+ }, {
93
+ setAdditionalObsSets: setAdditionalCellSets,
94
+ setObsSetColor: setCellSetColor,
95
+ setObsColorEncoding: setCellColorEncoding,
96
+ setObsSetSelection: setCellSetSelection,
97
+ setObsHighlight: setCellHighlight,
98
+ setSpatialTargetX: setTargetX,
99
+ setSpatialTargetY: setTargetY,
100
+ setSpatialRotationX: setRotationX,
101
+ setSpatialRotationY: setRotationY,
102
+ // setSpatialRotationZ: setRotationZ,
103
+ // setSpatialRotationOrbit: setRotationOrbit,
104
+
105
+ setSpatialZoom: setZoom,
106
+ }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.NEUROGLANCER], coordinationScopes);
107
+
108
+ const { classes } = useStyles();
109
+ const loaders = useLoaders();
110
+
111
+ const [{ obsSets: cellSets }] = useObsSetsData(
112
+ loaders, dataset, false,
113
+ { setObsSetSelection: setCellSetSelection, setObsSetColor: setCellSetColor },
114
+ { cellSetSelection, obsSetColor: cellSetColor },
115
+ { obsType },
116
+ );
117
+
118
+ const [{ obsIndex }] = useObsEmbeddingData(
119
+ loaders, dataset, true, {}, {},
120
+ { obsType, embeddingType: mapping },
121
+ );
122
+
123
+ const handleStateUpdate = useCallback((newState) => {
124
+ const { projectionScale, projectionOrientation, position } = newState;
125
+ setZoom(mapNeuroglancerToVitessce(projectionScale));
126
+ const vitessceEularMapping = quaternionToEuler(projectionOrientation);
127
+
128
+ // TODO: support z rotation on SpatialView?
129
+ setRotationX(vitessceEularMapping[0]);
130
+ setRotationY(vitessceEularMapping[1]);
131
+
132
+ // Note: To pan in Neuroglancer, use shift+leftKey+drag
133
+ setTargetX(position[0]);
134
+ setTargetY(position[1]);
135
+ }, [setZoom, setTargetX, setTargetY, setRotationX, setRotationY]);
136
+
137
+ const onSegmentClick = useCallback((value) => {
138
+ if (value) {
139
+ const selectedCellIds = [String(value)];
140
+ setObsSelection(
141
+ selectedCellIds, additionalCellSets, cellSetColor,
142
+ setCellSetSelection, setAdditionalCellSets, setCellSetColor,
143
+ setCellColorEncoding,
144
+ 'Selection ',
145
+ `: based on selected segments ${value}`,
146
+ );
147
+ }
148
+ }, [additionalCellSets, cellSetColor, setAdditionalCellSets,
149
+ setCellColorEncoding, setCellSetColor, setCellSetSelection,
150
+ ]);
151
+
152
+ const mergedCellSets = useMemo(() => mergeObsSets(
153
+ cellSets, additionalCellSets,
154
+ ), [cellSets, additionalCellSets]);
155
+
156
+ const cellColors = useMemo(() => getCellColors({
157
+ cellSets: mergedCellSets,
158
+ cellSetSelection,
159
+ cellSetColor,
160
+ obsIndex,
161
+ theme,
162
+ }), [mergedCellSets, theme,
163
+ cellSetColor, cellSetSelection, obsIndex]);
164
+
165
+ const rgbToHex = useCallback(rgb => (typeof rgb === 'string' ? rgb
166
+ : `#${rgb.map(c => c.toString(16).padStart(2, '0')).join('')}`), []);
167
+
168
+ const cellColorMapping = useMemo(() => {
169
+ const colorCellMapping = {};
170
+ cellColors.forEach((color, cell) => {
171
+ colorCellMapping[cell] = rgbToHex(color);
172
+ });
173
+ return colorCellMapping;
174
+ }, [cellColors, rgbToHex]);
175
+
176
+ const derivedViewerState = useMemo(() => ({
177
+ ...initialViewerState,
178
+ layers: initialViewerState.layers.map((layer, index) => (index === 0
179
+ ? {
180
+ ...layer,
181
+ segments: Object.keys(cellColorMapping).map(String),
182
+ segmentColors: cellColorMapping,
183
+ }
184
+ : layer)),
185
+ }), [cellColorMapping, initialViewerState]);
186
+
187
+ const derivedViewerState2 = useMemo(() => {
188
+ if (typeof spatialZoom === 'number' && typeof spatialTargetX === 'number') {
189
+ const projectionScale = mapVitessceToNeuroglancer(spatialZoom);
190
+ const position = [spatialTargetX, spatialTargetY, derivedViewerState.position[2]];
191
+ const projectionOrientation = normalizeQuaternion(
192
+ eulerToQuaternion(spatialRotationX, spatialRotationY),
193
+ );
194
+ return {
195
+ ...derivedViewerState,
196
+ projectionScale,
197
+ position,
198
+ projectionOrientation,
199
+ };
200
+ }
201
+ return derivedViewerState;
202
+ }, [derivedViewerState, spatialZoom, spatialTargetX,
203
+ spatialTargetY, spatialRotationX, spatialRotationY]);
204
+
205
+ const onSegmentHighlight = useCallback((obsId) => {
206
+ setCellHighlight(String(obsId));
207
+ }, [obsIndex, setCellHighlight]);
208
+
19
209
  return (
20
210
  <TitleInfo
21
211
  title={title}
@@ -26,9 +216,15 @@ export function NeuroglancerSubscriber(props) {
26
216
  downloadButtonVisible={downloadButtonVisible}
27
217
  removeGridComponent={removeGridComponent}
28
218
  isReady
219
+ withPadding={false}
29
220
  >
30
- {viewerStateInitial && <Neuroglancer viewerState={viewerStateInitial} />}
221
+ <Neuroglancer
222
+ classes={classes}
223
+ onSegmentClick={onSegmentClick}
224
+ onSelectHoveredCoords={onSegmentHighlight}
225
+ viewerState={derivedViewerState2}
226
+ setViewerState={handleStateUpdate}
227
+ />
31
228
  </TitleInfo>
32
-
33
229
  );
34
230
  }
package/src/styles.js CHANGED
@@ -1116,7 +1116,7 @@ const globalNeuroglancerStyles = {
1116
1116
  overflowY: 'scroll',
1117
1117
  wordWrap: 'break-word',
1118
1118
  },
1119
- '.neuroglancer-multiline-autocomplete-completion:nth-child(even):not(.neuroglancer-multiline-autocomplete-completion-active)': {
1119
+ '.neuroglancer-multiline-autocomplete-completion:nth-of-type(even):not(.neuroglancer-multiline-autocomplete-completion-active)': {
1120
1120
  backgroundColor: '#2b2b2b',
1121
1121
  },
1122
1122
  '.neuroglancer-multiline-autocomplete-completion:hover': {
@@ -1418,7 +1418,7 @@ const globalNeuroglancerStyles = {
1418
1418
  backgroundClip: 'border-box',
1419
1419
  backgroundColor: '#8080ff80',
1420
1420
  },
1421
- '.neuroglancer-stack-layout-drop-placeholder:first-child, .neuroglancer-stack-layout-drop-placeholder:last-child': {
1421
+ '.neuroglancer-stack-layout-drop-placeholder:first-of-type, .neuroglancer-stack-layout-drop-placeholder:last-of-type': {
1422
1422
  display: 'none',
1423
1423
  },
1424
1424
  '.neuroglancer-panel': { flex: 1 },
@@ -1564,7 +1564,7 @@ const globalNeuroglancerStyles = {
1564
1564
  alignItems: 'center',
1565
1565
  },
1566
1566
  '.neuroglancer-position-widget input:disabled': { pointerEvents: 'none' },
1567
- '.neuroglancer-position-widget .neuroglancer-copy-button:first-child': {
1567
+ '.neuroglancer-position-widget .neuroglancer-copy-button:first-of-type': {
1568
1568
  display: 'none',
1569
1569
  },
1570
1570
  '.neuroglancer-position-dimension-coordinate, .neuroglancer-position-dimension-name, .neuroglancer-position-dimension-scale': {