@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/dist/index.js
CHANGED
|
@@ -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":"
|
|
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,sBAqBC;CACF"}
|
package/dist-tsc/Neuroglancer.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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,16 +76,13 @@ export class NeuroglancerComp extends PureComponent {
|
|
|
73
76
|
setViewerState(nextState);
|
|
74
77
|
}
|
|
75
78
|
componentDidUpdate(prevProps) {
|
|
76
|
-
const {
|
|
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
|
}
|
|
83
83
|
}
|
|
84
84
|
render() {
|
|
85
|
-
const { classes, viewerState, cellColorMapping } = this.props;
|
|
86
|
-
return (_jsxs(_Fragment, { children: [_jsx(NeuroglancerGlobalStyles, { classes: classes }), _jsx("div", { className: classes.neuroglancerWrapper, children: _jsx(Suspense, { fallback: _jsx("div", { children: "Loading..." }), children: _jsx(LazyReactNeuroglancer, { brainMapsClientId: "NOT_A_VALID_ID", viewerState: viewerState, onViewerStateChanged: this.onViewerStateChanged, bundleRoot: this.bundleRoot, cellColorMapping: cellColorMapping, ref: this.onRef }) }) })] }));
|
|
85
|
+
const { classes, viewerState, cellColorMapping, onLayerLoadingChange } = this.props;
|
|
86
|
+
return (_jsxs(_Fragment, { children: [_jsx(NeuroglancerGlobalStyles, { classes: classes }), _jsx("div", { className: classes.neuroglancerWrapper, children: _jsx(Suspense, { fallback: _jsx("div", { children: "Loading..." }), children: _jsx(LazyReactNeuroglancer, { brainMapsClientId: "NOT_A_VALID_ID", viewerState: viewerState, onViewerStateChanged: this.onViewerStateChanged, onLayerLoadingChange: onLayerLoadingChange, bundleRoot: this.bundleRoot, cellColorMapping: cellColorMapping, ref: this.onRef }) }) })] }));
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NeuroglancerSubscriber.d.ts","sourceRoot":"","sources":["../src/NeuroglancerSubscriber.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"NeuroglancerSubscriber.d.ts","sourceRoot":"","sources":["../src/NeuroglancerSubscriber.js"],"names":[],"mappings":"AAwEA,gEA0vBC"}
|
|
@@ -17,6 +17,7 @@ const ZOOM_EPS = 1e-2;
|
|
|
17
17
|
const ROTATION_EPS = 1e-3;
|
|
18
18
|
const TARGET_EPS = 0.5;
|
|
19
19
|
const NG_ROT_COOLDOWN_MS = 120;
|
|
20
|
+
const GUIDE_URL = 'https://vitessce.io/docs/ng-guide/';
|
|
20
21
|
const LAST_INTERACTION_SOURCE = {
|
|
21
22
|
vitessce: 'vitessce',
|
|
22
23
|
neuroglancer: 'neuroglancer',
|
|
@@ -27,7 +28,7 @@ function rgbToHex(rgb) {
|
|
|
27
28
|
: `#${rgb.map(c => c.toString(16).padStart(2, '0')).join('')}`);
|
|
28
29
|
}
|
|
29
30
|
export function NeuroglancerSubscriber(props) {
|
|
30
|
-
const { uuid, coordinationScopes: coordinationScopesRaw, coordinationScopesBy: coordinationScopesByRaw, closeButtonVisible, downloadButtonVisible, removeGridComponent, theme, title = 'Spatial', subtitle = 'Powered by Neuroglancer', helpText = ViewHelpMapping.NEUROGLANCER,
|
|
31
|
+
const { uuid, coordinationScopes: coordinationScopesRaw, coordinationScopesBy: coordinationScopesByRaw, closeButtonVisible, downloadButtonVisible, removeGridComponent, theme, showAxisLines = false, title = 'Spatial', subtitle = 'Powered by Neuroglancer', helpText = ViewHelpMapping.NEUROGLANCER,
|
|
31
32
|
// Note: this is a temporary mechanism
|
|
32
33
|
// to pass an initial NG camera state.
|
|
33
34
|
// Ideally, all camera state should be passed via
|
|
@@ -94,6 +95,7 @@ export function NeuroglancerSubscriber(props) {
|
|
|
94
95
|
CoordinationType.TOOLTIPS_VISIBLE,
|
|
95
96
|
CoordinationType.TOOLTIP_CROSSHAIRS_VISIBLE,
|
|
96
97
|
CoordinationType.LEGEND_VISIBLE,
|
|
98
|
+
CoordinationType.SPATIAL_POINT_STROKE_WIDTH,
|
|
97
99
|
], coordinationScopes, coordinationScopesBy, CoordinationType.POINT_LAYER);
|
|
98
100
|
// Points data
|
|
99
101
|
const [obsPointsData, obsPointsDataStatus, obsPointsUrls, obsPointsErrors] = useMultiObsPoints(coordinationScopes, coordinationScopesBy, loaders, dataset, mergeCoordination, uuid);
|
|
@@ -132,8 +134,34 @@ export function NeuroglancerSubscriber(props) {
|
|
|
132
134
|
result[layerScope] = {};
|
|
133
135
|
segmentationChannelScopesByLayer?.[layerScope]?.forEach((channelScope) => {
|
|
134
136
|
const { obsSets: layerSets, obsIndex: layerIndex } = obsSegmentationsSetsData?.[layerScope]?.[channelScope] || {};
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
const { obsSetColor, obsColorEncoding, obsSetSelection, additionalObsSets, spatialChannelColor, spatialChannelOpacity, } = segmentationChannelCoordination[0][layerScope][channelScope];
|
|
138
|
+
if (obsColorEncoding === 'spatialChannelColor') {
|
|
139
|
+
// All segments get the same static channel color
|
|
140
|
+
if (layerIndex && spatialChannelColor) {
|
|
141
|
+
const hex = rgbToHex(spatialChannelColor);
|
|
142
|
+
const ngCellColors = {};
|
|
143
|
+
if (obsSetSelection?.length > 0) {
|
|
144
|
+
// Only color the segments belonging to selected sets.
|
|
145
|
+
const mergedCellSets = mergeObsSets(layerSets, additionalObsSets);
|
|
146
|
+
const selectedIds = new Set();
|
|
147
|
+
obsSetSelection.forEach((setPath) => {
|
|
148
|
+
const rootNode = mergedCellSets?.tree?.find(n => n.name === setPath[0]);
|
|
149
|
+
const leafNode = setPath.length > 1
|
|
150
|
+
? rootNode?.children?.find(n => n.name === setPath[1])
|
|
151
|
+
: rootNode;
|
|
152
|
+
leafNode?.set?.forEach(([id]) => selectedIds.add(String(id)));
|
|
153
|
+
});
|
|
154
|
+
layerIndex.forEach((id) => {
|
|
155
|
+
if (selectedIds.has(String(id))) {
|
|
156
|
+
ngCellColors[id] = hex;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
result[layerScope][channelScope] = ngCellColors;
|
|
161
|
+
result[layerScope].opacity = spatialChannelOpacity ?? 1.0;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else if (layerSets && layerIndex) {
|
|
137
165
|
const mergedCellSets = mergeObsSets(layerSets, additionalObsSets);
|
|
138
166
|
const cellColors = getCellColors({
|
|
139
167
|
cellSets: mergedCellSets,
|
|
@@ -147,14 +175,8 @@ export function NeuroglancerSubscriber(props) {
|
|
|
147
175
|
cellColors.forEach((color, i) => {
|
|
148
176
|
ngCellColors[i] = rgbToHex(color);
|
|
149
177
|
});
|
|
150
|
-
/* // TODO: Is this necessary?
|
|
151
|
-
const obsColorIndices = treeToCellSetColorIndicesBySetNames(
|
|
152
|
-
mergedLayerSets,
|
|
153
|
-
obsSetSelection,
|
|
154
|
-
obsSetColor,
|
|
155
|
-
);
|
|
156
|
-
*/
|
|
157
178
|
result[layerScope][channelScope] = ngCellColors;
|
|
179
|
+
result[layerScope].opacity = spatialChannelOpacity ?? 1.0;
|
|
158
180
|
}
|
|
159
181
|
});
|
|
160
182
|
});
|
|
@@ -169,7 +191,7 @@ export function NeuroglancerSubscriber(props) {
|
|
|
169
191
|
theme,
|
|
170
192
|
}, customIsEqualForCellColors);
|
|
171
193
|
// Obtain the Neuroglancer viewerState object.
|
|
172
|
-
const initalViewerState = useNeuroglancerViewerState(theme, segmentationLayerScopes, segmentationChannelScopesByLayer, segmentationLayerCoordination, segmentationChannelCoordination, obsSegmentationsUrls, obsSegmentationsData, pointLayerScopes, pointLayerCoordination, obsPointsUrls, obsPointsData, pointMultiIndicesData);
|
|
194
|
+
const initalViewerState = useNeuroglancerViewerState(theme, showAxisLines, segmentationLayerScopes, segmentationChannelScopesByLayer, segmentationLayerCoordination, segmentationChannelCoordination, obsSegmentationsUrls, obsSegmentationsData, pointLayerScopes, pointLayerCoordination, obsPointsUrls, obsPointsData, pointMultiIndicesData);
|
|
173
195
|
const [latestViewerStateIteration, incrementLatestViewerStateIteration] = useReducer(x => x + 1, 0);
|
|
174
196
|
const latestViewerStateRef = useRef({
|
|
175
197
|
...initalViewerState,
|
|
@@ -206,6 +228,8 @@ export function NeuroglancerSubscriber(props) {
|
|
|
206
228
|
z: spatialRotationZ,
|
|
207
229
|
orbit: spatialRotationOrbit,
|
|
208
230
|
});
|
|
231
|
+
// Track layer loading state for showing loading indicator
|
|
232
|
+
const [isLayersLoaded, setIsLayersLoaded] = useState(false);
|
|
209
233
|
// Track the last coord values we saw, and only mark "vitessce"
|
|
210
234
|
// when *those* actually change. This prevents cell set renders
|
|
211
235
|
// from spoofing the source.
|
|
@@ -324,6 +348,8 @@ export function NeuroglancerSubscriber(props) {
|
|
|
324
348
|
};
|
|
325
349
|
}, []);
|
|
326
350
|
const onSegmentClick = useCallback((value) => {
|
|
351
|
+
// Note: this callback is no longer called by the child component.
|
|
352
|
+
// Reference: https://github.com/vitessce/vitessce/pull/2439
|
|
327
353
|
if (value) {
|
|
328
354
|
const id = String(value);
|
|
329
355
|
const selectedCellIds = [id];
|
|
@@ -341,10 +367,18 @@ export function NeuroglancerSubscriber(props) {
|
|
|
341
367
|
}, [additionalCellSets, cellSetColor, setAdditionalCellSets,
|
|
342
368
|
setCellColorEncoding, setCellSetColor, setCellSetSelection,
|
|
343
369
|
]);
|
|
344
|
-
// Get the ultimate cellColorMapping to pass to NeuroglancerComp as a prop.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
370
|
+
// Get the ultimate cellColorMapping for each layer to pass to NeuroglancerComp as a prop.
|
|
371
|
+
const cellColorMappingByLayer = useMemo(() => {
|
|
372
|
+
const result = {};
|
|
373
|
+
segmentationLayerScopes?.forEach((layerScope) => {
|
|
374
|
+
const channelScope = segmentationChannelScopesByLayer?.[layerScope]?.[0];
|
|
375
|
+
result[layerScope] = {
|
|
376
|
+
colors: segmentationColorMapping?.[layerScope]?.[channelScope] ?? {},
|
|
377
|
+
opacity: segmentationColorMapping?.[layerScope]?.opacity ?? 1.0,
|
|
378
|
+
};
|
|
379
|
+
});
|
|
380
|
+
return result;
|
|
381
|
+
}, [segmentationColorMapping, segmentationLayerScopes, segmentationChannelScopesByLayer]);
|
|
348
382
|
// TODO: try to simplify using useMemoCustomComparison?
|
|
349
383
|
// This would allow us to refactor a lot of the checking-for-changes logic into a comparison function,
|
|
350
384
|
// simplify some of the manual bookkeeping like with prevCoordsRef and lastInteractionSource,
|
|
@@ -355,9 +389,6 @@ export function NeuroglancerSubscriber(props) {
|
|
|
355
389
|
if (current.layers.length <= 0) {
|
|
356
390
|
return current;
|
|
357
391
|
}
|
|
358
|
-
const nextSegments = Object.keys(cellColorMapping);
|
|
359
|
-
const prevLayer = current?.layers?.[0] || {};
|
|
360
|
-
const prevSegments = prevLayer.segments || [];
|
|
361
392
|
const { projectionScale, projectionOrientation, position } = current;
|
|
362
393
|
// Did Vitessce coords change vs the *previous* render?
|
|
363
394
|
const rotChangedNow = !nearEq(spatialRotationX, prevCoordsRef.current.rx, ROTATION_EPS)
|
|
@@ -462,18 +493,23 @@ export function NeuroglancerSubscriber(props) {
|
|
|
462
493
|
nextOrientation = lastNgPushOrientationRef.current ?? projectionOrientation;
|
|
463
494
|
lastInteractionSource.current = null;
|
|
464
495
|
}
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
496
|
+
const updatedLayers = current?.layers?.map((layer, idx) => {
|
|
497
|
+
const layerScope = segmentationLayerScopes?.[idx];
|
|
498
|
+
const layerColorMapping = cellColorMappingByLayer?.[layerScope]?.colors ?? {};
|
|
499
|
+
const layerSegments = Object.keys(layerColorMapping);
|
|
500
|
+
return {
|
|
501
|
+
...layer,
|
|
502
|
+
segments: layerSegments,
|
|
503
|
+
segmentColors: layerColorMapping,
|
|
504
|
+
objectAlpha: cellColorMappingByLayer?.[layerScope]?.opacity ?? 1.0,
|
|
505
|
+
};
|
|
506
|
+
}) ?? [];
|
|
470
507
|
const updated = {
|
|
471
508
|
...current,
|
|
472
509
|
projectionScale: nextProjectionScale,
|
|
473
510
|
projectionOrientation: nextOrientation,
|
|
474
511
|
position: nextPosition,
|
|
475
|
-
layers:
|
|
476
|
-
|| [])] : current?.layers,
|
|
512
|
+
layers: updatedLayers,
|
|
477
513
|
};
|
|
478
514
|
latestViewerStateRef.current = updated;
|
|
479
515
|
prevCoordsRef.current = {
|
|
@@ -486,17 +522,24 @@ export function NeuroglancerSubscriber(props) {
|
|
|
486
522
|
ty: spatialTargetY,
|
|
487
523
|
};
|
|
488
524
|
return updated;
|
|
489
|
-
}, [
|
|
525
|
+
}, [cellColorMappingByLayer, spatialZoom, spatialRotationX, spatialRotationY,
|
|
490
526
|
spatialRotationZ, spatialTargetX, spatialTargetY, initalViewerState,
|
|
491
527
|
latestViewerStateIteration]);
|
|
492
528
|
const onSegmentHighlight = useCallback((obsId) => {
|
|
493
529
|
setCellHighlight(String(obsId));
|
|
494
530
|
}, [setCellHighlight]);
|
|
531
|
+
const handleLayerLoadingChange = useCallback((isLoaded) => {
|
|
532
|
+
setIsLayersLoaded(isLoaded);
|
|
533
|
+
}, []);
|
|
495
534
|
// TODO: if all cells are deselected, a black view is shown, rather we want to show empty NG view?
|
|
496
535
|
// if (!cellColorMapping || Object.keys(cellColorMapping).length === 0) {
|
|
497
536
|
// return;
|
|
498
537
|
// }
|
|
499
538
|
const hasLayers = derivedViewerState?.layers?.length > 0;
|
|
500
539
|
// console.log(derivedViewerState);
|
|
501
|
-
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, children: _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,
|
|
540
|
+
return (_jsx(TitleInfo, { title: title, info: subtitle, helpText: helpText, isSpatial: true, theme: theme, closeButtonVisible: closeButtonVisible, downloadButtonVisible: downloadButtonVisible, removeGridComponent: removeGridComponent, isReady: isReady && isLayersLoaded, 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,
|
|
541
|
+
// Segmentations
|
|
542
|
+
segmentationLayerScopes: segmentationLayerScopes, segmentationLayerCoordination: segmentationLayerCoordination, segmentationChannelScopesByLayer: segmentationChannelScopesByLayer, segmentationChannelCoordination: segmentationChannelCoordination,
|
|
543
|
+
// Points
|
|
544
|
+
pointLayerScopes: pointLayerScopes, pointLayerCoordination: pointLayerCoordination, pointMultiIndicesData: pointMultiIndicesData }) }), _jsx(NeuroglancerComp, { classes: classes, onSegmentClick: onSegmentClick, onSelectHoveredCoords: onSegmentHighlight, viewerState: derivedViewerState, cellColorMapping: cellColorMappingByLayer, setViewerState: handleStateUpdate, onLayerLoadingChange: handleLayerLoadingChange })] })) : null }));
|
|
502
545
|
}
|
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
* @property {() => void} onSelectionDetailsStateChanged
|
|
35
35
|
* A function of the form `() => {}` to respond to selection changes in the viewer.
|
|
36
36
|
* @property {() => void} onViewerStateChanged
|
|
37
|
+
* @property {(isLoaded: boolean) => void} onLayerLoadingChange
|
|
38
|
+
* A function of the form `(isLoaded) => {}`, called when layer loading state changes.
|
|
39
|
+
* The `isLoaded` argument will be `true` when all segmentation layers have finished loading
|
|
40
|
+
* their data sources, or `false` when layers are still loading.
|
|
37
41
|
*
|
|
38
42
|
* @property {Array<Object>} callbacks
|
|
39
43
|
* // ngServer: string,
|
|
@@ -64,6 +68,7 @@ export default class Neuroglancer {
|
|
|
64
68
|
onVisibleChanged: null;
|
|
65
69
|
onSelectionDetailsStateChanged: null;
|
|
66
70
|
onViewerStateChanged: null;
|
|
71
|
+
onLayerLoadingChange: null;
|
|
67
72
|
key: null;
|
|
68
73
|
callbacks: never[];
|
|
69
74
|
ngServer: string;
|
|
@@ -77,7 +82,7 @@ export default class Neuroglancer {
|
|
|
77
82
|
disposers: any[];
|
|
78
83
|
prevColorOverrides: Set<any>;
|
|
79
84
|
overrideColorsById: any;
|
|
80
|
-
|
|
85
|
+
allKnownIdsByLayer: {};
|
|
81
86
|
minimalPoseSnapshot: () => {
|
|
82
87
|
position: any[];
|
|
83
88
|
projectionScale: any;
|
|
@@ -86,7 +91,7 @@ export default class Neuroglancer {
|
|
|
86
91
|
scheduleEmit: () => () => void;
|
|
87
92
|
withoutEmitting: (fn: any) => void;
|
|
88
93
|
didLayersChange: (prevVS: any, nextVS: any) => boolean;
|
|
89
|
-
applyColorsAndVisibility: (
|
|
94
|
+
applyColorsAndVisibility: (cellColorMappingByLayer: any) => void;
|
|
90
95
|
componentDidMount(): void;
|
|
91
96
|
componentDidUpdate(prevProps: any, prevState: any): void;
|
|
92
97
|
componentWillUnmount(): void;
|
|
@@ -140,6 +145,12 @@ export type NgProps = {
|
|
|
140
145
|
*/
|
|
141
146
|
onSelectionDetailsStateChanged: () => void;
|
|
142
147
|
onViewerStateChanged: () => void;
|
|
148
|
+
/**
|
|
149
|
+
* A function of the form `(isLoaded) => {}`, called when layer loading state changes.
|
|
150
|
+
* The `isLoaded` argument will be `true` when all segmentation layers have finished loading
|
|
151
|
+
* their data sources, or `false` when layers are still loading.
|
|
152
|
+
*/
|
|
153
|
+
onLayerLoadingChange: (isLoaded: boolean) => void;
|
|
143
154
|
/**
|
|
144
155
|
* // ngServer: string,
|
|
145
156
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactNeuroglancer.d.ts","sourceRoot":"","sources":["../src/ReactNeuroglancer.js"],"names":[],"mappings":"AAiCA
|
|
1
|
+
{"version":3,"file":"ReactNeuroglancer.d.ts","sourceRoot":"","sources":["../src/ReactNeuroglancer.js"],"names":[],"mappings":"AAiCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;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;;;;;;;;;;;;;MAaE;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,0BAiJC;IAED,yDAwJC;IAED,6BAUC;IAcD,mCAQC;IAED,0DAyCE;IAEF,yCAOE;IAEF,0BAsCE;IA5BI,mCAAyB;IA+B/B,sCAWE;IAEF,qCAUE;IAEF,sBAOC;CACF;;qBAr7Ba,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;;;;;;0BACV,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI;;;;eAK3B,KAAK,CAAC,MAAM,CAAC"}
|
|
@@ -63,6 +63,10 @@ let viewerNoKey;
|
|
|
63
63
|
* @property {() => void} onSelectionDetailsStateChanged
|
|
64
64
|
* A function of the form `() => {}` to respond to selection changes in the viewer.
|
|
65
65
|
* @property {() => void} onViewerStateChanged
|
|
66
|
+
* @property {(isLoaded: boolean) => void} onLayerLoadingChange
|
|
67
|
+
* A function of the form `(isLoaded) => {}`, called when layer loading state changes.
|
|
68
|
+
* The `isLoaded` argument will be `true` when all segmentation layers have finished loading
|
|
69
|
+
* their data sources, or `false` when layers are still loading.
|
|
66
70
|
*
|
|
67
71
|
* @property {Array<Object>} callbacks
|
|
68
72
|
* // ngServer: string,
|
|
@@ -358,6 +362,7 @@ export default class Neuroglancer extends React.Component {
|
|
|
358
362
|
onVisibleChanged: null,
|
|
359
363
|
onSelectionDetailsStateChanged: null,
|
|
360
364
|
onViewerStateChanged: null,
|
|
365
|
+
onLayerLoadingChange: null,
|
|
361
366
|
key: null,
|
|
362
367
|
callbacks: [],
|
|
363
368
|
ngServer: 'https://neuroglancer-demo.appspot.com/',
|
|
@@ -373,7 +378,7 @@ export default class Neuroglancer extends React.Component {
|
|
|
373
378
|
this.disposers = [];
|
|
374
379
|
this.prevColorOverrides = new Set();
|
|
375
380
|
this.overrideColorsById = Object.create(null);
|
|
376
|
-
this.
|
|
381
|
+
this.allKnownIdsByLayer = {};
|
|
377
382
|
}
|
|
378
383
|
minimalPoseSnapshot = () => {
|
|
379
384
|
const v = this.viewer;
|
|
@@ -423,31 +428,33 @@ export default class Neuroglancer extends React.Component {
|
|
|
423
428
|
return JSON.stringify(prevLayers) !== JSON.stringify(nextLayers);
|
|
424
429
|
};
|
|
425
430
|
/* To add colors to the segments, turning unselected to grey */
|
|
426
|
-
applyColorsAndVisibility = (
|
|
431
|
+
applyColorsAndVisibility = (cellColorMappingByLayer) => {
|
|
427
432
|
if (!this.viewer)
|
|
428
433
|
return;
|
|
429
|
-
//
|
|
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)
|
|
434
|
+
// Build full color table per layer
|
|
446
435
|
const baseLayers = (this.props.viewerState?.layers)
|
|
447
436
|
?? (this.viewer.state.toJSON().layers || []);
|
|
448
|
-
const newLayers = baseLayers.map((layer
|
|
449
|
-
// if
|
|
450
|
-
|
|
437
|
+
const newLayers = baseLayers.map((layer) => {
|
|
438
|
+
// Match layerScope by checking if the NG layer name contains the scope key.
|
|
439
|
+
// NG layer names are of the form:
|
|
440
|
+
// "obsSegmentations-init_A_obsSegmentations_0-init_A_obsSegmentations_0"
|
|
441
|
+
const layerScope = Object.keys(cellColorMappingByLayer).find(scope => layer.name?.includes(scope));
|
|
442
|
+
const selected = { ...(cellColorMappingByLayer[layerScope]?.colors || {}) };
|
|
443
|
+
// Track all known IDs for this layer scope
|
|
444
|
+
if (!this.allKnownIdsByLayer)
|
|
445
|
+
this.allKnownIdsByLayer = {};
|
|
446
|
+
if (!this.allKnownIdsByLayer[layerScope]) {
|
|
447
|
+
this.allKnownIdsByLayer[layerScope] = new Set();
|
|
448
|
+
}
|
|
449
|
+
for (const id of Object.keys(selected)) {
|
|
450
|
+
this.allKnownIdsByLayer[layerScope].add(id);
|
|
451
|
+
}
|
|
452
|
+
// Build a full color table: selected keep their hex, others grey
|
|
453
|
+
const fullSegmentColors = {};
|
|
454
|
+
for (const id of this.allKnownIdsByLayer[layerScope] || []) {
|
|
455
|
+
fullSegmentColors[id] = selected[id] || GREY_HEX;
|
|
456
|
+
}
|
|
457
|
+
if (layer.type === 'segmentation') {
|
|
451
458
|
return { ...layer, segmentColors: fullSegmentColors };
|
|
452
459
|
}
|
|
453
460
|
return layer;
|
|
@@ -552,11 +559,42 @@ export default class Neuroglancer extends React.Component {
|
|
|
552
559
|
else {
|
|
553
560
|
viewerNoKey = this.viewer;
|
|
554
561
|
}
|
|
555
|
-
|
|
562
|
+
const { visibleChunksChanged } = this.viewer.chunkQueueManager;
|
|
563
|
+
let firstChunkLoaded = false;
|
|
564
|
+
this.disposers.push(visibleChunksChanged.add(() => {
|
|
565
|
+
if (!firstChunkLoaded) {
|
|
566
|
+
for (const layer of this.viewer.layerManager.managedLayers) {
|
|
567
|
+
if (layer.layer instanceof SegmentationUserLayer) {
|
|
568
|
+
const hasVisibleChunk = layer.layer.renderLayers?.some((rl) => {
|
|
569
|
+
const { numVisibleChunksAvailable, numVisibleChunksNeeded, } = rl.layerChunkProgressInfo || {};
|
|
570
|
+
if (!numVisibleChunksNeeded || !numVisibleChunksAvailable)
|
|
571
|
+
return false;
|
|
572
|
+
// Neuroglancer only shows chunks when a certain % is loaded.
|
|
573
|
+
// The 0.25 is from testing different values, can be reduced to 0.2 to shorten loader time
|
|
574
|
+
return (numVisibleChunksAvailable / numVisibleChunksNeeded) > 0.25;
|
|
575
|
+
});
|
|
576
|
+
if (hasVisibleChunk) {
|
|
577
|
+
firstChunkLoaded = true;
|
|
578
|
+
// Two frames to avoid flash while the following two happens
|
|
579
|
+
// Neuroglancer issues WebGL draw calls
|
|
580
|
+
requestAnimationFrame(() => {
|
|
581
|
+
// GPU has painted, pixels visible on screen
|
|
582
|
+
requestAnimationFrame(() => {
|
|
583
|
+
this.props.onLayerLoadingChange?.(true);
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}));
|
|
592
|
+
this.disposers.push(() => { firstChunkLoaded = false; });
|
|
593
|
+
// TODO: This is purely for debugging - exposes the NG viewer to be tested via console
|
|
556
594
|
// window.viewer = this.viewer;
|
|
557
595
|
}
|
|
558
596
|
componentDidUpdate(prevProps, prevState) {
|
|
559
|
-
const { viewerState, cellColorMapping } = this.props;
|
|
597
|
+
const { viewerState, cellColorMapping: cellColorMappingByLayer } = this.props;
|
|
560
598
|
// The restoreState() call clears the 'selected' (hovered on) segment, which is needed
|
|
561
599
|
// by Neuroglancer's code to toggle segment visibilty on a mouse click. To free the user
|
|
562
600
|
// from having to move the mouse before clicking, save the selected segment and restore
|
|
@@ -592,6 +630,23 @@ export default class Neuroglancer extends React.Component {
|
|
|
592
630
|
if (layer.layer instanceof SegmentationUserLayer) {
|
|
593
631
|
const { segmentSelectionState } = layer.layer.displayState;
|
|
594
632
|
segmentSelectionState.set(selectedSegments[layer.name]);
|
|
633
|
+
const layerScope = Object.keys(cellColorMappingByLayer).find(scope => layer.name?.includes(scope));
|
|
634
|
+
if (layerScope) {
|
|
635
|
+
const opacity = cellColorMappingByLayer[layerScope]?.opacity ?? 1.0;
|
|
636
|
+
layer.layer.displayState.objectAlpha.value = opacity;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// Update annotation layer shaders from viewerState config,
|
|
640
|
+
// skipping update if shader is unchanged to avoid costly re-renders
|
|
641
|
+
if (layer.layer instanceof AnnotationUserLayer) {
|
|
642
|
+
const matchingLayer = (viewerState?.layers || []).find(l => l.name === layer.name);
|
|
643
|
+
if (matchingLayer?.shader) {
|
|
644
|
+
/* eslint-disable-next-line no-underscore-dangle */
|
|
645
|
+
const currentShader = layer.layer.annotationDisplayState.shader.value_;
|
|
646
|
+
if (currentShader !== matchingLayer.shader) {
|
|
647
|
+
layer.layer.annotationDisplayState.shader.value = matchingLayer.shader;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
595
650
|
}
|
|
596
651
|
}
|
|
597
652
|
// For some reason setting position to an empty array doesn't reset
|
|
@@ -633,21 +688,24 @@ export default class Neuroglancer extends React.Component {
|
|
|
633
688
|
this.withoutEmitting(() => {
|
|
634
689
|
const layers = Array.isArray(viewerState.layers) ? viewerState.layers : [];
|
|
635
690
|
this.viewer.state.restoreState({ layers });
|
|
636
|
-
if (
|
|
637
|
-
this.applyColorsAndVisibility(
|
|
691
|
+
if (cellColorMappingByLayer && Object.keys(cellColorMappingByLayer).length) {
|
|
692
|
+
this.applyColorsAndVisibility(cellColorMappingByLayer);
|
|
638
693
|
}
|
|
639
694
|
});
|
|
640
695
|
}
|
|
641
696
|
// If colors changed (but layers didn’t): re-apply colors
|
|
642
697
|
// this was to avid NG randomly assigning colors to the segments by resetting them
|
|
643
698
|
const prevSize = prevProps.cellColorMapping
|
|
644
|
-
? Object.
|
|
645
|
-
|
|
646
|
-
const
|
|
699
|
+
? Object.values(prevProps.cellColorMapping)
|
|
700
|
+
.reduce((acc, v) => acc + Object.keys(v?.colors || {}).length, 0) : 0;
|
|
701
|
+
const currSize = cellColorMappingByLayer
|
|
702
|
+
? Object.values(cellColorMappingByLayer)
|
|
703
|
+
.reduce((acc, v) => acc + Object.keys(v?.colors || {}).length, 0) : 0;
|
|
704
|
+
const mappingRefChanged = prevProps.cellColorMapping !== this.props.cellColorMapping;
|
|
647
705
|
if (!this.didLayersChange(prevVS, viewerState)
|
|
648
706
|
&& (mappingRefChanged || prevSize !== currSize)) {
|
|
649
707
|
this.withoutEmitting(() => {
|
|
650
|
-
this.applyColorsAndVisibility(
|
|
708
|
+
this.applyColorsAndVisibility(cellColorMappingByLayer);
|
|
651
709
|
});
|
|
652
710
|
}
|
|
653
711
|
// Treat "real" layer source/type changes differently from segment list changes.
|
|
@@ -655,7 +713,7 @@ export default class Neuroglancer extends React.Component {
|
|
|
655
713
|
const stripSegFields = layers => (layers || []).map((l) => {
|
|
656
714
|
if (!l)
|
|
657
715
|
return l;
|
|
658
|
-
const { segments, segmentColors, ...rest } = l;
|
|
716
|
+
const { segments, segmentColors, objectAlpha, ...rest } = l;
|
|
659
717
|
return rest; // ignore segments + segmentColors for comparison
|
|
660
718
|
});
|
|
661
719
|
const prevLayers = prevProps.viewerState?.layers;
|
|
@@ -21,7 +21,7 @@ export function toNgLayerName(dataType: any, layerScope: any, channelScope?: nul
|
|
|
21
21
|
/**
|
|
22
22
|
* @returns [viewerState]
|
|
23
23
|
*/
|
|
24
|
-
export function useNeuroglancerViewerState(theme: any, segmentationLayerScopes: any, segmentationChannelScopesByLayer: any, segmentationLayerCoordination: any, segmentationChannelCoordination: any, obsSegmentationsUrls: any, obsSegmentationsData: any, pointLayerScopes: any, pointLayerCoordination: any, obsPointsUrls: any, obsPointsData: any, pointMultiIndicesData: any): any;
|
|
24
|
+
export function useNeuroglancerViewerState(theme: any, showAxisLines: any, segmentationLayerScopes: any, segmentationChannelScopesByLayer: any, segmentationLayerCoordination: any, segmentationChannelCoordination: any, obsSegmentationsUrls: any, obsSegmentationsData: any, pointLayerScopes: any, pointLayerCoordination: any, obsPointsUrls: any, obsPointsData: any, pointMultiIndicesData: any): any;
|
|
25
25
|
export namespace DEFAULT_NG_PROPS {
|
|
26
26
|
let layout: string;
|
|
27
27
|
let position: number[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"data-hook-ng-utils.d.ts","sourceRoot":"","sources":["../src/data-hook-ng-utils.js"],"names":[],"mappings":"AAgDA;;;;KAIK;AACL,sDAHa,MAAM,GACJ;IAAE,CAAC,EAAC,CAAC,MAAM,EAAC,IAAI,CAAC,CAAC;IAAC,CAAC,EAAC,CAAC,MAAM,EAAC,IAAI,CAAC,CAAC;IAAC,CAAC,EAAC,CAAC,MAAM,EAAC,IAAI,CAAC,CAAA;CAAE,CA+BnE;AAED,2FAQC;AAED;;;;;;;;GAQG;AACH;;GAEG;AACH,
|
|
1
|
+
{"version":3,"file":"data-hook-ng-utils.d.ts","sourceRoot":"","sources":["../src/data-hook-ng-utils.js"],"names":[],"mappings":"AAgDA;;;;KAIK;AACL,sDAHa,MAAM,GACJ;IAAE,CAAC,EAAC,CAAC,MAAM,EAAC,IAAI,CAAC,CAAC;IAAC,CAAC,EAAC,CAAC,MAAM,EAAC,IAAI,CAAC,CAAC;IAAC,CAAC,EAAC,CAAC,MAAM,EAAC,IAAI,CAAC,CAAA;CAAE,CA+BnE;AAED,2FAQC;AAED;;;;;;;;GAQG;AACH;;GAEG;AACH,6YAoKC"}
|