@vitessce/neuroglancer 3.9.5 → 3.9.7

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.
Files changed (37) hide show
  1. package/dist/{ReactNeuroglancer-BCg93QGV.js → ReactNeuroglancer-pv4bM8Yp.js} +43 -26
  2. package/dist/index-BEPd2Tds.js +37856 -0
  3. package/dist/index.js +1 -1
  4. package/dist-tsc/Neuroglancer.d.ts +0 -2
  5. package/dist-tsc/Neuroglancer.d.ts.map +1 -1
  6. package/dist-tsc/Neuroglancer.js +26 -26
  7. package/dist-tsc/NeuroglancerSubscriber.d.ts.map +1 -1
  8. package/dist-tsc/NeuroglancerSubscriber.js +219 -53
  9. package/dist-tsc/ReactNeuroglancer.d.ts +2 -2
  10. package/dist-tsc/ReactNeuroglancer.d.ts.map +1 -1
  11. package/dist-tsc/ReactNeuroglancer.js +31 -28
  12. package/dist-tsc/data-hook-ng-utils.d.ts +18 -20
  13. package/dist-tsc/data-hook-ng-utils.d.ts.map +1 -1
  14. package/dist-tsc/data-hook-ng-utils.js +136 -68
  15. package/dist-tsc/shader-utils.d.ts +126 -0
  16. package/dist-tsc/shader-utils.d.ts.map +1 -0
  17. package/dist-tsc/shader-utils.js +547 -0
  18. package/dist-tsc/shader-utils.test.d.ts +2 -0
  19. package/dist-tsc/shader-utils.test.d.ts.map +1 -0
  20. package/dist-tsc/shader-utils.test.js +364 -0
  21. package/dist-tsc/use-memo-custom-comparison.d.ts +14 -0
  22. package/dist-tsc/use-memo-custom-comparison.d.ts.map +1 -0
  23. package/dist-tsc/use-memo-custom-comparison.js +150 -0
  24. package/package.json +9 -8
  25. package/src/Neuroglancer.js +31 -26
  26. package/src/NeuroglancerSubscriber.js +361 -81
  27. package/src/README.md +28 -0
  28. package/src/ReactNeuroglancer.js +34 -27
  29. package/src/data-hook-ng-utils.js +178 -78
  30. package/src/shader-utils.js +653 -0
  31. package/src/shader-utils.test.js +432 -0
  32. package/src/use-memo-custom-comparison.js +190 -0
  33. package/dist/index-Wdrc02VW.js +0 -32390
  34. package/dist-tsc/data-hook-ng-utils.test.d.ts +0 -2
  35. package/dist-tsc/data-hook-ng-utils.test.d.ts.map +0 -1
  36. package/dist-tsc/data-hook-ng-utils.test.js +0 -35
  37. package/src/data-hook-ng-utils.test.js +0 -52
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { N } from "./index-Wdrc02VW.js";
1
+ import { N } from "./index-BEPd2Tds.js";
2
2
  export {
3
3
  N as NeuroglancerSubscriber
4
4
  };
