@vitessce/neuroglancer 4.0.0-test.0 → 4.0.0-test.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-tsc/Neuroglancer.d.ts.map +1 -1
- package/dist-tsc/Neuroglancer.js +28 -6
- package/dist-tsc/NeuroglancerSubscriber.d.ts.map +1 -1
- package/dist-tsc/NeuroglancerSubscriber.js +67 -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 +30 -5
- package/src/NeuroglancerSubscriber.js +97 -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/ReactNeuroglancer-Bg6f79hO.js +0 -67181
- package/dist/index-DZhbMDug.js +0 -35064
- package/dist/index.js +0 -4
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) {
|
|
@@ -31,13 +33,35 @@ export class NeuroglancerComp extends PureComponent {
|
|
|
31
33
|
// Mount
|
|
32
34
|
const { viewer } = viewerRef;
|
|
33
35
|
this.prevMouseStateChanged = viewer.mouseState.changed;
|
|
34
|
-
|
|
36
|
+
// For now, can omit the sliceView bindings, as we only use perspectiveView
|
|
37
|
+
// viewer.inputEventBindings.sliceView.set('at:dblclick0', () => {});
|
|
35
38
|
viewer.inputEventBindings.perspectiveView.set('at:dblclick0', () => {});
|
|
36
39
|
|
|
37
|
-
//
|
|
40
|
+
// Disable space interaction to prevent triggering 4panels layout.
|
|
38
41
|
viewer.inputEventBindings.sliceView.set('at:space', () => {});
|
|
39
42
|
viewer.inputEventBindings.perspectiveView.set('at:space', () => {});
|
|
40
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);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (map.parents) {
|
|
59
|
+
map.parents.forEach(p => remapWheelToZoom(p));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
remapWheelToZoom(viewer.inputEventBindings.perspectiveView);
|
|
64
|
+
|
|
41
65
|
this.prevHoverHandler = () => {
|
|
42
66
|
if (viewer.mouseState.pickedValue !== undefined) {
|
|
43
67
|
const pickedSegment = viewer.mouseState.pickedValue;
|
|
@@ -68,7 +92,7 @@ export class NeuroglancerComp extends PureComponent {
|
|
|
68
92
|
}
|
|
69
93
|
|
|
70
94
|
render() {
|
|
71
|
-
const { classes, viewerState, cellColorMapping } = this.props;
|
|
95
|
+
const { classes, viewerState, cellColorMapping, onLayerLoadingChange } = this.props;
|
|
72
96
|
|
|
73
97
|
return (
|
|
74
98
|
<>
|
|
@@ -79,6 +103,7 @@ export class NeuroglancerComp extends PureComponent {
|
|
|
79
103
|
brainMapsClientId="NOT_A_VALID_ID"
|
|
80
104
|
viewerState={viewerState}
|
|
81
105
|
onViewerStateChanged={this.onViewerStateChanged}
|
|
106
|
+
onLayerLoadingChange={onLayerLoadingChange}
|
|
82
107
|
bundleRoot={this.bundleRoot}
|
|
83
108
|
cellColorMapping={cellColorMapping}
|
|
84
109
|
ref={this.onRef}
|
|
@@ -79,6 +79,7 @@ export function NeuroglancerSubscriber(props) {
|
|
|
79
79
|
downloadButtonVisible,
|
|
80
80
|
removeGridComponent,
|
|
81
81
|
theme,
|
|
82
|
+
showAxisLines = false,
|
|
82
83
|
title = 'Spatial',
|
|
83
84
|
subtitle = 'Powered by Neuroglancer',
|
|
84
85
|
helpText = ViewHelpMapping.NEUROGLANCER,
|
|
@@ -207,6 +208,7 @@ export function NeuroglancerSubscriber(props) {
|
|
|
207
208
|
CoordinationType.TOOLTIPS_VISIBLE,
|
|
208
209
|
CoordinationType.TOOLTIP_CROSSHAIRS_VISIBLE,
|
|
209
210
|
CoordinationType.LEGEND_VISIBLE,
|
|
211
|
+
CoordinationType.SPATIAL_POINT_STROKE_WIDTH,
|
|
210
212
|
],
|
|
211
213
|
coordinationScopes,
|
|
212
214
|
coordinationScopesBy,
|
|
@@ -283,13 +285,41 @@ export function NeuroglancerSubscriber(props) {
|
|
|
283
285
|
segmentationChannelScopesByLayer?.[layerScope]?.forEach((channelScope) => {
|
|
284
286
|
const { obsSets: layerSets, obsIndex: layerIndex } = obsSegmentationsSetsData
|
|
285
287
|
?.[layerScope]?.[channelScope] || {};
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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) {
|
|
293
323
|
const mergedCellSets = mergeObsSets(layerSets, additionalObsSets);
|
|
294
324
|
const cellColors = getCellColors({
|
|
295
325
|
cellSets: mergedCellSets,
|
|
@@ -303,14 +333,8 @@ export function NeuroglancerSubscriber(props) {
|
|
|
303
333
|
cellColors.forEach((color, i) => {
|
|
304
334
|
ngCellColors[i] = rgbToHex(color);
|
|
305
335
|
});
|
|
306
|
-
/* // TODO: Is this necessary?
|
|
307
|
-
const obsColorIndices = treeToCellSetColorIndicesBySetNames(
|
|
308
|
-
mergedLayerSets,
|
|
309
|
-
obsSetSelection,
|
|
310
|
-
obsSetColor,
|
|
311
|
-
);
|
|
312
|
-
*/
|
|
313
336
|
result[layerScope][channelScope] = ngCellColors;
|
|
337
|
+
result[layerScope].opacity = spatialChannelOpacity ?? 1.0;
|
|
314
338
|
}
|
|
315
339
|
});
|
|
316
340
|
});
|
|
@@ -329,6 +353,7 @@ export function NeuroglancerSubscriber(props) {
|
|
|
329
353
|
// Obtain the Neuroglancer viewerState object.
|
|
330
354
|
const initalViewerState = useNeuroglancerViewerState(
|
|
331
355
|
theme,
|
|
356
|
+
showAxisLines,
|
|
332
357
|
segmentationLayerScopes,
|
|
333
358
|
segmentationChannelScopesByLayer,
|
|
334
359
|
segmentationLayerCoordination,
|
|
@@ -383,6 +408,9 @@ export function NeuroglancerSubscriber(props) {
|
|
|
383
408
|
orbit: spatialRotationOrbit,
|
|
384
409
|
});
|
|
385
410
|
|
|
411
|
+
// Track layer loading state for showing loading indicator
|
|
412
|
+
const [isLayersLoaded, setIsLayersLoaded] = useState(false);
|
|
413
|
+
|
|
386
414
|
// Track the last coord values we saw, and only mark "vitessce"
|
|
387
415
|
// when *those* actually change. This prevents cell set renders
|
|
388
416
|
// from spoofing the source.
|
|
@@ -540,14 +568,19 @@ export function NeuroglancerSubscriber(props) {
|
|
|
540
568
|
setCellColorEncoding, setCellSetColor, setCellSetSelection,
|
|
541
569
|
]);
|
|
542
570
|
|
|
543
|
-
// Get the ultimate cellColorMapping to pass to NeuroglancerComp as a prop.
|
|
544
|
-
// For now, we take the first layer and channel for cell colors.
|
|
545
|
-
const cellColorMapping = useMemo(() => (segmentationColorMapping
|
|
546
|
-
?.[segmentationLayerScopes?.[0]]
|
|
547
|
-
?.[segmentationChannelScopesByLayer?.[segmentationLayerScopes?.[0]]?.[0]]
|
|
548
|
-
?? {}
|
|
549
|
-
), [segmentationColorMapping]);
|
|
571
|
+
// Get the ultimate cellColorMapping for each layer to pass to NeuroglancerComp as a prop.
|
|
550
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]);
|
|
551
584
|
|
|
552
585
|
// TODO: try to simplify using useMemoCustomComparison?
|
|
553
586
|
// This would allow us to refactor a lot of the checking-for-changes logic into a comparison function,
|
|
@@ -560,9 +593,6 @@ export function NeuroglancerSubscriber(props) {
|
|
|
560
593
|
return current;
|
|
561
594
|
}
|
|
562
595
|
|
|
563
|
-
const nextSegments = Object.keys(cellColorMapping);
|
|
564
|
-
const prevLayer = current?.layers?.[0] || {};
|
|
565
|
-
const prevSegments = prevLayer.segments || [];
|
|
566
596
|
const { projectionScale, projectionOrientation, position } = current;
|
|
567
597
|
|
|
568
598
|
// Did Vitessce coords change vs the *previous* render?
|
|
@@ -699,20 +729,24 @@ export function NeuroglancerSubscriber(props) {
|
|
|
699
729
|
lastInteractionSource.current = null;
|
|
700
730
|
}
|
|
701
731
|
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
+
}) ?? [];
|
|
708
743
|
|
|
709
744
|
const updated = {
|
|
710
745
|
...current,
|
|
711
746
|
projectionScale: nextProjectionScale,
|
|
712
747
|
projectionOrientation: nextOrientation,
|
|
713
748
|
position: nextPosition,
|
|
714
|
-
layers:
|
|
715
|
-
|| [])] : current?.layers,
|
|
749
|
+
layers: updatedLayers,
|
|
716
750
|
};
|
|
717
751
|
|
|
718
752
|
latestViewerStateRef.current = updated;
|
|
@@ -728,7 +762,7 @@ export function NeuroglancerSubscriber(props) {
|
|
|
728
762
|
};
|
|
729
763
|
|
|
730
764
|
return updated;
|
|
731
|
-
}, [
|
|
765
|
+
}, [cellColorMappingByLayer, spatialZoom, spatialRotationX, spatialRotationY,
|
|
732
766
|
spatialRotationZ, spatialTargetX, spatialTargetY, initalViewerState,
|
|
733
767
|
latestViewerStateIteration]);
|
|
734
768
|
|
|
@@ -736,6 +770,10 @@ export function NeuroglancerSubscriber(props) {
|
|
|
736
770
|
setCellHighlight(String(obsId));
|
|
737
771
|
}, [setCellHighlight]);
|
|
738
772
|
|
|
773
|
+
const handleLayerLoadingChange = useCallback((isLoaded) => {
|
|
774
|
+
setIsLayersLoaded(isLoaded);
|
|
775
|
+
}, []);
|
|
776
|
+
|
|
739
777
|
// TODO: if all cells are deselected, a black view is shown, rather we want to show empty NG view?
|
|
740
778
|
// if (!cellColorMapping || Object.keys(cellColorMapping).length === 0) {
|
|
741
779
|
// return;
|
|
@@ -745,6 +783,7 @@ export function NeuroglancerSubscriber(props) {
|
|
|
745
783
|
// console.log(derivedViewerState);
|
|
746
784
|
|
|
747
785
|
return (
|
|
786
|
+
|
|
748
787
|
<TitleInfo
|
|
749
788
|
title={title}
|
|
750
789
|
info={subtitle}
|
|
@@ -754,34 +793,43 @@ export function NeuroglancerSubscriber(props) {
|
|
|
754
793
|
closeButtonVisible={closeButtonVisible}
|
|
755
794
|
downloadButtonVisible={downloadButtonVisible}
|
|
756
795
|
removeGridComponent={removeGridComponent}
|
|
757
|
-
isReady={isReady}
|
|
796
|
+
isReady={isReady && isLayersLoaded}
|
|
758
797
|
errors={errors}
|
|
759
798
|
withPadding={false}
|
|
760
799
|
guideUrl={GUIDE_URL}
|
|
761
800
|
>
|
|
762
|
-
|
|
763
|
-
<div style={{ position: '
|
|
764
|
-
<
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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>
|
|
773
820
|
|
|
774
|
-
{hasLayers ? (
|
|
775
821
|
<NeuroglancerComp
|
|
776
822
|
classes={classes}
|
|
777
823
|
onSegmentClick={onSegmentClick}
|
|
778
824
|
onSelectHoveredCoords={onSegmentHighlight}
|
|
779
825
|
viewerState={derivedViewerState}
|
|
780
|
-
cellColorMapping={
|
|
826
|
+
cellColorMapping={cellColorMappingByLayer}
|
|
781
827
|
setViewerState={handleStateUpdate}
|
|
828
|
+
onLayerLoadingChange={handleLayerLoadingChange}
|
|
782
829
|
/>
|
|
783
|
-
|
|
784
|
-
|
|
830
|
+
</div>
|
|
831
|
+
) : null}
|
|
785
832
|
</TitleInfo>
|
|
833
|
+
|
|
786
834
|
);
|
|
787
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
|
|
|
@@ -106,6 +106,7 @@ export function toNgLayerName(dataType, layerScope, channelScope = null) {
|
|
|
106
106
|
*/
|
|
107
107
|
export function useNeuroglancerViewerState(
|
|
108
108
|
theme,
|
|
109
|
+
showAxisLines,
|
|
109
110
|
segmentationLayerScopes,
|
|
110
111
|
segmentationChannelScopesByLayer,
|
|
111
112
|
segmentationLayerCoordination,
|
|
@@ -140,20 +141,35 @@ export function useNeuroglancerViewerState(
|
|
|
140
141
|
const {
|
|
141
142
|
spatialChannelVisible,
|
|
142
143
|
} = channelCoordination || {};
|
|
144
|
+
const { source: ngSource, ...otherNgOptions } = layerData.neuroglancerOptions ?? {};
|
|
145
|
+
|
|
146
|
+
// Build source: if neuroglancerOptions has subsources
|
|
147
|
+
const hasNgSourceOptions = layerData.neuroglancerOptions?.subsources
|
|
148
|
+
|| layerData.neuroglancerOptions?.enableDefaultSubsources !== undefined;
|
|
149
|
+
|
|
150
|
+
const source = hasNgSourceOptions
|
|
151
|
+
? {
|
|
152
|
+
url: toPrecomputedSource(layerUrl),
|
|
153
|
+
subsources: layerData.neuroglancerOptions.subsources,
|
|
154
|
+
enableDefaultSubsources: layerData.neuroglancerOptions.enableDefaultSubsources
|
|
155
|
+
?? false,
|
|
156
|
+
}
|
|
157
|
+
: toPrecomputedSource(layerUrl);
|
|
158
|
+
|
|
143
159
|
result = {
|
|
144
160
|
...result,
|
|
145
161
|
layers: [
|
|
146
162
|
...result.layers,
|
|
147
163
|
{
|
|
148
164
|
type: 'segmentation',
|
|
149
|
-
source
|
|
165
|
+
source,
|
|
150
166
|
segments: [],
|
|
151
167
|
name: toNgLayerName(DataType.OBS_SEGMENTATIONS, layerScope, channelScope),
|
|
152
168
|
visible: spatialLayerVisible && spatialChannelVisible, // Both layer and channel
|
|
153
169
|
// visibility must be true for the layer to be visible.
|
|
154
170
|
// TODO: update this to extract specific properties from
|
|
155
171
|
// neuroglancerOptions as needed.
|
|
156
|
-
...
|
|
172
|
+
...otherNgOptions,
|
|
157
173
|
},
|
|
158
174
|
],
|
|
159
175
|
};
|
|
@@ -180,6 +196,7 @@ export function useNeuroglancerViewerState(
|
|
|
180
196
|
featureSelection,
|
|
181
197
|
featureFilterMode,
|
|
182
198
|
featureColor,
|
|
199
|
+
spatialPointStrokeWidth,
|
|
183
200
|
} = layerCoordination || {};
|
|
184
201
|
|
|
185
202
|
// Dynamically construct the shader based on the color encoding
|
|
@@ -196,6 +213,7 @@ export function useNeuroglancerViewerState(
|
|
|
196
213
|
|
|
197
214
|
featureIndexProp: layerData.neuroglancerOptions?.featureIndexProp,
|
|
198
215
|
pointIndexProp: layerData.neuroglancerOptions?.pointIndexProp,
|
|
216
|
+
pointMarkerBorderWidth: spatialPointStrokeWidth ?? 0.0,
|
|
199
217
|
});
|
|
200
218
|
|
|
201
219
|
result = {
|
|
@@ -235,6 +253,7 @@ export function useNeuroglancerViewerState(
|
|
|
235
253
|
return result;
|
|
236
254
|
}, {
|
|
237
255
|
theme,
|
|
256
|
+
showAxisLines,
|
|
238
257
|
segmentationLayerScopes,
|
|
239
258
|
segmentationChannelScopesByLayer,
|
|
240
259
|
segmentationLayerCoordination,
|