@vitessce/neuroglancer 3.9.6 → 3.9.8
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-BSLfuCt9.js → ReactNeuroglancer-Bxe4YcLF.js} +102 -30
- package/dist/index-anGvS-pL.js +37930 -0
- package/dist/index.js +1 -1
- package/dist-tsc/Neuroglancer.d.ts +0 -2
- package/dist-tsc/Neuroglancer.d.ts.map +1 -1
- package/dist-tsc/Neuroglancer.js +28 -28
- package/dist-tsc/NeuroglancerSubscriber.d.ts.map +1 -1
- package/dist-tsc/NeuroglancerSubscriber.js +70 -27
- package/dist-tsc/ReactNeuroglancer.d.ts +13 -2
- package/dist-tsc/ReactNeuroglancer.d.ts.map +1 -1
- package/dist-tsc/ReactNeuroglancer.js +89 -31
- package/dist-tsc/data-hook-ng-utils.d.ts +1 -1
- package/dist-tsc/data-hook-ng-utils.d.ts.map +1 -1
- package/dist-tsc/data-hook-ng-utils.js +18 -4
- package/dist-tsc/shader-utils.d.ts +12 -12
- package/dist-tsc/shader-utils.d.ts.map +1 -1
- package/dist-tsc/shader-utils.js +51 -26
- package/dist-tsc/shader-utils.test.js +20 -0
- package/dist-tsc/use-memo-custom-comparison.d.ts.map +1 -1
- package/dist-tsc/use-memo-custom-comparison.js +6 -0
- package/package.json +9 -9
- package/src/Neuroglancer.js +33 -27
- package/src/NeuroglancerSubscriber.js +102 -49
- package/src/ReactNeuroglancer.js +99 -30
- package/src/data-hook-ng-utils.js +21 -2
- package/src/shader-utils.js +79 -26
- package/src/shader-utils.test.js +20 -0
- package/src/use-memo-custom-comparison.js +7 -0
- package/dist/index-DvhFVdN_.js +0 -37826
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitessce/neuroglancer",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.8",
|
|
4
4
|
"author": "Gehlenborg Lab",
|
|
5
5
|
"homepage": "http://vitessce.io",
|
|
6
6
|
"repository": {
|
|
@@ -20,14 +20,14 @@
|
|
|
20
20
|
"lodash-es": "^4.17.21",
|
|
21
21
|
"three": "^0.154.0",
|
|
22
22
|
"react": "18.3.1",
|
|
23
|
-
"@vitessce/neuroglancer-workers": "3.9.
|
|
24
|
-
"@vitessce/vit-s": "3.9.
|
|
25
|
-
"@vitessce/styles": "3.9.
|
|
26
|
-
"@vitessce/utils": "3.9.
|
|
27
|
-
"@vitessce/constants-internal": "3.9.
|
|
28
|
-
"@vitessce/
|
|
29
|
-
"@vitessce/tooltip": "3.9.
|
|
30
|
-
"@vitessce/legend": "3.9.
|
|
23
|
+
"@vitessce/neuroglancer-workers": "3.9.8",
|
|
24
|
+
"@vitessce/vit-s": "3.9.8",
|
|
25
|
+
"@vitessce/styles": "3.9.8",
|
|
26
|
+
"@vitessce/sets-utils": "3.9.8",
|
|
27
|
+
"@vitessce/constants-internal": "3.9.8",
|
|
28
|
+
"@vitessce/utils": "3.9.8",
|
|
29
|
+
"@vitessce/tooltip": "3.9.8",
|
|
30
|
+
"@vitessce/legend": "3.9.8"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@testing-library/jest-dom": "^6.6.3",
|
package/src/Neuroglancer.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/* eslint-disable react-refresh/only-export-components */
|
|
2
2
|
import React, { PureComponent, Suspense } from 'react';
|
|
3
|
-
import { ChunkWorker } from '@vitessce/neuroglancer-workers';
|
|
3
|
+
import { ChunkWorker, AsyncComputationWorker } from '@vitessce/neuroglancer-workers';
|
|
4
4
|
import { NeuroglancerGlobalStyles } from './styles.js';
|
|
5
5
|
|
|
6
6
|
const LazyReactNeuroglancer = React.lazy(() => import('./ReactNeuroglancer.js'));
|
|
7
7
|
|
|
8
8
|
function createWorker() {
|
|
9
|
-
|
|
9
|
+
const worker = new ChunkWorker();
|
|
10
|
+
worker.AsyncComputationWorker = AsyncComputationWorker;
|
|
11
|
+
return worker;
|
|
10
12
|
}
|
|
11
13
|
export class NeuroglancerComp extends PureComponent {
|
|
12
14
|
constructor(props) {
|
|
@@ -14,8 +16,6 @@ export class NeuroglancerComp extends PureComponent {
|
|
|
14
16
|
this.bundleRoot = createWorker();
|
|
15
17
|
this.cellColorMapping = props.cellColorMapping;
|
|
16
18
|
this.justReceivedExternalUpdate = false;
|
|
17
|
-
this.prevElement = null;
|
|
18
|
-
this.prevClickHandler = null;
|
|
19
19
|
this.prevMouseStateChanged = null;
|
|
20
20
|
this.prevHoverHandler = null;
|
|
21
21
|
this.onViewerStateChanged = this.onViewerStateChanged.bind(this);
|
|
@@ -32,41 +32,49 @@ export class NeuroglancerComp extends PureComponent {
|
|
|
32
32
|
if (viewerRef) {
|
|
33
33
|
// Mount
|
|
34
34
|
const { viewer } = viewerRef;
|
|
35
|
-
this.prevElement = viewer.element;
|
|
36
35
|
this.prevMouseStateChanged = viewer.mouseState.changed;
|
|
37
|
-
|
|
36
|
+
// For now, can omit the sliceView bindings, as we only use perspectiveView
|
|
37
|
+
// viewer.inputEventBindings.sliceView.set('at:dblclick0', () => {});
|
|
38
38
|
viewer.inputEventBindings.perspectiveView.set('at:dblclick0', () => {});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
|
|
40
|
+
// Disable space interaction to prevent triggering 4panels layout.
|
|
41
|
+
viewer.inputEventBindings.sliceView.set('at:space', () => {});
|
|
42
|
+
viewer.inputEventBindings.perspectiveView.set('at:space', () => {});
|
|
43
|
+
|
|
44
|
+
// Remap plain wheel to ctrl+wheel (zoom) action
|
|
45
|
+
// by traversing the parent binding maps.
|
|
46
|
+
const remapWheelToZoom = (map) => {
|
|
47
|
+
if (map.bindings) {
|
|
48
|
+
const ctrlWheelAction = map.bindings.get('at:control+wheel');
|
|
49
|
+
if (ctrlWheelAction) {
|
|
50
|
+
// Replace plain wheel with the zoom action
|
|
51
|
+
map.bindings.set('at:wheel', ctrlWheelAction);
|
|
52
|
+
const ctrlWheelBubble = map.bindings.get('bubble:control+wheel');
|
|
53
|
+
if (ctrlWheelBubble) {
|
|
54
|
+
map.bindings.set('bubble:wheel', ctrlWheelBubble);
|
|
47
55
|
}
|
|
48
|
-
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (map.parents) {
|
|
59
|
+
map.parents.forEach(p => remapWheelToZoom(p));
|
|
49
60
|
}
|
|
50
61
|
};
|
|
62
|
+
|
|
63
|
+
remapWheelToZoom(viewer.inputEventBindings.perspectiveView);
|
|
64
|
+
|
|
51
65
|
this.prevHoverHandler = () => {
|
|
52
66
|
if (viewer.mouseState.pickedValue !== undefined) {
|
|
53
67
|
const pickedSegment = viewer.mouseState.pickedValue;
|
|
54
68
|
this.latestOnSelectHoveredCoords?.(pickedSegment?.low);
|
|
55
69
|
}
|
|
56
70
|
};
|
|
57
|
-
|
|
71
|
+
|
|
58
72
|
viewer.mouseState.changed.add(this.prevHoverHandler);
|
|
59
73
|
} else {
|
|
60
|
-
// Unmount (viewerRef is null)
|
|
61
|
-
if (this.prevElement && this.prevClickHandler) {
|
|
62
|
-
this.prevElement.removeEventListener('mouseup', this.prevClickHandler);
|
|
63
|
-
this.prevClickHandler = null;
|
|
64
|
-
}
|
|
65
74
|
if (this.prevMouseStateChanged && this.prevHoverHandler) {
|
|
66
75
|
this.prevMouseStateChanged.remove(this.prevHoverHandler);
|
|
67
76
|
this.prevHoverHandler = null;
|
|
68
77
|
}
|
|
69
|
-
this.prevElement = null;
|
|
70
78
|
this.prevMouseStateChanged = null;
|
|
71
79
|
}
|
|
72
80
|
}
|
|
@@ -77,17 +85,14 @@ export class NeuroglancerComp extends PureComponent {
|
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
componentDidUpdate(prevProps) {
|
|
80
|
-
const {
|
|
81
|
-
if (prevProps.onSegmentClick !== onSegmentClick) {
|
|
82
|
-
this.latestOnSegmentClick = onSegmentClick;
|
|
83
|
-
}
|
|
88
|
+
const { onSelectHoveredCoords } = this.props;
|
|
84
89
|
if (prevProps.onSelectHoveredCoords !== onSelectHoveredCoords) {
|
|
85
90
|
this.latestOnSelectHoveredCoords = onSelectHoveredCoords;
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
render() {
|
|
90
|
-
const { classes, viewerState, cellColorMapping } = this.props;
|
|
95
|
+
const { classes, viewerState, cellColorMapping, onLayerLoadingChange } = this.props;
|
|
91
96
|
|
|
92
97
|
return (
|
|
93
98
|
<>
|
|
@@ -98,6 +103,7 @@ export class NeuroglancerComp extends PureComponent {
|
|
|
98
103
|
brainMapsClientId="NOT_A_VALID_ID"
|
|
99
104
|
viewerState={viewerState}
|
|
100
105
|
onViewerStateChanged={this.onViewerStateChanged}
|
|
106
|
+
onLayerLoadingChange={onLayerLoadingChange}
|
|
101
107
|
bundleRoot={this.bundleRoot}
|
|
102
108
|
cellColorMapping={cellColorMapping}
|
|
103
109
|
ref={this.onRef}
|
|
@@ -57,6 +57,8 @@ const ROTATION_EPS = 1e-3;
|
|
|
57
57
|
const TARGET_EPS = 0.5;
|
|
58
58
|
const NG_ROT_COOLDOWN_MS = 120;
|
|
59
59
|
|
|
60
|
+
const GUIDE_URL = 'https://vitessce.io/docs/ng-guide/';
|
|
61
|
+
|
|
60
62
|
const LAST_INTERACTION_SOURCE = {
|
|
61
63
|
vitessce: 'vitessce',
|
|
62
64
|
neuroglancer: 'neuroglancer',
|
|
@@ -77,6 +79,7 @@ export function NeuroglancerSubscriber(props) {
|
|
|
77
79
|
downloadButtonVisible,
|
|
78
80
|
removeGridComponent,
|
|
79
81
|
theme,
|
|
82
|
+
showAxisLines = false,
|
|
80
83
|
title = 'Spatial',
|
|
81
84
|
subtitle = 'Powered by Neuroglancer',
|
|
82
85
|
helpText = ViewHelpMapping.NEUROGLANCER,
|
|
@@ -205,6 +208,7 @@ export function NeuroglancerSubscriber(props) {
|
|
|
205
208
|
CoordinationType.TOOLTIPS_VISIBLE,
|
|
206
209
|
CoordinationType.TOOLTIP_CROSSHAIRS_VISIBLE,
|
|
207
210
|
CoordinationType.LEGEND_VISIBLE,
|
|
211
|
+
CoordinationType.SPATIAL_POINT_STROKE_WIDTH,
|
|
208
212
|
],
|
|
209
213
|
coordinationScopes,
|
|
210
214
|
coordinationScopesBy,
|
|
@@ -281,13 +285,41 @@ export function NeuroglancerSubscriber(props) {
|
|
|
281
285
|
segmentationChannelScopesByLayer?.[layerScope]?.forEach((channelScope) => {
|
|
282
286
|
const { obsSets: layerSets, obsIndex: layerIndex } = obsSegmentationsSetsData
|
|
283
287
|
?.[layerScope]?.[channelScope] || {};
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
288
|
+
const {
|
|
289
|
+
obsSetColor,
|
|
290
|
+
obsColorEncoding,
|
|
291
|
+
obsSetSelection,
|
|
292
|
+
additionalObsSets,
|
|
293
|
+
spatialChannelColor,
|
|
294
|
+
spatialChannelOpacity,
|
|
295
|
+
} = segmentationChannelCoordination[0][layerScope][channelScope];
|
|
296
|
+
if (obsColorEncoding === 'spatialChannelColor') {
|
|
297
|
+
// All segments get the same static channel color
|
|
298
|
+
if (layerIndex && spatialChannelColor) {
|
|
299
|
+
const hex = rgbToHex(spatialChannelColor);
|
|
300
|
+
const ngCellColors = {};
|
|
301
|
+
|
|
302
|
+
if (obsSetSelection?.length > 0) {
|
|
303
|
+
// Only color the segments belonging to selected sets.
|
|
304
|
+
const mergedCellSets = mergeObsSets(layerSets, additionalObsSets);
|
|
305
|
+
const selectedIds = new Set();
|
|
306
|
+
obsSetSelection.forEach((setPath) => {
|
|
307
|
+
const rootNode = mergedCellSets?.tree?.find(n => n.name === setPath[0]);
|
|
308
|
+
const leafNode = setPath.length > 1
|
|
309
|
+
? rootNode?.children?.find(n => n.name === setPath[1])
|
|
310
|
+
: rootNode;
|
|
311
|
+
leafNode?.set?.forEach(([id]) => selectedIds.add(String(id)));
|
|
312
|
+
});
|
|
313
|
+
layerIndex.forEach((id) => {
|
|
314
|
+
if (selectedIds.has(String(id))) {
|
|
315
|
+
ngCellColors[id] = hex;
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
result[layerScope][channelScope] = ngCellColors;
|
|
320
|
+
result[layerScope].opacity = spatialChannelOpacity ?? 1.0;
|
|
321
|
+
}
|
|
322
|
+
} else if (layerSets && layerIndex) {
|
|
291
323
|
const mergedCellSets = mergeObsSets(layerSets, additionalObsSets);
|
|
292
324
|
const cellColors = getCellColors({
|
|
293
325
|
cellSets: mergedCellSets,
|
|
@@ -301,14 +333,8 @@ export function NeuroglancerSubscriber(props) {
|
|
|
301
333
|
cellColors.forEach((color, i) => {
|
|
302
334
|
ngCellColors[i] = rgbToHex(color);
|
|
303
335
|
});
|
|
304
|
-
/* // TODO: Is this necessary?
|
|
305
|
-
const obsColorIndices = treeToCellSetColorIndicesBySetNames(
|
|
306
|
-
mergedLayerSets,
|
|
307
|
-
obsSetSelection,
|
|
308
|
-
obsSetColor,
|
|
309
|
-
);
|
|
310
|
-
*/
|
|
311
336
|
result[layerScope][channelScope] = ngCellColors;
|
|
337
|
+
result[layerScope].opacity = spatialChannelOpacity ?? 1.0;
|
|
312
338
|
}
|
|
313
339
|
});
|
|
314
340
|
});
|
|
@@ -327,6 +353,7 @@ export function NeuroglancerSubscriber(props) {
|
|
|
327
353
|
// Obtain the Neuroglancer viewerState object.
|
|
328
354
|
const initalViewerState = useNeuroglancerViewerState(
|
|
329
355
|
theme,
|
|
356
|
+
showAxisLines,
|
|
330
357
|
segmentationLayerScopes,
|
|
331
358
|
segmentationChannelScopesByLayer,
|
|
332
359
|
segmentationLayerCoordination,
|
|
@@ -381,6 +408,9 @@ export function NeuroglancerSubscriber(props) {
|
|
|
381
408
|
orbit: spatialRotationOrbit,
|
|
382
409
|
});
|
|
383
410
|
|
|
411
|
+
// Track layer loading state for showing loading indicator
|
|
412
|
+
const [isLayersLoaded, setIsLayersLoaded] = useState(false);
|
|
413
|
+
|
|
384
414
|
// Track the last coord values we saw, and only mark "vitessce"
|
|
385
415
|
// when *those* actually change. This prevents cell set renders
|
|
386
416
|
// from spoofing the source.
|
|
@@ -512,6 +542,8 @@ export function NeuroglancerSubscriber(props) {
|
|
|
512
542
|
}, []);
|
|
513
543
|
|
|
514
544
|
const onSegmentClick = useCallback((value) => {
|
|
545
|
+
// Note: this callback is no longer called by the child component.
|
|
546
|
+
// Reference: https://github.com/vitessce/vitessce/pull/2439
|
|
515
547
|
if (value) {
|
|
516
548
|
const id = String(value);
|
|
517
549
|
const selectedCellIds = [id];
|
|
@@ -536,14 +568,19 @@ export function NeuroglancerSubscriber(props) {
|
|
|
536
568
|
setCellColorEncoding, setCellSetColor, setCellSetSelection,
|
|
537
569
|
]);
|
|
538
570
|
|
|
539
|
-
// Get the ultimate cellColorMapping to pass to NeuroglancerComp as a prop.
|
|
540
|
-
// For now, we take the first layer and channel for cell colors.
|
|
541
|
-
const cellColorMapping = useMemo(() => (segmentationColorMapping
|
|
542
|
-
?.[segmentationLayerScopes?.[0]]
|
|
543
|
-
?.[segmentationChannelScopesByLayer?.[segmentationLayerScopes?.[0]]?.[0]]
|
|
544
|
-
?? {}
|
|
545
|
-
), [segmentationColorMapping]);
|
|
571
|
+
// Get the ultimate cellColorMapping for each layer to pass to NeuroglancerComp as a prop.
|
|
546
572
|
|
|
573
|
+
const cellColorMappingByLayer = useMemo(() => {
|
|
574
|
+
const result = {};
|
|
575
|
+
segmentationLayerScopes?.forEach((layerScope) => {
|
|
576
|
+
const channelScope = segmentationChannelScopesByLayer?.[layerScope]?.[0];
|
|
577
|
+
result[layerScope] = {
|
|
578
|
+
colors: segmentationColorMapping?.[layerScope]?.[channelScope] ?? {},
|
|
579
|
+
opacity: segmentationColorMapping?.[layerScope]?.opacity ?? 1.0,
|
|
580
|
+
};
|
|
581
|
+
});
|
|
582
|
+
return result;
|
|
583
|
+
}, [segmentationColorMapping, segmentationLayerScopes, segmentationChannelScopesByLayer]);
|
|
547
584
|
|
|
548
585
|
// TODO: try to simplify using useMemoCustomComparison?
|
|
549
586
|
// This would allow us to refactor a lot of the checking-for-changes logic into a comparison function,
|
|
@@ -556,9 +593,6 @@ export function NeuroglancerSubscriber(props) {
|
|
|
556
593
|
return current;
|
|
557
594
|
}
|
|
558
595
|
|
|
559
|
-
const nextSegments = Object.keys(cellColorMapping);
|
|
560
|
-
const prevLayer = current?.layers?.[0] || {};
|
|
561
|
-
const prevSegments = prevLayer.segments || [];
|
|
562
596
|
const { projectionScale, projectionOrientation, position } = current;
|
|
563
597
|
|
|
564
598
|
// Did Vitessce coords change vs the *previous* render?
|
|
@@ -695,20 +729,24 @@ export function NeuroglancerSubscriber(props) {
|
|
|
695
729
|
lastInteractionSource.current = null;
|
|
696
730
|
}
|
|
697
731
|
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
732
|
+
const updatedLayers = current?.layers?.map((layer, idx) => {
|
|
733
|
+
const layerScope = segmentationLayerScopes?.[idx];
|
|
734
|
+
const layerColorMapping = cellColorMappingByLayer?.[layerScope]?.colors ?? {};
|
|
735
|
+
const layerSegments = Object.keys(layerColorMapping);
|
|
736
|
+
return {
|
|
737
|
+
...layer,
|
|
738
|
+
segments: layerSegments,
|
|
739
|
+
segmentColors: layerColorMapping,
|
|
740
|
+
objectAlpha: cellColorMappingByLayer?.[layerScope]?.opacity ?? 1.0,
|
|
741
|
+
};
|
|
742
|
+
}) ?? [];
|
|
704
743
|
|
|
705
744
|
const updated = {
|
|
706
745
|
...current,
|
|
707
746
|
projectionScale: nextProjectionScale,
|
|
708
747
|
projectionOrientation: nextOrientation,
|
|
709
748
|
position: nextPosition,
|
|
710
|
-
layers:
|
|
711
|
-
|| [])] : current?.layers,
|
|
749
|
+
layers: updatedLayers,
|
|
712
750
|
};
|
|
713
751
|
|
|
714
752
|
latestViewerStateRef.current = updated;
|
|
@@ -724,7 +762,7 @@ export function NeuroglancerSubscriber(props) {
|
|
|
724
762
|
};
|
|
725
763
|
|
|
726
764
|
return updated;
|
|
727
|
-
}, [
|
|
765
|
+
}, [cellColorMappingByLayer, spatialZoom, spatialRotationX, spatialRotationY,
|
|
728
766
|
spatialRotationZ, spatialTargetX, spatialTargetY, initalViewerState,
|
|
729
767
|
latestViewerStateIteration]);
|
|
730
768
|
|
|
@@ -732,6 +770,10 @@ export function NeuroglancerSubscriber(props) {
|
|
|
732
770
|
setCellHighlight(String(obsId));
|
|
733
771
|
}, [setCellHighlight]);
|
|
734
772
|
|
|
773
|
+
const handleLayerLoadingChange = useCallback((isLoaded) => {
|
|
774
|
+
setIsLayersLoaded(isLoaded);
|
|
775
|
+
}, []);
|
|
776
|
+
|
|
735
777
|
// TODO: if all cells are deselected, a black view is shown, rather we want to show empty NG view?
|
|
736
778
|
// if (!cellColorMapping || Object.keys(cellColorMapping).length === 0) {
|
|
737
779
|
// return;
|
|
@@ -741,6 +783,7 @@ export function NeuroglancerSubscriber(props) {
|
|
|
741
783
|
// console.log(derivedViewerState);
|
|
742
784
|
|
|
743
785
|
return (
|
|
786
|
+
|
|
744
787
|
<TitleInfo
|
|
745
788
|
title={title}
|
|
746
789
|
info={subtitle}
|
|
@@ -750,33 +793,43 @@ export function NeuroglancerSubscriber(props) {
|
|
|
750
793
|
closeButtonVisible={closeButtonVisible}
|
|
751
794
|
downloadButtonVisible={downloadButtonVisible}
|
|
752
795
|
removeGridComponent={removeGridComponent}
|
|
753
|
-
isReady={isReady}
|
|
796
|
+
isReady={isReady && isLayersLoaded}
|
|
754
797
|
errors={errors}
|
|
755
798
|
withPadding={false}
|
|
799
|
+
guideUrl={GUIDE_URL}
|
|
756
800
|
>
|
|
757
|
-
|
|
758
|
-
<div style={{ position: '
|
|
759
|
-
<
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
801
|
+
{hasLayers ? (
|
|
802
|
+
<div style={{ position: 'relative', width: '100%', height: '100%' }} ref={containerRef}>
|
|
803
|
+
<div style={{ position: 'absolute', top: 0, right: 0, zIndex: 50 }}>
|
|
804
|
+
<MultiLegend
|
|
805
|
+
theme="dark"
|
|
806
|
+
maxHeight={ngHeight}
|
|
807
|
+
|
|
808
|
+
// Segmentations
|
|
809
|
+
segmentationLayerScopes={segmentationLayerScopes}
|
|
810
|
+
segmentationLayerCoordination={segmentationLayerCoordination}
|
|
811
|
+
segmentationChannelScopesByLayer={segmentationChannelScopesByLayer}
|
|
812
|
+
segmentationChannelCoordination={segmentationChannelCoordination}
|
|
813
|
+
|
|
814
|
+
// Points
|
|
815
|
+
pointLayerScopes={pointLayerScopes}
|
|
816
|
+
pointLayerCoordination={pointLayerCoordination}
|
|
817
|
+
pointMultiIndicesData={pointMultiIndicesData}
|
|
818
|
+
/>
|
|
819
|
+
</div>
|
|
768
820
|
|
|
769
|
-
{hasLayers ? (
|
|
770
821
|
<NeuroglancerComp
|
|
771
822
|
classes={classes}
|
|
772
823
|
onSegmentClick={onSegmentClick}
|
|
773
824
|
onSelectHoveredCoords={onSegmentHighlight}
|
|
774
825
|
viewerState={derivedViewerState}
|
|
775
|
-
cellColorMapping={
|
|
826
|
+
cellColorMapping={cellColorMappingByLayer}
|
|
776
827
|
setViewerState={handleStateUpdate}
|
|
828
|
+
onLayerLoadingChange={handleLayerLoadingChange}
|
|
777
829
|
/>
|
|
778
|
-
|
|
779
|
-
|
|
830
|
+
</div>
|
|
831
|
+
) : null}
|
|
780
832
|
</TitleInfo>
|
|
833
|
+
|
|
781
834
|
);
|
|
782
835
|
}
|
package/src/ReactNeuroglancer.js
CHANGED
|
@@ -67,6 +67,10 @@ let viewerNoKey;
|
|
|
67
67
|
* @property {() => void} onSelectionDetailsStateChanged
|
|
68
68
|
* A function of the form `() => {}` to respond to selection changes in the viewer.
|
|
69
69
|
* @property {() => void} onViewerStateChanged
|
|
70
|
+
* @property {(isLoaded: boolean) => void} onLayerLoadingChange
|
|
71
|
+
* A function of the form `(isLoaded) => {}`, called when layer loading state changes.
|
|
72
|
+
* The `isLoaded` argument will be `true` when all segmentation layers have finished loading
|
|
73
|
+
* their data sources, or `false` when layers are still loading.
|
|
70
74
|
*
|
|
71
75
|
* @property {Array<Object>} callbacks
|
|
72
76
|
* // ngServer: string,
|
|
@@ -411,6 +415,7 @@ export default class Neuroglancer extends React.Component {
|
|
|
411
415
|
onVisibleChanged: null,
|
|
412
416
|
onSelectionDetailsStateChanged: null,
|
|
413
417
|
onViewerStateChanged: null,
|
|
418
|
+
onLayerLoadingChange: null,
|
|
414
419
|
key: null,
|
|
415
420
|
callbacks: [],
|
|
416
421
|
ngServer: 'https://neuroglancer-demo.appspot.com/',
|
|
@@ -427,7 +432,7 @@ export default class Neuroglancer extends React.Component {
|
|
|
427
432
|
this.disposers = [];
|
|
428
433
|
this.prevColorOverrides = new Set();
|
|
429
434
|
this.overrideColorsById = Object.create(null);
|
|
430
|
-
this.
|
|
435
|
+
this.allKnownIdsByLayer = {};
|
|
431
436
|
}
|
|
432
437
|
|
|
433
438
|
minimalPoseSnapshot = () => {
|
|
@@ -476,30 +481,36 @@ export default class Neuroglancer extends React.Component {
|
|
|
476
481
|
};
|
|
477
482
|
|
|
478
483
|
/* To add colors to the segments, turning unselected to grey */
|
|
479
|
-
applyColorsAndVisibility = (
|
|
484
|
+
applyColorsAndVisibility = (cellColorMappingByLayer) => {
|
|
480
485
|
if (!this.viewer) return;
|
|
481
|
-
//
|
|
482
|
-
// that drop out of the current selection.
|
|
483
|
-
const selected = { ...(cellColorMapping || {}) }; // clone, don't mutate props
|
|
484
|
-
for (const id of Object.keys(selected)) this.allKnownIds.add(id);
|
|
485
|
-
// If empty on first call, seed from initial segmentColors (if present)
|
|
486
|
-
if (this.allKnownIds.size === 0) {
|
|
487
|
-
const init = this.props.viewerState?.layers?.[0]?.segmentColors || {};
|
|
488
|
-
for (const id of Object.keys(init)) this.allKnownIds.add(id);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Build a full color table: selected keep their hex, others grey
|
|
492
|
-
const fullSegmentColors = {};
|
|
493
|
-
for (const id of this.allKnownIds) {
|
|
494
|
-
fullSegmentColors[id] = selected[id] || GREY_HEX;
|
|
495
|
-
}
|
|
496
|
-
// Patch layers with the new segmentColors (pose untouched)
|
|
486
|
+
// Build full color table per layer
|
|
497
487
|
const baseLayers = (this.props.viewerState?.layers)
|
|
498
488
|
?? (this.viewer.state.toJSON().layers || []);
|
|
499
489
|
|
|
500
|
-
const newLayers = baseLayers.map((layer
|
|
501
|
-
// if
|
|
502
|
-
|
|
490
|
+
const newLayers = baseLayers.map((layer) => {
|
|
491
|
+
// Match layerScope by checking if the NG layer name contains the scope key.
|
|
492
|
+
// NG layer names are of the form:
|
|
493
|
+
// "obsSegmentations-init_A_obsSegmentations_0-init_A_obsSegmentations_0"
|
|
494
|
+
const layerScope = Object.keys(cellColorMappingByLayer).find(scope => layer.name?.includes(scope));
|
|
495
|
+
|
|
496
|
+
const selected = { ...(cellColorMappingByLayer[layerScope]?.colors || {}) };
|
|
497
|
+
|
|
498
|
+
// Track all known IDs for this layer scope
|
|
499
|
+
if (!this.allKnownIdsByLayer) this.allKnownIdsByLayer = {};
|
|
500
|
+
if (!this.allKnownIdsByLayer[layerScope]) {
|
|
501
|
+
this.allKnownIdsByLayer[layerScope] = new Set();
|
|
502
|
+
}
|
|
503
|
+
for (const id of Object.keys(selected)) {
|
|
504
|
+
this.allKnownIdsByLayer[layerScope].add(id);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Build a full color table: selected keep their hex, others grey
|
|
508
|
+
const fullSegmentColors = {};
|
|
509
|
+
for (const id of this.allKnownIdsByLayer[layerScope] || []) {
|
|
510
|
+
fullSegmentColors[id] = selected[id] || GREY_HEX;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (layer.type === 'segmentation') {
|
|
503
514
|
return { ...layer, segmentColors: fullSegmentColors };
|
|
504
515
|
}
|
|
505
516
|
return layer;
|
|
@@ -619,12 +630,46 @@ export default class Neuroglancer extends React.Component {
|
|
|
619
630
|
viewerNoKey = this.viewer;
|
|
620
631
|
}
|
|
621
632
|
|
|
622
|
-
|
|
633
|
+
const { visibleChunksChanged } = this.viewer.chunkQueueManager;
|
|
634
|
+
let firstChunkLoaded = false;
|
|
635
|
+
this.disposers.push(visibleChunksChanged.add(() => {
|
|
636
|
+
if (!firstChunkLoaded) {
|
|
637
|
+
for (const layer of this.viewer.layerManager.managedLayers) {
|
|
638
|
+
if (layer.layer instanceof SegmentationUserLayer) {
|
|
639
|
+
const hasVisibleChunk = layer.layer.renderLayers?.some((rl) => {
|
|
640
|
+
const {
|
|
641
|
+
numVisibleChunksAvailable,
|
|
642
|
+
numVisibleChunksNeeded,
|
|
643
|
+
} = rl.layerChunkProgressInfo || {};
|
|
644
|
+
if (!numVisibleChunksNeeded || !numVisibleChunksAvailable) return false;
|
|
645
|
+
// Neuroglancer only shows chunks when a certain % is loaded.
|
|
646
|
+
// The 0.25 is from testing different values, can be reduced to 0.2 to shorten loader time
|
|
647
|
+
return (numVisibleChunksAvailable / numVisibleChunksNeeded) > 0.25;
|
|
648
|
+
});
|
|
649
|
+
if (hasVisibleChunk) {
|
|
650
|
+
firstChunkLoaded = true;
|
|
651
|
+
// Two frames to avoid flash while the following two happens
|
|
652
|
+
// Neuroglancer issues WebGL draw calls
|
|
653
|
+
requestAnimationFrame(() => {
|
|
654
|
+
// GPU has painted, pixels visible on screen
|
|
655
|
+
requestAnimationFrame(() => {
|
|
656
|
+
this.props.onLayerLoadingChange?.(true);
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}));
|
|
665
|
+
this.disposers.push(() => { firstChunkLoaded = false; });
|
|
666
|
+
|
|
667
|
+
// TODO: This is purely for debugging - exposes the NG viewer to be tested via console
|
|
623
668
|
// window.viewer = this.viewer;
|
|
624
669
|
}
|
|
625
670
|
|
|
626
671
|
componentDidUpdate(prevProps, prevState) {
|
|
627
|
-
const { viewerState, cellColorMapping } = this.props;
|
|
672
|
+
const { viewerState, cellColorMapping: cellColorMappingByLayer } = this.props;
|
|
628
673
|
// The restoreState() call clears the 'selected' (hovered on) segment, which is needed
|
|
629
674
|
// by Neuroglancer's code to toggle segment visibilty on a mouse click. To free the user
|
|
630
675
|
// from having to move the mouse before clicking, save the selected segment and restore
|
|
@@ -661,6 +706,27 @@ export default class Neuroglancer extends React.Component {
|
|
|
661
706
|
if (layer.layer instanceof SegmentationUserLayer) {
|
|
662
707
|
const { segmentSelectionState } = layer.layer.displayState;
|
|
663
708
|
segmentSelectionState.set(selectedSegments[layer.name]);
|
|
709
|
+
const layerScope = Object.keys(cellColorMappingByLayer).find(
|
|
710
|
+
scope => layer.name?.includes(scope),
|
|
711
|
+
);
|
|
712
|
+
if (layerScope) {
|
|
713
|
+
const opacity = cellColorMappingByLayer[layerScope]?.opacity ?? 1.0;
|
|
714
|
+
layer.layer.displayState.objectAlpha.value = opacity;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// Update annotation layer shaders from viewerState config,
|
|
718
|
+
// skipping update if shader is unchanged to avoid costly re-renders
|
|
719
|
+
if (layer.layer instanceof AnnotationUserLayer) {
|
|
720
|
+
const matchingLayer = (viewerState?.layers || []).find(
|
|
721
|
+
l => l.name === layer.name,
|
|
722
|
+
);
|
|
723
|
+
if (matchingLayer?.shader) {
|
|
724
|
+
/* eslint-disable-next-line no-underscore-dangle */
|
|
725
|
+
const currentShader = layer.layer.annotationDisplayState.shader.value_;
|
|
726
|
+
if (currentShader !== matchingLayer.shader) {
|
|
727
|
+
layer.layer.annotationDisplayState.shader.value = matchingLayer.shader;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
664
730
|
}
|
|
665
731
|
}
|
|
666
732
|
|
|
@@ -702,8 +768,8 @@ export default class Neuroglancer extends React.Component {
|
|
|
702
768
|
this.withoutEmitting(() => {
|
|
703
769
|
const layers = Array.isArray(viewerState.layers) ? viewerState.layers : [];
|
|
704
770
|
this.viewer.state.restoreState({ layers });
|
|
705
|
-
if (
|
|
706
|
-
this.applyColorsAndVisibility(
|
|
771
|
+
if (cellColorMappingByLayer && Object.keys(cellColorMappingByLayer).length) {
|
|
772
|
+
this.applyColorsAndVisibility(cellColorMappingByLayer);
|
|
707
773
|
}
|
|
708
774
|
});
|
|
709
775
|
}
|
|
@@ -711,13 +777,16 @@ export default class Neuroglancer extends React.Component {
|
|
|
711
777
|
// If colors changed (but layers didn’t): re-apply colors
|
|
712
778
|
// this was to avid NG randomly assigning colors to the segments by resetting them
|
|
713
779
|
const prevSize = prevProps.cellColorMapping
|
|
714
|
-
? Object.
|
|
715
|
-
|
|
716
|
-
const
|
|
780
|
+
? Object.values(prevProps.cellColorMapping)
|
|
781
|
+
.reduce((acc, v) => acc + Object.keys(v?.colors || {}).length, 0) : 0;
|
|
782
|
+
const currSize = cellColorMappingByLayer
|
|
783
|
+
? Object.values(cellColorMappingByLayer)
|
|
784
|
+
.reduce((acc, v) => acc + Object.keys(v?.colors || {}).length, 0) : 0;
|
|
785
|
+
const mappingRefChanged = prevProps.cellColorMapping !== this.props.cellColorMapping;
|
|
717
786
|
if (!this.didLayersChange(prevVS, viewerState)
|
|
718
787
|
&& (mappingRefChanged || prevSize !== currSize)) {
|
|
719
788
|
this.withoutEmitting(() => {
|
|
720
|
-
this.applyColorsAndVisibility(
|
|
789
|
+
this.applyColorsAndVisibility(cellColorMappingByLayer);
|
|
721
790
|
});
|
|
722
791
|
}
|
|
723
792
|
|
|
@@ -725,7 +794,7 @@ export default class Neuroglancer extends React.Component {
|
|
|
725
794
|
// We only restore layers (not pose) when sources change OR on the first time segments appear.
|
|
726
795
|
const stripSegFields = layers => (layers || []).map((l) => {
|
|
727
796
|
if (!l) return l;
|
|
728
|
-
const { segments, segmentColors, ...rest } = l;
|
|
797
|
+
const { segments, segmentColors, objectAlpha, ...rest } = l;
|
|
729
798
|
return rest; // ignore segments + segmentColors for comparison
|
|
730
799
|
});
|
|
731
800
|
|