@@ -3,8 +3,6 @@ export class NeuroglancerComp {
3
3
  bundleRoot: any;
4
4
  cellColorMapping: any;
5
5
  justReceivedExternalUpdate: boolean;
6
- prevElement: any;
7
- prevClickHandler: ((event: any) => void) | null;
8
6
  prevMouseStateChanged: any;
9
7
  prevHoverHandler: (() => void) | null;
10
8
  onViewerStateChanged(nextState: any): void;
@@ -1 +1 @@
1
- {"version":3,"file":"Neuroglancer.d.ts","sourceRoot":"","sources":["../src/Neuroglancer.js"],"names":[],"mappings":"AAUA;IACE,wBAcC;IAZC,gBAAgC;IAChC,sBAA8C;IAC9C,oCAAuC;IACvC,iBAAuB;IACvB,gDAA4B;IAC5B,2BAAiC;IACjC,sCAA4B;IAsD9B,2CAGC;IAjDD,4BA4CC;IAhDC,0BAAgD;IAChD,iCAA8D;IAsDhE,yCAQC;IAED,sBAoBC;CACF"}
1
+ {"version":3,"file":"Neuroglancer.d.ts","sourceRoot":"","sources":["../src/Neuroglancer.js"],"names":[],"mappings":"AAYA;IACE,wBAYC;IAVC,gBAAgC;IAChC,sBAA8C;IAC9C,oCAAuC;IACvC,2BAAiC;IACjC,sCAA4B;IA8D9B,2CAGC;IAzDD,4BAoDC;IAxDC,0BAAgD;IAChD,iCAA8D;IA8DhE,yCAKC;IAED,sBAoBC;CACF"}
@@ -1,11 +1,13 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /* eslint-disable react-refresh/only-export-components */
3
3
  import React, { PureComponent, Suspense } from 'react';
4
- import { ChunkWorker } from '@vitessce/neuroglancer-workers';
4
+ import { ChunkWorker, AsyncComputationWorker } from '@vitessce/neuroglancer-workers';
5
5
  import { NeuroglancerGlobalStyles } from './styles.js';
6
6
  const LazyReactNeuroglancer = React.lazy(() => import('./ReactNeuroglancer.js'));
7
7
  function createWorker() {
8
- return new ChunkWorker();
8
+ const worker = new ChunkWorker();
9
+ worker.AsyncComputationWorker = AsyncComputationWorker;
10
+ return worker;
9
11
  }
10
12
  export class NeuroglancerComp extends PureComponent {
11
13
  constructor(props) {
@@ -13,8 +15,6 @@ export class NeuroglancerComp extends PureComponent {
13
15
  this.bundleRoot = createWorker();
14
16
  this.cellColorMapping = props.cellColorMapping;
15
17
  this.justReceivedExternalUpdate = false;
16
- this.prevElement = null;
17
- this.prevClickHandler = null;
18
18
  this.prevMouseStateChanged = null;
19
19
  this.prevHoverHandler = null;
20
20
  this.onViewerStateChanged = this.onViewerStateChanged.bind(this);
@@ -29,42 +29,45 @@ export class NeuroglancerComp extends PureComponent {
29
29
  if (viewerRef) {
30
30
  // Mount
31
31
  const { viewer } = viewerRef;
32
- this.prevElement = viewer.element;
33
32
  this.prevMouseStateChanged = viewer.mouseState.changed;
34
- viewer.inputEventBindings.sliceView.set('at:dblclick0', () => { });
33
+ // For now, can omit the sliceView bindings, as we only use perspectiveView
34
+ // viewer.inputEventBindings.sliceView.set('at:dblclick0', () => {});
35
35
  viewer.inputEventBindings.perspectiveView.set('at:dblclick0', () => { });
36
- this.prevClickHandler = (event) => {
37
- if (event.button === 0) {
38
- // Wait for mouseState to update
39
- requestAnimationFrame(() => {
40
- const { pickedValue, pickedRenderLayer } = viewer.mouseState;
41
- // Only trigger selection when a segment is clicked rather than any click on the view
42
- if (pickedValue && pickedValue.low !== undefined && pickedRenderLayer) {
43
- this.latestOnSegmentClick?.(pickedValue.low);
36
+ // Disable space interaction to prevent triggering 4panels layout.
37
+ viewer.inputEventBindings.sliceView.set('at:space', () => { });
38
+ viewer.inputEventBindings.perspectiveView.set('at:space', () => { });
39
+ // Remap plain wheel to ctrl+wheel (zoom) action
40
+ // by traversing the parent binding maps.
41
+ const remapWheelToZoom = (map) => {
42
+ if (map.bindings) {
43
+ const ctrlWheelAction = map.bindings.get('at:control+wheel');
44
+ if (ctrlWheelAction) {
45
+ // Replace plain wheel with the zoom action
46
+ map.bindings.set('at:wheel', ctrlWheelAction);
47
+ const ctrlWheelBubble = map.bindings.get('bubble:control+wheel');
48
+ if (ctrlWheelBubble) {
49
+ map.bindings.set('bubble:wheel', ctrlWheelBubble);
44
50
  }
45
- });
51
+ }
52
+ }
53
+ if (map.parents) {
54
+ map.parents.forEach(p => remapWheelToZoom(p));
46
55
  }
47
56
  };
57
+ remapWheelToZoom(viewer.inputEventBindings.perspectiveView);
48
58
  this.prevHoverHandler = () => {
49
59
  if (viewer.mouseState.pickedValue !== undefined) {
50
60
  const pickedSegment = viewer.mouseState.pickedValue;
51
61
  this.latestOnSelectHoveredCoords?.(pickedSegment?.low);
52
62
  }
53
63
  };
54
- viewer.element.addEventListener('mouseup', this.prevClickHandler);
55
64
  viewer.mouseState.changed.add(this.prevHoverHandler);
56
65
  }
57
66
  else {
58
- // Unmount (viewerRef is null)
59
- if (this.prevElement && this.prevClickHandler) {
60
- this.prevElement.removeEventListener('mouseup', this.prevClickHandler);
61
- this.prevClickHandler = null;
62
- }
63
67
  if (this.prevMouseStateChanged && this.prevHoverHandler) {
64
68
  this.prevMouseStateChanged.remove(this.prevHoverHandler);
65
69
  this.prevHoverHandler = null;
66
70
  }
67
- this.prevElement = null;
68
71
  this.prevMouseStateChanged = null;
69
72
  }
70
73
  }
@@ -73,10 +76,7 @@ export class NeuroglancerComp extends PureComponent {
73
76
  setViewerState(nextState);
74
77
  }
75
78
  componentDidUpdate(prevProps) {
76
- const { onSegmentClick, onSelectHoveredCoords } = this.props;
77
- if (prevProps.onSegmentClick !== onSegmentClick) {
78
- this.latestOnSegmentClick = onSegmentClick;
79
- }
79
+ const { onSelectHoveredCoords } = this.props;
80
80
  if (prevProps.onSelectHoveredCoords !== onSelectHoveredCoords) {
81
81
  this.latestOnSelectHoveredCoords = onSelectHoveredCoords;
82
82
  }
@@ -1 +1 @@
1
- {"version":3,"file":"NeuroglancerSubscriber.d.ts","sourceRoot":"","sources":["../src/NeuroglancerSubscriber.js"],"names":[],"mappings":"AAkDA,gEAgeC"}
1
+ {"version":3,"file":"NeuroglancerSubscriber.d.ts","sourceRoot":"","sources":["../src/NeuroglancerSubscriber.js"],"names":[],"mappings":"AAwEA,gEAkuBC"}
@@ -1,11 +1,14 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /* eslint-disable max-len */
2
3
  /* eslint-disable no-unused-vars */
3
- import React, { useCallback, useMemo, useRef, useEffect, useState } from 'react';
4
- import { TitleInfo, useCoordination, useObsSetsData, useLoaders, useObsEmbeddingData, useCoordinationScopes, } from '@vitessce/vit-s';
5
- import { ViewHelpMapping, ViewType, COMPONENT_COORDINATION_TYPES, } from '@vitessce/constants-internal';
4
+ import React, { useCallback, useMemo, useRef, useEffect, useState, useReducer } from 'react';
5
+ import { TitleInfo, useReady, useInitialCoordination, useCoordination, useCoordinationScopes, useCoordinationScopesBy, useComplexCoordination, useMultiCoordinationScopesNonNull, useMultiCoordinationScopesSecondaryNonNull, useComplexCoordinationSecondary, useLoaders, useMergeCoordination, useMultiObsPoints, usePointMultiObsFeatureMatrixIndices, useMultiObsSegmentations, useSegmentationMultiFeatureSelection, useSegmentationMultiObsFeatureMatrixIndices, useSegmentationMultiObsSets, useGridItemSize, } from '@vitessce/vit-s';
6
+ import { ViewHelpMapping, ViewType, CoordinationType, COMPONENT_COORDINATION_TYPES, } from '@vitessce/constants-internal';
6
7
  import { mergeObsSets, getCellColors, setObsSelection } from '@vitessce/sets-utils';
8
+ import { MultiLegend } from '@vitessce/legend';
7
9
  import { NeuroglancerComp } from './Neuroglancer.js';
8
10
  import { useNeuroglancerViewerState } from './data-hook-ng-utils.js';
11
+ import { useMemoCustomComparison, customIsEqualForCellColors, } from './use-memo-custom-comparison.js';
9
12
  import { useStyles } from './styles.js';
10
13
  import { quaternionToEuler, eulerToQuaternion, valueGreaterThanEpsilon, nearEq, makeVitNgZoomCalibrator, conjQuat, multiplyQuat, rad2deg, deg2rad, Q_Y_UP, } from './utils.js';
11
14
  const VITESSCE_INTERACTION_DELAY = 50;
@@ -14,6 +17,7 @@ const ZOOM_EPS = 1e-2;
14
17
  const ROTATION_EPS = 1e-3;
15
18
  const TARGET_EPS = 0.5;
16
19
  const NG_ROT_COOLDOWN_MS = 120;
20
+ const GUIDE_URL = 'https://vitessce.io/docs/ng-guide/';
17
21
  const LAST_INTERACTION_SOURCE = {
18
22
  vitessce: 'vitessce',
19
23
  neuroglancer: 'neuroglancer',
@@ -24,21 +28,187 @@ function rgbToHex(rgb) {
24
28
  : `#${rgb.map(c => c.toString(16).padStart(2, '0')).join('')}`);
25
29
  }
26
30
  export function NeuroglancerSubscriber(props) {
27
- const { coordinationScopes: coordinationScopesRaw, closeButtonVisible, downloadButtonVisible, removeGridComponent, theme, title = 'Neuroglancer', helpText = ViewHelpMapping.NEUROGLANCER, } = props;
31
+ const { uuid, coordinationScopes: coordinationScopesRaw, coordinationScopesBy: coordinationScopesByRaw, closeButtonVisible, downloadButtonVisible, removeGridComponent, theme, title = 'Spatial', subtitle = 'Powered by Neuroglancer', helpText = ViewHelpMapping.NEUROGLANCER,
32
+ // Note: this is a temporary mechanism
33
+ // to pass an initial NG camera state.
34
+ // Ideally, all camera state should be passed via
35
+ // the existing spatialZoom, spatialTargetX, spatialRotationOrbit, etc,
36
+ // and then NeuroglancerSubscriber should internally convert
37
+ // to NG-compatible values, which would eliminate the need for this.
38
+ initialNgCameraState, } = props;
28
39
  const loaders = useLoaders();
40
+ const mergeCoordination = useMergeCoordination();
41
+ // Acccount for possible meta-coordination.
29
42
  const coordinationScopes = useCoordinationScopes(coordinationScopesRaw);
43
+ const coordinationScopesBy = useCoordinationScopesBy(coordinationScopes, coordinationScopesByRaw);
30
44
  const [{ dataset, obsType, spatialZoom, spatialTargetX, spatialTargetY, spatialRotationX, spatialRotationY, spatialRotationZ, spatialRotationOrbit,
31
45
  // spatialOrbitAxis, // always along Y-axis - not used in conversion
32
46
  embeddingType: mapping, obsSetColor: cellSetColor, obsSetSelection: cellSetSelection, additionalObsSets: additionalCellSets, }, { setAdditionalObsSets: setAdditionalCellSets, setObsSetColor: setCellSetColor, setObsColorEncoding: setCellColorEncoding, setObsSetSelection: setCellSetSelection, setObsHighlight: setCellHighlight, setSpatialTargetX: setTargetX, setSpatialTargetY: setTargetY, setSpatialRotationX: setRotationX,
33
47
  // setSpatialRotationY: setRotationY,
34
48
  // setSpatialRotationZ: setRotationZ,
35
49
  setSpatialRotationOrbit: setRotationOrbit, setSpatialZoom: setZoom, }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.NEUROGLANCER], coordinationScopes);
50
+ const [ngWidth, ngHeight, containerRef] = useGridItemSize();
51
+ const [segmentationLayerScopes, segmentationChannelScopesByLayer,] = useMultiCoordinationScopesSecondaryNonNull(CoordinationType.SEGMENTATION_CHANNEL, CoordinationType.SEGMENTATION_LAYER, coordinationScopes, coordinationScopesBy);
52
+ const pointLayerScopes = useMultiCoordinationScopesNonNull(CoordinationType.POINT_LAYER, coordinationScopes);
53
+ // Object keys are coordination scope names for spatialSegmentationLayer.
54
+ const segmentationLayerCoordination = useComplexCoordination([
55
+ CoordinationType.FILE_UID,
56
+ CoordinationType.SEGMENTATION_CHANNEL,
57
+ CoordinationType.SPATIAL_LAYER_VISIBLE,
58
+ CoordinationType.SPATIAL_LAYER_OPACITY,
59
+ ], coordinationScopes, coordinationScopesBy, CoordinationType.SEGMENTATION_LAYER);
60
+ // Object keys are coordination scope names for spatialSegmentationChannel.
61
+ const segmentationChannelCoordination = useComplexCoordinationSecondary([
62
+ CoordinationType.OBS_TYPE,
63
+ CoordinationType.SPATIAL_TARGET_C,
64
+ CoordinationType.SPATIAL_CHANNEL_VISIBLE,
65
+ CoordinationType.SPATIAL_CHANNEL_OPACITY,
66
+ CoordinationType.SPATIAL_CHANNEL_COLOR,
67
+ CoordinationType.SPATIAL_SEGMENTATION_FILLED,
68
+ CoordinationType.SPATIAL_SEGMENTATION_STROKE_WIDTH,
69
+ CoordinationType.OBS_COLOR_ENCODING,
70
+ CoordinationType.FEATURE_SELECTION,
71
+ CoordinationType.FEATURE_AGGREGATION_STRATEGY,
72
+ CoordinationType.FEATURE_VALUE_COLORMAP,
73
+ CoordinationType.FEATURE_VALUE_COLORMAP_RANGE,
74
+ CoordinationType.OBS_SET_COLOR,
75
+ CoordinationType.OBS_SET_SELECTION,
76
+ CoordinationType.ADDITIONAL_OBS_SETS,
77
+ CoordinationType.OBS_HIGHLIGHT,
78
+ CoordinationType.TOOLTIPS_VISIBLE,
79
+ CoordinationType.TOOLTIP_CROSSHAIRS_VISIBLE,
80
+ CoordinationType.LEGEND_VISIBLE,
81
+ ], coordinationScopes, coordinationScopesBy, CoordinationType.SEGMENTATION_LAYER, CoordinationType.SEGMENTATION_CHANNEL);
82
+ // Point layer
83
+ const pointLayerCoordination = useComplexCoordination([
84
+ CoordinationType.OBS_TYPE,
85
+ CoordinationType.SPATIAL_LAYER_VISIBLE,
86
+ CoordinationType.SPATIAL_LAYER_OPACITY,
87
+ CoordinationType.OBS_COLOR_ENCODING,
88
+ CoordinationType.FEATURE_COLOR,
89
+ CoordinationType.FEATURE_FILTER_MODE,
90
+ CoordinationType.FEATURE_SELECTION,
91
+ CoordinationType.FEATURE_VALUE_COLORMAP,
92
+ CoordinationType.FEATURE_VALUE_COLORMAP_RANGE,
93
+ CoordinationType.SPATIAL_LAYER_COLOR,
94
+ CoordinationType.OBS_HIGHLIGHT,
95
+ CoordinationType.TOOLTIPS_VISIBLE,
96
+ CoordinationType.TOOLTIP_CROSSHAIRS_VISIBLE,
97
+ CoordinationType.LEGEND_VISIBLE,
98
+ ], coordinationScopes, coordinationScopesBy, CoordinationType.POINT_LAYER);
99
+ // Points data
100
+ const [obsPointsData, obsPointsDataStatus, obsPointsUrls, obsPointsErrors] = useMultiObsPoints(coordinationScopes, coordinationScopesBy, loaders, dataset, mergeCoordination, uuid);
101
+ const [pointMultiIndicesData, pointMultiIndicesDataStatus, pointMultiIndicesDataErrors] = usePointMultiObsFeatureMatrixIndices(coordinationScopes, coordinationScopesBy, loaders, dataset);
102
+ // Segmentations data
103
+ const [obsSegmentationsData, obsSegmentationsDataStatus, obsSegmentationsUrls, obsSegmentationsDataErrors] = useMultiObsSegmentations(coordinationScopes, coordinationScopesBy, loaders, dataset, mergeCoordination, uuid);
104
+ const [obsSegmentationsSetsData, obsSegmentationsSetsDataStatus, obsSegmentationsSetsDataErrors] = useSegmentationMultiObsSets(coordinationScopes, coordinationScopesBy, loaders, dataset);
105
+ const [segmentationMultiExpressionData, segmentationMultiLoadedFeatureSelection, segmentationMultiExpressionExtents, segmentationMultiExpressionNormData, segmentationMultiFeatureSelectionStatus, segmentationMultiFeatureSelectionErrors,] = useSegmentationMultiFeatureSelection(coordinationScopes, coordinationScopesBy, loaders, dataset);
106
+ const [segmentationMultiIndicesData, segmentationMultiIndicesDataStatus, segmentationMultiIndicesDataErrors] = useSegmentationMultiObsFeatureMatrixIndices(coordinationScopes, coordinationScopesBy, loaders, dataset);
107
+ const errors = [
108
+ ...obsPointsErrors,
109
+ ...obsSegmentationsDataErrors,
110
+ ...obsSegmentationsSetsDataErrors,
111
+ ...pointMultiIndicesDataErrors,
112
+ ...segmentationMultiFeatureSelectionErrors,
113
+ ...segmentationMultiIndicesDataErrors,
114
+ ];
115
+ const isReady = useReady([
116
+ // Points
117
+ obsPointsDataStatus,
118
+ pointMultiIndicesDataStatus,
119
+ // Segmentations
120
+ obsSegmentationsDataStatus,
121
+ obsSegmentationsSetsDataStatus,
122
+ segmentationMultiFeatureSelectionStatus,
123
+ segmentationMultiIndicesDataStatus,
124
+ ]);
36
125
  // console.log("NG Subs Render orbit", spatialRotationX, spatialRotationY, spatialRotationOrbit);
37
126
  const { classes } = useStyles();
38
- const [{ obsSets: cellSets }] = useObsSetsData(loaders, dataset, false, { setObsSetSelection: setCellSetSelection, setObsSetColor: setCellSetColor }, { cellSetSelection, obsSetColor: cellSetColor }, { obsType });
39
- const [{ obsIndex }] = useObsEmbeddingData(loaders, dataset, true, {}, {}, { obsType, embeddingType: mapping });
40
- const [initalViewerState] = useNeuroglancerViewerState(loaders, dataset, false, undefined, undefined, { obsType: 'cell' });
41
- const latestViewerStateRef = useRef(initalViewerState);
127
+ const segmentationColorMapping = useMemoCustomComparison(() => {
128
+ // TODO: ultimately, segmentationColorMapping becomes cellColorMapping, and makes its way into the viewerState.
129
+ // It may make sense to merge the multiple useMemoCustomComparisons upstream of derivedViewerState into one.
130
+ // This would complicate the comparison function, but the multiple separate useMemos are not really necessary.
131
+ const result = {};
132
+ segmentationLayerScopes?.forEach((layerScope) => {
133
+ result[layerScope] = {};
134
+ segmentationChannelScopesByLayer?.[layerScope]?.forEach((channelScope) => {
135
+ const { obsSets: layerSets, obsIndex: layerIndex } = obsSegmentationsSetsData?.[layerScope]?.[channelScope] || {};
136
+ const { obsSetColor, obsColorEncoding, obsSetSelection, additionalObsSets, spatialChannelColor, } = segmentationChannelCoordination[0][layerScope][channelScope];
137
+ if (obsColorEncoding === 'spatialChannelColor') {
138
+ // All segments get the same static channel color
139
+ if (layerIndex && spatialChannelColor) {
140
+ const hex = rgbToHex(spatialChannelColor);
141
+ const ngCellColors = {};
142
+ if (obsSetSelection?.length > 0) {
143
+ // Only color the segments belonging to selected sets.
144
+ const mergedCellSets = mergeObsSets(layerSets, additionalObsSets);
145
+ const selectedIds = new Set();
146
+ obsSetSelection.forEach((setPath) => {
147
+ const rootNode = mergedCellSets?.tree?.find(n => n.name === setPath[0]);
148
+ const leafNode = setPath.length > 1
149
+ ? rootNode?.children?.find(n => n.name === setPath[1])
150
+ : rootNode;
151
+ leafNode?.set?.forEach(([id]) => selectedIds.add(String(id)));
152
+ });
153
+ layerIndex.forEach((id) => {
154
+ if (selectedIds.has(String(id))) {
155
+ ngCellColors[id] = hex;
156
+ }
157
+ });
158
+ }
159
+ result[layerScope][channelScope] = ngCellColors;
160
+ }
161
+ }
162
+ else if (layerSets && layerIndex) {
163
+ const mergedCellSets = mergeObsSets(layerSets, additionalObsSets);
164
+ const cellColors = getCellColors({
165
+ cellSets: mergedCellSets,
166
+ cellSetSelection: obsSetSelection,
167
+ cellSetColor: obsSetColor,
168
+ obsIndex: layerIndex,
169
+ theme,
170
+ });
171
+ // Convert the list of colors to an object of hex strings, which NG requires.
172
+ const ngCellColors = {};
173
+ cellColors.forEach((color, i) => {
174
+ ngCellColors[i] = rgbToHex(color);
175
+ });
176
+ result[layerScope][channelScope] = ngCellColors;
177
+ }
178
+ });
179
+ });
180
+ return result;
181
+ }, {
182
+ // The dependencies for the comparison,
183
+ // used by the custom equality function.
184
+ segmentationLayerScopes,
185
+ segmentationChannelScopesByLayer,
186
+ obsSegmentationsSetsData,
187
+ segmentationChannelCoordination,
188
+ theme,
189
+ }, customIsEqualForCellColors);
190
+ // Obtain the Neuroglancer viewerState object.
191
+ const initalViewerState = useNeuroglancerViewerState(theme, segmentationLayerScopes, segmentationChannelScopesByLayer, segmentationLayerCoordination, segmentationChannelCoordination, obsSegmentationsUrls, obsSegmentationsData, pointLayerScopes, pointLayerCoordination, obsPointsUrls, obsPointsData, pointMultiIndicesData);
192
+ const [latestViewerStateIteration, incrementLatestViewerStateIteration] = useReducer(x => x + 1, 0);
193
+ const latestViewerStateRef = useRef({
194
+ ...initalViewerState,
195
+ ...(initialNgCameraState ?? {}),
196
+ });
197
+ useEffect(() => {
198
+ const prevNgCameraState = {
199
+ position: latestViewerStateRef.current.position,
200
+ projectionOrientation: latestViewerStateRef.current.projectionOrientation,
201
+ projectionScale: latestViewerStateRef.current.projectionScale,
202
+ };
203
+ latestViewerStateRef.current = {
204
+ ...initalViewerState,
205
+ ...prevNgCameraState,
206
+ };
207
+ // Force a re-render by incrementing a piece of state.
208
+ // This works because we have made latestViewerStateIteration
209
+ // a dependency for derivedViewerState, triggering the useMemo downstream.
210
+ incrementLatestViewerStateIteration();
211
+ }, [initalViewerState]);
42
212
  const initialRotationPushedRef = useRef(false);
43
213
  const ngRotPushAtRef = useRef(0);
44
214
  const lastInteractionSource = useRef(null);
@@ -67,15 +237,6 @@ export function NeuroglancerSubscriber(props) {
67
237
  tx: spatialTargetX,
68
238
  ty: spatialTargetY,
69
239
  });
70
- const mergedCellSets = useMemo(() => mergeObsSets(cellSets, additionalCellSets), [cellSets, additionalCellSets]);
71
- const cellColors = useMemo(() => getCellColors({
72
- cellSets: mergedCellSets,
73
- cellSetSelection,
74
- cellSetColor,
75
- obsIndex,
76
- theme,
77
- }), [mergedCellSets, theme,
78
- cellSetColor, cellSetSelection, obsIndex]);
79
240
  /*
80
241
  * handleStateUpdate - Interactions from NG to Vitessce are pushed here
81
242
  */
@@ -182,6 +343,8 @@ export function NeuroglancerSubscriber(props) {
182
343
  };
183
344
  }, []);
184
345
  const onSegmentClick = useCallback((value) => {
346
+ // Note: this callback is no longer called by the child component.
347
+ // Reference: https://github.com/vitessce/vitessce/pull/2439
185
348
  if (value) {
186
349
  const id = String(value);
187
350
  const selectedCellIds = [id];
@@ -190,38 +353,34 @@ export function NeuroglancerSubscriber(props) {
190
353
  if (alreadySelectedId) {
191
354
  return;
192
355
  }
356
+ // TODO: update this now that we are using layer/channel-based organization of segmentations.
357
+ // There is no more "top-level" obsSets coordination; it is only on a per-layer basis.
358
+ // We should probably just assume the first segmentation layer/channel when updating the logic,
359
+ // since it is not clear how we would determine which layer/channel to update if there are multiple.
193
360
  setObsSelection(selectedCellIds, additionalCellSets, cellSetColor, setCellSetSelection, setAdditionalCellSets, setCellSetColor, setCellColorEncoding, 'Selection ', `: based on selected segments ${value}`);
194
361
  }
195
362
  }, [additionalCellSets, cellSetColor, setAdditionalCellSets,
196
363
  setCellColorEncoding, setCellSetColor, setCellSetSelection,
197
364
  ]);
198
- const batchedUpdateTimeoutRef = useRef(null);
199
- const [batchedCellColors, setBatchedCellColors] = useState(cellColors);
200
- useEffect(() => {
201
- if (batchedUpdateTimeoutRef.current) {
202
- clearTimeout(batchedUpdateTimeoutRef.current);
203
- }
204
- batchedUpdateTimeoutRef.current = setTimeout(() => {
205
- setBatchedCellColors(cellColors);
206
- }, 100);
207
- // TODO: look into deferredValue from React
208
- // startTransition(() => {
209
- // setBatchedCellColors(cellColors);
210
- // });
211
- }, [cellColors]);
212
- // TODO use a ref if slow - see prev commits
213
- const cellColorMapping = useMemo(() => {
214
- const colorMapping = {};
215
- batchedCellColors.forEach((color, cell) => {
216
- colorMapping[cell] = rgbToHex(color);
365
+ // Get the ultimate cellColorMapping for each layer to pass to NeuroglancerComp as a prop.
366
+ const cellColorMappingByLayer = useMemo(() => {
367
+ const result = {};
368
+ segmentationLayerScopes?.forEach((layerScope) => {
369
+ const channelScope = segmentationChannelScopesByLayer?.[layerScope]?.[0];
370
+ result[layerScope] = segmentationColorMapping?.[layerScope]?.[channelScope] ?? {};
217
371
  });
218
- return colorMapping;
219
- }, [batchedCellColors]);
372
+ return result;
373
+ }, [segmentationColorMapping, segmentationLayerScopes, segmentationChannelScopesByLayer]);
374
+ // TODO: try to simplify using useMemoCustomComparison?
375
+ // This would allow us to refactor a lot of the checking-for-changes logic into a comparison function,
376
+ // simplify some of the manual bookkeeping like with prevCoordsRef and lastInteractionSource,
377
+ // and would allow us to potentially remove usage of some refs (e.g., latestViewerStateRef)
378
+ // by relying on the memoization to prevent unnecessary updates.
220
379
  const derivedViewerState = useMemo(() => {
221
380
  const { current } = latestViewerStateRef;
222
- const nextSegments = Object.keys(cellColorMapping);
223
- const prevLayer = current?.layers?.[0] || {};
224
- const prevSegments = prevLayer.segments || [];
381
+ if (current.layers.length <= 0) {
382
+ return current;
383
+ }
225
384
  const { projectionScale, projectionOrientation, position } = current;
226
385
  // Did Vitessce coords change vs the *previous* render?
227
386
  const rotChangedNow = !nearEq(spatialRotationX, prevCoordsRef.current.rx, ROTATION_EPS)
@@ -326,18 +485,22 @@ export function NeuroglancerSubscriber(props) {
326
485
  nextOrientation = lastNgPushOrientationRef.current ?? projectionOrientation;
327
486
  lastInteractionSource.current = null;
328
487
  }
329
- const newLayer0 = {
330
- ...prevLayer,
331
- segments: nextSegments,
332
- segmentColors: cellColorMapping,
333
- };
488
+ const updatedLayers = current?.layers?.map((layer, idx) => {
489
+ const layerScope = segmentationLayerScopes?.[idx];
490
+ const layerColorMapping = cellColorMappingByLayer?.[layerScope] ?? {};
491
+ const layerSegments = Object.keys(layerColorMapping);
492
+ return {
493
+ ...layer,
494
+ segments: layerSegments,
495
+ segmentColors: layerColorMapping,
496
+ };
497
+ }) ?? [];
334
498
  const updated = {
335
499
  ...current,
336
500
  projectionScale: nextProjectionScale,
337
501
  projectionOrientation: nextOrientation,
338
502
  position: nextPosition,
339
- layers: prevSegments.length === 0 ? [newLayer0, ...(current?.layers?.slice(1)
340
- || [])] : current?.layers,
503
+ layers: updatedLayers,
341
504
  };
342
505
  latestViewerStateRef.current = updated;
343
506
  prevCoordsRef.current = {
@@ -350,14 +513,17 @@ export function NeuroglancerSubscriber(props) {
350
513
  ty: spatialTargetY,
351
514
  };
352
515
  return updated;
353
- }, [cellColorMapping, spatialZoom, spatialRotationX, spatialRotationY,
354
- spatialRotationZ, spatialTargetX, spatialTargetY]);
516
+ }, [cellColorMappingByLayer, spatialZoom, spatialRotationX, spatialRotationY,
517
+ spatialRotationZ, spatialTargetX, spatialTargetY, initalViewerState,
518
+ latestViewerStateIteration]);
355
519
  const onSegmentHighlight = useCallback((obsId) => {
356
520
  setCellHighlight(String(obsId));
357
- }, [obsIndex, setCellHighlight]);
521
+ }, [setCellHighlight]);
358
522
  // TODO: if all cells are deselected, a black view is shown, rather we want to show empty NG view?
359
523
  // if (!cellColorMapping || Object.keys(cellColorMapping).length === 0) {
360
524
  // return;
361
525
  // }
362
- 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 }) }));
526
+ const hasLayers = derivedViewerState?.layers?.length > 0;
527
+ // console.log(derivedViewerState);
528
+ return (_jsx(TitleInfo, { title: title, info: subtitle, helpText: helpText, isSpatial: true, theme: theme, closeButtonVisible: closeButtonVisible, downloadButtonVisible: downloadButtonVisible, removeGridComponent: removeGridComponent, isReady: isReady, errors: errors, withPadding: false, guideUrl: GUIDE_URL, children: hasLayers ? (_jsxs("div", { style: { position: 'relative', width: '100%', height: '100%' }, ref: containerRef, children: [_jsx("div", { style: { position: 'absolute', top: 0, right: 0, zIndex: 50 }, children: _jsx(MultiLegend, { theme: "dark", maxHeight: ngHeight, segmentationLayerScopes: segmentationLayerScopes, segmentationLayerCoordination: segmentationLayerCoordination, segmentationChannelScopesByLayer: segmentationChannelScopesByLayer, segmentationChannelCoordination: segmentationChannelCoordination }) }), _jsx(NeuroglancerComp, { classes: classes, onSegmentClick: onSegmentClick, onSelectHoveredCoords: onSegmentHighlight, viewerState: derivedViewerState, cellColorMapping: cellColorMappingByLayer, setViewerState: handleStateUpdate })] })) : null }));
363
529
  }
@@ -77,7 +77,7 @@ export default class Neuroglancer {
77
77
  disposers: any[];
78
78
  prevColorOverrides: Set<any>;
79
79
  overrideColorsById: any;
80
- allKnownIds: Set<any>;
80
+ allKnownIdsByLayer: {};
81
81
  minimalPoseSnapshot: () => {
82
82
  position: any[];
83
83
  projectionScale: any;
@@ -86,7 +86,7 @@ export default class Neuroglancer {
86
86
  scheduleEmit: () => () => void;
87
87
  withoutEmitting: (fn: any) => void;
88
88
  didLayersChange: (prevVS: any, nextVS: any) => boolean;
89
- applyColorsAndVisibility: (cellColorMapping: any) => void;
89
+ applyColorsAndVisibility: (cellColorMappingByLayer: any) => void;
90
90
  componentDidMount(): void;
91
91
  componentDidUpdate(prevProps: any, prevState: any): void;
92
92
  componentWillUnmount(): void;
@@ -1 +1 @@
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"}
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,uBAA4B;IAG9B;;;;MASE;IAGF,+BAWE;IAGF,mCAKE;IAGF,uDASE;IAGF,iEAsCE;IAEF,0BA+GC;IAED,yDAiIC;IAED,6BAUC;IAcD,mCAQC;IAED,0DAyCE;IAEF,yCAOE;IAEF,0BAsCE;IA5BI,mCAAyB;IA+B/B,sCAWE;IAEF,qCAUE;IAEF,sBAOC;CACF;;qBAv3Ba,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"}
@@ -373,7 +373,7 @@ export default class Neuroglancer extends React.Component {
373
373
  this.disposers = [];
374
374
  this.prevColorOverrides = new Set();
375
375
  this.overrideColorsById = Object.create(null);
376
- this.allKnownIds = new Set();
376
+ this.allKnownIdsByLayer = {};
377
377
  }
378
378
  minimalPoseSnapshot = () => {
379
379
  const v = this.viewer;
@@ -423,31 +423,33 @@ export default class Neuroglancer extends React.Component {
423
423
  return JSON.stringify(prevLayers) !== JSON.stringify(nextLayers);
424
424
  };
425
425
  /* To add colors to the segments, turning unselected to grey */
426
- applyColorsAndVisibility = (cellColorMapping) => {
426
+ applyColorsAndVisibility = (cellColorMappingByLayer) => {
427
427
  if (!this.viewer)
428
428
  return;
429
- // Track all ids we've ever seen so we can grey the ones
430
- // that drop out of the current selection.
431
- const selected = { ...(cellColorMapping || {}) }; // clone, don't mutate props
432
- for (const id of Object.keys(selected))
433
- this.allKnownIds.add(id);
434
- // If empty on first call, seed from initial segmentColors (if present)
435
- if (this.allKnownIds.size === 0) {
436
- const init = this.props.viewerState?.layers?.[0]?.segmentColors || {};
437
- for (const id of Object.keys(init))
438
- this.allKnownIds.add(id);
439
- }
440
- // Build a full color table: selected keep their hex, others grey
441
- const fullSegmentColors = {};
442
- for (const id of this.allKnownIds) {
443
- fullSegmentColors[id] = selected[id] || GREY_HEX;
444
- }
445
- // Patch layers with the new segmentColors (pose untouched)
429
+ // Build full color table per layer
446
430
  const baseLayers = (this.props.viewerState?.layers)
447
431
  ?? (this.viewer.state.toJSON().layers || []);
448
- const newLayers = baseLayers.map((layer, idx) => {
449
- // if only one layer, take that or check layer.type === 'segmentation'.
450
- if (idx === 0 || layer?.type === 'segmentation') {
432
+ const newLayers = baseLayers.map((layer) => {
433
+ // Match layerScope by checking if the NG layer name contains the scope key.
434
+ // NG layer names are of the form:
435
+ // "obsSegmentations-init_A_obsSegmentations_0-init_A_obsSegmentations_0"
436
+ const layerScope = Object.keys(cellColorMappingByLayer).find(scope => layer.name?.includes(scope));
437
+ const selected = { ...(cellColorMappingByLayer[layerScope] || {}) };
438
+ // Track all known IDs for this layer scope
439
+ if (!this.allKnownIdsByLayer)
440
+ this.allKnownIdsByLayer = {};
441
+ if (!this.allKnownIdsByLayer[layerScope]) {
442
+ this.allKnownIdsByLayer[layerScope] = new Set();
443
+ }
444
+ for (const id of Object.keys(selected)) {
445
+ this.allKnownIdsByLayer[layerScope].add(id);
446
+ }
447
+ // Build a full color table: selected keep their hex, others grey
448
+ const fullSegmentColors = {};
449
+ for (const id of this.allKnownIdsByLayer[layerScope] || []) {
450
+ fullSegmentColors[id] = selected[id] || GREY_HEX;
451
+ }
452
+ if (layer.type === 'segmentation') {
451
453
  return { ...layer, segmentColors: fullSegmentColors };
452
454
  }
453
455
  return layer;
@@ -556,7 +558,7 @@ export default class Neuroglancer extends React.Component {
556
558
  // window.viewer = this.viewer;
557
559
  }
558
560
  componentDidUpdate(prevProps, prevState) {
559
- const { viewerState, cellColorMapping } = this.props;
561
+ const { viewerState, cellColorMapping: cellColorMappingByLayer } = this.props;
560
562
  // The restoreState() call clears the 'selected' (hovered on) segment, which is needed
561
563
  // by Neuroglancer's code to toggle segment visibilty on a mouse click. To free the user
562
564
  // from having to move the mouse before clicking, save the selected segment and restore
@@ -633,8 +635,8 @@ export default class Neuroglancer extends React.Component {
633
635
  this.withoutEmitting(() => {
634
636
  const layers = Array.isArray(viewerState.layers) ? viewerState.layers : [];
635
637
  this.viewer.state.restoreState({ layers });
636
- if (cellColorMapping && Object.keys(cellColorMapping).length) {
637
- this.applyColorsAndVisibility(cellColorMapping);
638
+ if (cellColorMappingByLayer && Object.keys(cellColorMappingByLayer).length) {
639
+ this.applyColorsAndVisibility(cellColorMappingByLayer);
638
640
  }
639
641
  });
640
642
  }
@@ -642,12 +644,13 @@ export default class Neuroglancer extends React.Component {
642
644
  // this was to avid NG randomly assigning colors to the segments by resetting them
643
645
  const prevSize = prevProps.cellColorMapping
644
646
  ? Object.keys(prevProps.cellColorMapping).length : 0;
645
- const currSize = cellColorMapping ? Object.keys(cellColorMapping).length : 0;
646
- const mappingRefChanged = prevProps.cellColorMapping !== cellColorMapping;
647
+ const currSize = cellColorMappingByLayer
648
+ ? Object.keys(cellColorMappingByLayer).length : 0;
649
+ const mappingRefChanged = prevProps.cellColorMapping !== this.props.cellColorMapping;
647
650
  if (!this.didLayersChange(prevVS, viewerState)
648
651
  && (mappingRefChanged || prevSize !== currSize)) {
649
652
  this.withoutEmitting(() => {
650
- this.applyColorsAndVisibility(cellColorMapping);
653
+ this.applyColorsAndVisibility(cellColorMappingByLayer);
651
654
  });
652
655
  }
653
656
  // Treat "real" layer source/type changes differently from segment list changes.