@vitessce/neuroglancer 3.9.4 → 3.9.6
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-BCg93QGV.js → ReactNeuroglancer-BSLfuCt9.js} +1 -1
- package/dist/{index-Wdrc02VW.js → index-DvhFVdN_.js} +15782 -10346
- package/dist/index.js +1 -1
- package/dist-tsc/NeuroglancerSubscriber.d.ts.map +1 -1
- package/dist-tsc/NeuroglancerSubscriber.js +182 -43
- package/dist-tsc/data-hook-ng-utils.d.ts +18 -20
- package/dist-tsc/data-hook-ng-utils.d.ts.map +1 -1
- package/dist-tsc/data-hook-ng-utils.js +136 -68
- package/dist-tsc/shader-utils.d.ts +126 -0
- package/dist-tsc/shader-utils.d.ts.map +1 -0
- package/dist-tsc/shader-utils.js +547 -0
- package/dist-tsc/shader-utils.test.d.ts +2 -0
- package/dist-tsc/shader-utils.test.d.ts.map +1 -0
- package/dist-tsc/shader-utils.test.js +364 -0
- package/dist-tsc/use-memo-custom-comparison.d.ts +14 -0
- package/dist-tsc/use-memo-custom-comparison.d.ts.map +1 -0
- package/dist-tsc/use-memo-custom-comparison.js +149 -0
- package/package.json +9 -8
- package/src/NeuroglancerSubscriber.js +320 -69
- package/src/README.md +28 -0
- package/src/data-hook-ng-utils.js +178 -78
- package/src/shader-utils.js +653 -0
- package/src/shader-utils.test.js +432 -0
- package/src/use-memo-custom-comparison.js +189 -0
- package/dist-tsc/data-hook-ng-utils.test.d.ts +0 -2
- package/dist-tsc/data-hook-ng-utils.test.d.ts.map +0 -1
- package/dist-tsc/data-hook-ng-utils.test.js +0 -35
- package/src/data-hook-ng-utils.test.js +0 -52
package/dist/index.js
CHANGED
|
@@ -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":"AAsEA,gEAusBC"}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/* eslint-disable max-len */
|
|
2
3
|
/* eslint-disable no-unused-vars */
|
|
3
|
-
import React, { useCallback, useMemo, useRef, useEffect, useState } from 'react';
|
|
4
|
-
import { TitleInfo, useCoordination,
|
|
5
|
-
import { ViewHelpMapping, ViewType, COMPONENT_COORDINATION_TYPES, } from '@vitessce/constants-internal';
|
|
4
|
+
import React, { useCallback, useMemo, useRef, useEffect, useState, useReducer } from 'react';
|
|
5
|
+
import { TitleInfo, useReady, useInitialCoordination, useCoordination, useCoordinationScopes, useCoordinationScopesBy, useComplexCoordination, useMultiCoordinationScopesNonNull, useMultiCoordinationScopesSecondaryNonNull, useComplexCoordinationSecondary, useLoaders, useMergeCoordination, useMultiObsPoints, usePointMultiObsFeatureMatrixIndices, useMultiObsSegmentations, useSegmentationMultiFeatureSelection, useSegmentationMultiObsFeatureMatrixIndices, useSegmentationMultiObsSets, useGridItemSize, } from '@vitessce/vit-s';
|
|
6
|
+
import { ViewHelpMapping, ViewType, CoordinationType, COMPONENT_COORDINATION_TYPES, } from '@vitessce/constants-internal';
|
|
6
7
|
import { mergeObsSets, getCellColors, setObsSelection } from '@vitessce/sets-utils';
|
|
8
|
+
import { MultiLegend } from '@vitessce/legend';
|
|
7
9
|
import { NeuroglancerComp } from './Neuroglancer.js';
|
|
8
10
|
import { useNeuroglancerViewerState } from './data-hook-ng-utils.js';
|
|
11
|
+
import { useMemoCustomComparison, customIsEqualForCellColors, } from './use-memo-custom-comparison.js';
|
|
9
12
|
import { useStyles } from './styles.js';
|
|
10
13
|
import { quaternionToEuler, eulerToQuaternion, valueGreaterThanEpsilon, nearEq, makeVitNgZoomCalibrator, conjQuat, multiplyQuat, rad2deg, deg2rad, Q_Y_UP, } from './utils.js';
|
|
11
14
|
const VITESSCE_INTERACTION_DELAY = 50;
|
|
@@ -24,21 +27,169 @@ function rgbToHex(rgb) {
|
|
|
24
27
|
: `#${rgb.map(c => c.toString(16).padStart(2, '0')).join('')}`);
|
|
25
28
|
}
|
|
26
29
|
export function NeuroglancerSubscriber(props) {
|
|
27
|
-
const { coordinationScopes: coordinationScopesRaw, closeButtonVisible, downloadButtonVisible, removeGridComponent, theme, title = 'Neuroglancer', helpText = ViewHelpMapping.NEUROGLANCER,
|
|
30
|
+
const { uuid, coordinationScopes: coordinationScopesRaw, coordinationScopesBy: coordinationScopesByRaw, closeButtonVisible, downloadButtonVisible, removeGridComponent, theme, title = 'Spatial', subtitle = 'Powered by Neuroglancer', helpText = ViewHelpMapping.NEUROGLANCER,
|
|
31
|
+
// Note: this is a temporary mechanism
|
|
32
|
+
// to pass an initial NG camera state.
|
|
33
|
+
// Ideally, all camera state should be passed via
|
|
34
|
+
// the existing spatialZoom, spatialTargetX, spatialRotationOrbit, etc,
|
|
35
|
+
// and then NeuroglancerSubscriber should internally convert
|
|
36
|
+
// to NG-compatible values, which would eliminate the need for this.
|
|
37
|
+
initialNgCameraState, } = props;
|
|
28
38
|
const loaders = useLoaders();
|
|
39
|
+
const mergeCoordination = useMergeCoordination();
|
|
40
|
+
// Acccount for possible meta-coordination.
|
|
29
41
|
const coordinationScopes = useCoordinationScopes(coordinationScopesRaw);
|
|
42
|
+
const coordinationScopesBy = useCoordinationScopesBy(coordinationScopes, coordinationScopesByRaw);
|
|
30
43
|
const [{ dataset, obsType, spatialZoom, spatialTargetX, spatialTargetY, spatialRotationX, spatialRotationY, spatialRotationZ, spatialRotationOrbit,
|
|
31
44
|
// spatialOrbitAxis, // always along Y-axis - not used in conversion
|
|
32
45
|
embeddingType: mapping, obsSetColor: cellSetColor, obsSetSelection: cellSetSelection, additionalObsSets: additionalCellSets, }, { setAdditionalObsSets: setAdditionalCellSets, setObsSetColor: setCellSetColor, setObsColorEncoding: setCellColorEncoding, setObsSetSelection: setCellSetSelection, setObsHighlight: setCellHighlight, setSpatialTargetX: setTargetX, setSpatialTargetY: setTargetY, setSpatialRotationX: setRotationX,
|
|
33
46
|
// setSpatialRotationY: setRotationY,
|
|
34
47
|
// setSpatialRotationZ: setRotationZ,
|
|
35
48
|
setSpatialRotationOrbit: setRotationOrbit, setSpatialZoom: setZoom, }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.NEUROGLANCER], coordinationScopes);
|
|
49
|
+
const [ngWidth, ngHeight, containerRef] = useGridItemSize();
|
|
50
|
+
const [segmentationLayerScopes, segmentationChannelScopesByLayer,] = useMultiCoordinationScopesSecondaryNonNull(CoordinationType.SEGMENTATION_CHANNEL, CoordinationType.SEGMENTATION_LAYER, coordinationScopes, coordinationScopesBy);
|
|
51
|
+
const pointLayerScopes = useMultiCoordinationScopesNonNull(CoordinationType.POINT_LAYER, coordinationScopes);
|
|
52
|
+
// Object keys are coordination scope names for spatialSegmentationLayer.
|
|
53
|
+
const segmentationLayerCoordination = useComplexCoordination([
|
|
54
|
+
CoordinationType.FILE_UID,
|
|
55
|
+
CoordinationType.SEGMENTATION_CHANNEL,
|
|
56
|
+
CoordinationType.SPATIAL_LAYER_VISIBLE,
|
|
57
|
+
CoordinationType.SPATIAL_LAYER_OPACITY,
|
|
58
|
+
], coordinationScopes, coordinationScopesBy, CoordinationType.SEGMENTATION_LAYER);
|
|
59
|
+
// Object keys are coordination scope names for spatialSegmentationChannel.
|
|
60
|
+
const segmentationChannelCoordination = useComplexCoordinationSecondary([
|
|
61
|
+
CoordinationType.OBS_TYPE,
|
|
62
|
+
CoordinationType.SPATIAL_TARGET_C,
|
|
63
|
+
CoordinationType.SPATIAL_CHANNEL_VISIBLE,
|
|
64
|
+
CoordinationType.SPATIAL_CHANNEL_OPACITY,
|
|
65
|
+
CoordinationType.SPATIAL_CHANNEL_COLOR,
|
|
66
|
+
CoordinationType.SPATIAL_SEGMENTATION_FILLED,
|
|
67
|
+
CoordinationType.SPATIAL_SEGMENTATION_STROKE_WIDTH,
|
|
68
|
+
CoordinationType.OBS_COLOR_ENCODING,
|
|
69
|
+
CoordinationType.FEATURE_SELECTION,
|
|
70
|
+
CoordinationType.FEATURE_AGGREGATION_STRATEGY,
|
|
71
|
+
CoordinationType.FEATURE_VALUE_COLORMAP,
|
|
72
|
+
CoordinationType.FEATURE_VALUE_COLORMAP_RANGE,
|
|
73
|
+
CoordinationType.OBS_SET_COLOR,
|
|
74
|
+
CoordinationType.OBS_SET_SELECTION,
|
|
75
|
+
CoordinationType.ADDITIONAL_OBS_SETS,
|
|
76
|
+
CoordinationType.OBS_HIGHLIGHT,
|
|
77
|
+
CoordinationType.TOOLTIPS_VISIBLE,
|
|
78
|
+
CoordinationType.TOOLTIP_CROSSHAIRS_VISIBLE,
|
|
79
|
+
CoordinationType.LEGEND_VISIBLE,
|
|
80
|
+
], coordinationScopes, coordinationScopesBy, CoordinationType.SEGMENTATION_LAYER, CoordinationType.SEGMENTATION_CHANNEL);
|
|
81
|
+
// Point layer
|
|
82
|
+
const pointLayerCoordination = useComplexCoordination([
|
|
83
|
+
CoordinationType.OBS_TYPE,
|
|
84
|
+
CoordinationType.SPATIAL_LAYER_VISIBLE,
|
|
85
|
+
CoordinationType.SPATIAL_LAYER_OPACITY,
|
|
86
|
+
CoordinationType.OBS_COLOR_ENCODING,
|
|
87
|
+
CoordinationType.FEATURE_COLOR,
|
|
88
|
+
CoordinationType.FEATURE_FILTER_MODE,
|
|
89
|
+
CoordinationType.FEATURE_SELECTION,
|
|
90
|
+
CoordinationType.FEATURE_VALUE_COLORMAP,
|
|
91
|
+
CoordinationType.FEATURE_VALUE_COLORMAP_RANGE,
|
|
92
|
+
CoordinationType.SPATIAL_LAYER_COLOR,
|
|
93
|
+
CoordinationType.OBS_HIGHLIGHT,
|
|
94
|
+
CoordinationType.TOOLTIPS_VISIBLE,
|
|
95
|
+
CoordinationType.TOOLTIP_CROSSHAIRS_VISIBLE,
|
|
96
|
+
CoordinationType.LEGEND_VISIBLE,
|
|
97
|
+
], coordinationScopes, coordinationScopesBy, CoordinationType.POINT_LAYER);
|
|
98
|
+
// Points data
|
|
99
|
+
const [obsPointsData, obsPointsDataStatus, obsPointsUrls, obsPointsErrors] = useMultiObsPoints(coordinationScopes, coordinationScopesBy, loaders, dataset, mergeCoordination, uuid);
|
|
100
|
+
const [pointMultiIndicesData, pointMultiIndicesDataStatus, pointMultiIndicesDataErrors] = usePointMultiObsFeatureMatrixIndices(coordinationScopes, coordinationScopesBy, loaders, dataset);
|
|
101
|
+
// Segmentations data
|
|
102
|
+
const [obsSegmentationsData, obsSegmentationsDataStatus, obsSegmentationsUrls, obsSegmentationsDataErrors] = useMultiObsSegmentations(coordinationScopes, coordinationScopesBy, loaders, dataset, mergeCoordination, uuid);
|
|
103
|
+
const [obsSegmentationsSetsData, obsSegmentationsSetsDataStatus, obsSegmentationsSetsDataErrors] = useSegmentationMultiObsSets(coordinationScopes, coordinationScopesBy, loaders, dataset);
|
|
104
|
+
const [segmentationMultiExpressionData, segmentationMultiLoadedFeatureSelection, segmentationMultiExpressionExtents, segmentationMultiExpressionNormData, segmentationMultiFeatureSelectionStatus, segmentationMultiFeatureSelectionErrors,] = useSegmentationMultiFeatureSelection(coordinationScopes, coordinationScopesBy, loaders, dataset);
|
|
105
|
+
const [segmentationMultiIndicesData, segmentationMultiIndicesDataStatus, segmentationMultiIndicesDataErrors] = useSegmentationMultiObsFeatureMatrixIndices(coordinationScopes, coordinationScopesBy, loaders, dataset);
|
|
106
|
+
const errors = [
|
|
107
|
+
...obsPointsErrors,
|
|
108
|
+
...obsSegmentationsDataErrors,
|
|
109
|
+
...obsSegmentationsSetsDataErrors,
|
|
110
|
+
...pointMultiIndicesDataErrors,
|
|
111
|
+
...segmentationMultiFeatureSelectionErrors,
|
|
112
|
+
...segmentationMultiIndicesDataErrors,
|
|
113
|
+
];
|
|
114
|
+
const isReady = useReady([
|
|
115
|
+
// Points
|
|
116
|
+
obsPointsDataStatus,
|
|
117
|
+
pointMultiIndicesDataStatus,
|
|
118
|
+
// Segmentations
|
|
119
|
+
obsSegmentationsDataStatus,
|
|
120
|
+
obsSegmentationsSetsDataStatus,
|
|
121
|
+
segmentationMultiFeatureSelectionStatus,
|
|
122
|
+
segmentationMultiIndicesDataStatus,
|
|
123
|
+
]);
|
|
36
124
|
// console.log("NG Subs Render orbit", spatialRotationX, spatialRotationY, spatialRotationOrbit);
|
|
37
125
|
const { classes } = useStyles();
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
126
|
+
const segmentationColorMapping = useMemoCustomComparison(() => {
|
|
127
|
+
// TODO: ultimately, segmentationColorMapping becomes cellColorMapping, and makes its way into the viewerState.
|
|
128
|
+
// It may make sense to merge the multiple useMemoCustomComparisons upstream of derivedViewerState into one.
|
|
129
|
+
// This would complicate the comparison function, but the multiple separate useMemos are not really necessary.
|
|
130
|
+
const result = {};
|
|
131
|
+
segmentationLayerScopes?.forEach((layerScope) => {
|
|
132
|
+
result[layerScope] = {};
|
|
133
|
+
segmentationChannelScopesByLayer?.[layerScope]?.forEach((channelScope) => {
|
|
134
|
+
const { obsSets: layerSets, obsIndex: layerIndex } = obsSegmentationsSetsData?.[layerScope]?.[channelScope] || {};
|
|
135
|
+
if (layerSets && layerIndex) {
|
|
136
|
+
const { obsSetColor, obsColorEncoding, obsSetSelection, additionalObsSets, } = segmentationChannelCoordination[0][layerScope][channelScope];
|
|
137
|
+
const mergedCellSets = mergeObsSets(layerSets, additionalObsSets);
|
|
138
|
+
const cellColors = getCellColors({
|
|
139
|
+
cellSets: mergedCellSets,
|
|
140
|
+
cellSetSelection: obsSetSelection,
|
|
141
|
+
cellSetColor: obsSetColor,
|
|
142
|
+
obsIndex: layerIndex,
|
|
143
|
+
theme,
|
|
144
|
+
});
|
|
145
|
+
// Convert the list of colors to an object of hex strings, which NG requires.
|
|
146
|
+
const ngCellColors = {};
|
|
147
|
+
cellColors.forEach((color, i) => {
|
|
148
|
+
ngCellColors[i] = rgbToHex(color);
|
|
149
|
+
});
|
|
150
|
+
/* // TODO: Is this necessary?
|
|
151
|
+
const obsColorIndices = treeToCellSetColorIndicesBySetNames(
|
|
152
|
+
mergedLayerSets,
|
|
153
|
+
obsSetSelection,
|
|
154
|
+
obsSetColor,
|
|
155
|
+
);
|
|
156
|
+
*/
|
|
157
|
+
result[layerScope][channelScope] = ngCellColors;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
return result;
|
|
162
|
+
}, {
|
|
163
|
+
// The dependencies for the comparison,
|
|
164
|
+
// used by the custom equality function.
|
|
165
|
+
segmentationLayerScopes,
|
|
166
|
+
segmentationChannelScopesByLayer,
|
|
167
|
+
obsSegmentationsSetsData,
|
|
168
|
+
segmentationChannelCoordination,
|
|
169
|
+
theme,
|
|
170
|
+
}, customIsEqualForCellColors);
|
|
171
|
+
// Obtain the Neuroglancer viewerState object.
|
|
172
|
+
const initalViewerState = useNeuroglancerViewerState(theme, segmentationLayerScopes, segmentationChannelScopesByLayer, segmentationLayerCoordination, segmentationChannelCoordination, obsSegmentationsUrls, obsSegmentationsData, pointLayerScopes, pointLayerCoordination, obsPointsUrls, obsPointsData, pointMultiIndicesData);
|
|
173
|
+
const [latestViewerStateIteration, incrementLatestViewerStateIteration] = useReducer(x => x + 1, 0);
|
|
174
|
+
const latestViewerStateRef = useRef({
|
|
175
|
+
...initalViewerState,
|
|
176
|
+
...(initialNgCameraState ?? {}),
|
|
177
|
+
});
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
const prevNgCameraState = {
|
|
180
|
+
position: latestViewerStateRef.current.position,
|
|
181
|
+
projectionOrientation: latestViewerStateRef.current.projectionOrientation,
|
|
182
|
+
projectionScale: latestViewerStateRef.current.projectionScale,
|
|
183
|
+
};
|
|
184
|
+
latestViewerStateRef.current = {
|
|
185
|
+
...initalViewerState,
|
|
186
|
+
...prevNgCameraState,
|
|
187
|
+
};
|
|
188
|
+
// Force a re-render by incrementing a piece of state.
|
|
189
|
+
// This works because we have made latestViewerStateIteration
|
|
190
|
+
// a dependency for derivedViewerState, triggering the useMemo downstream.
|
|
191
|
+
incrementLatestViewerStateIteration();
|
|
192
|
+
}, [initalViewerState]);
|
|
42
193
|
const initialRotationPushedRef = useRef(false);
|
|
43
194
|
const ngRotPushAtRef = useRef(0);
|
|
44
195
|
const lastInteractionSource = useRef(null);
|
|
@@ -67,15 +218,6 @@ export function NeuroglancerSubscriber(props) {
|
|
|
67
218
|
tx: spatialTargetX,
|
|
68
219
|
ty: spatialTargetY,
|
|
69
220
|
});
|
|
70
|
-
const mergedCellSets = useMemo(() => mergeObsSets(cellSets, additionalCellSets), [cellSets, additionalCellSets]);
|
|
71
|
-
const cellColors = useMemo(() => getCellColors({
|
|
72
|
-
cellSets: mergedCellSets,
|
|
73
|
-
cellSetSelection,
|
|
74
|
-
cellSetColor,
|
|
75
|
-
obsIndex,
|
|
76
|
-
theme,
|
|
77
|
-
}), [mergedCellSets, theme,
|
|
78
|
-
cellSetColor, cellSetSelection, obsIndex]);
|
|
79
221
|
/*
|
|
80
222
|
* handleStateUpdate - Interactions from NG to Vitessce are pushed here
|
|
81
223
|
*/
|
|
@@ -190,35 +332,29 @@ export function NeuroglancerSubscriber(props) {
|
|
|
190
332
|
if (alreadySelectedId) {
|
|
191
333
|
return;
|
|
192
334
|
}
|
|
335
|
+
// TODO: update this now that we are using layer/channel-based organization of segmentations.
|
|
336
|
+
// There is no more "top-level" obsSets coordination; it is only on a per-layer basis.
|
|
337
|
+
// We should probably just assume the first segmentation layer/channel when updating the logic,
|
|
338
|
+
// since it is not clear how we would determine which layer/channel to update if there are multiple.
|
|
193
339
|
setObsSelection(selectedCellIds, additionalCellSets, cellSetColor, setCellSetSelection, setAdditionalCellSets, setCellSetColor, setCellColorEncoding, 'Selection ', `: based on selected segments ${value}`);
|
|
194
340
|
}
|
|
195
341
|
}, [additionalCellSets, cellSetColor, setAdditionalCellSets,
|
|
196
342
|
setCellColorEncoding, setCellSetColor, setCellSetSelection,
|
|
197
343
|
]);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
// TODO: look into deferredValue from React
|
|
208
|
-
// startTransition(() => {
|
|
209
|
-
// setBatchedCellColors(cellColors);
|
|
210
|
-
// });
|
|
211
|
-
}, [cellColors]);
|
|
212
|
-
// TODO use a ref if slow - see prev commits
|
|
213
|
-
const cellColorMapping = useMemo(() => {
|
|
214
|
-
const colorMapping = {};
|
|
215
|
-
batchedCellColors.forEach((color, cell) => {
|
|
216
|
-
colorMapping[cell] = rgbToHex(color);
|
|
217
|
-
});
|
|
218
|
-
return colorMapping;
|
|
219
|
-
}, [batchedCellColors]);
|
|
344
|
+
// Get the ultimate cellColorMapping to pass to NeuroglancerComp as a prop.
|
|
345
|
+
// For now, we take the first layer and channel for cell colors.
|
|
346
|
+
const cellColorMapping = useMemo(() => (segmentationColorMapping?.[segmentationLayerScopes?.[0]]?.[segmentationChannelScopesByLayer?.[segmentationLayerScopes?.[0]]?.[0]]
|
|
347
|
+
?? {}), [segmentationColorMapping]);
|
|
348
|
+
// TODO: try to simplify using useMemoCustomComparison?
|
|
349
|
+
// This would allow us to refactor a lot of the checking-for-changes logic into a comparison function,
|
|
350
|
+
// simplify some of the manual bookkeeping like with prevCoordsRef and lastInteractionSource,
|
|
351
|
+
// and would allow us to potentially remove usage of some refs (e.g., latestViewerStateRef)
|
|
352
|
+
// by relying on the memoization to prevent unnecessary updates.
|
|
220
353
|
const derivedViewerState = useMemo(() => {
|
|
221
354
|
const { current } = latestViewerStateRef;
|
|
355
|
+
if (current.layers.length <= 0) {
|
|
356
|
+
return current;
|
|
357
|
+
}
|
|
222
358
|
const nextSegments = Object.keys(cellColorMapping);
|
|
223
359
|
const prevLayer = current?.layers?.[0] || {};
|
|
224
360
|
const prevSegments = prevLayer.segments || [];
|
|
@@ -351,13 +487,16 @@ export function NeuroglancerSubscriber(props) {
|
|
|
351
487
|
};
|
|
352
488
|
return updated;
|
|
353
489
|
}, [cellColorMapping, spatialZoom, spatialRotationX, spatialRotationY,
|
|
354
|
-
spatialRotationZ, spatialTargetX, spatialTargetY
|
|
490
|
+
spatialRotationZ, spatialTargetX, spatialTargetY, initalViewerState,
|
|
491
|
+
latestViewerStateIteration]);
|
|
355
492
|
const onSegmentHighlight = useCallback((obsId) => {
|
|
356
493
|
setCellHighlight(String(obsId));
|
|
357
|
-
}, [
|
|
494
|
+
}, [setCellHighlight]);
|
|
358
495
|
// TODO: if all cells are deselected, a black view is shown, rather we want to show empty NG view?
|
|
359
496
|
// if (!cellColorMapping || Object.keys(cellColorMapping).length === 0) {
|
|
360
497
|
// return;
|
|
361
498
|
// }
|
|
362
|
-
|
|
499
|
+
const hasLayers = derivedViewerState?.layers?.length > 0;
|
|
500
|
+
// 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, segmentationLayerScopes: segmentationLayerScopes, segmentationLayerCoordination: segmentationLayerCoordination, segmentationChannelScopesByLayer: segmentationChannelScopesByLayer, segmentationChannelCoordination: segmentationChannelCoordination }) }), hasLayers ? (_jsx(NeuroglancerComp, { classes: classes, onSegmentClick: onSegmentClick, onSelectHoveredCoords: onSegmentHighlight, viewerState: derivedViewerState, cellColorMapping: cellColorMapping, setViewerState: handleStateUpdate })) : null] }) }));
|
|
363
502
|
}
|
|
@@ -1,22 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
z: [number, "nm"];
|
|
13
|
-
};
|
|
14
|
-
position: any[];
|
|
15
|
-
projectionOrientation: any[];
|
|
16
|
-
projectionScale: any;
|
|
17
|
-
crossSectionScale: any;
|
|
18
|
-
}[];
|
|
19
|
-
export function useExtractOptionsForNg(loaders: any, dataset: any, dataType: any): any[];
|
|
1
|
+
/**
|
|
2
|
+
* Normalize dimensionX/Y/Z to nanometers.
|
|
3
|
+
* @param {object} opts
|
|
4
|
+
* @returns {{ x:[number,'nm'], y:[number,'nm'], z:[number,'nm'] }}
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeDimensionsToNanometers(opts: object): {
|
|
7
|
+
x: [number, "nm"];
|
|
8
|
+
y: [number, "nm"];
|
|
9
|
+
z: [number, "nm"];
|
|
10
|
+
};
|
|
11
|
+
export function toNgLayerName(dataType: any, layerScope: any, channelScope?: null): string;
|
|
20
12
|
/**
|
|
21
13
|
* Get the parameters for NG's viewerstate.
|
|
22
14
|
* @param {object} loaders The object mapping
|
|
@@ -29,12 +21,18 @@ export function useExtractOptionsForNg(loaders: any, dataset: any, dataType: any
|
|
|
29
21
|
/**
|
|
30
22
|
* @returns [viewerState]
|
|
31
23
|
*/
|
|
32
|
-
export function useNeuroglancerViewerState(
|
|
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;
|
|
33
25
|
export namespace DEFAULT_NG_PROPS {
|
|
34
26
|
let layout: string;
|
|
35
27
|
let position: number[];
|
|
36
28
|
let projectionOrientation: number[];
|
|
37
29
|
let projectionScale: number;
|
|
38
30
|
let crossSectionScale: number;
|
|
31
|
+
namespace dimensions {
|
|
32
|
+
let x: (string | number)[];
|
|
33
|
+
let y: (string | number)[];
|
|
34
|
+
let z: (string | number)[];
|
|
35
|
+
}
|
|
36
|
+
let layers: never[];
|
|
39
37
|
}
|
|
40
38
|
//# sourceMappingURL=data-hook-ng-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"data-hook-ng-utils.d.ts","sourceRoot":"","sources":["../src/data-hook-ng-utils.js"],"names":[],"mappings":"
|
|
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,yXAiJC"}
|
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
2
1
|
import { DataType } from '@vitessce/constants-internal';
|
|
2
|
+
import { cloneDeep } from 'lodash-es';
|
|
3
|
+
import { useMemoCustomComparison, customIsEqualForInitialViewerState } from './use-memo-custom-comparison.js';
|
|
4
|
+
import { getPointsShader } from './shader-utils.js';
|
|
3
5
|
export const DEFAULT_NG_PROPS = {
|
|
4
6
|
layout: '3d',
|
|
5
7
|
position: [0, 0, 0],
|
|
6
8
|
projectionOrientation: [0, 0, 0, 1],
|
|
7
9
|
projectionScale: 1024,
|
|
8
10
|
crossSectionScale: 1,
|
|
11
|
+
dimensions: {
|
|
12
|
+
x: [1, 'nm'],
|
|
13
|
+
y: [1, 'nm'],
|
|
14
|
+
z: [1, 'nm'],
|
|
15
|
+
},
|
|
16
|
+
layers: [],
|
|
9
17
|
};
|
|
10
18
|
function toPrecomputedSource(url) {
|
|
11
19
|
if (!url) {
|
|
12
|
-
|
|
20
|
+
throw new Error('toPrecomputedSource: URL is required');
|
|
13
21
|
}
|
|
14
22
|
return `precomputed://${url}`;
|
|
15
23
|
}
|
|
@@ -36,8 +44,8 @@ function isInNanometerRange(value, unit, minNm = 1, maxNm = 100) {
|
|
|
36
44
|
* @param {object} opts
|
|
37
45
|
* @returns {{ x:[number,'nm'], y:[number,'nm'], z:[number,'nm'] }}
|
|
38
46
|
*/
|
|
39
|
-
function normalizeDimensionsToNanometers(opts) {
|
|
40
|
-
const { dimensionUnit, dimensionX, dimensionY, dimensionZ } = opts;
|
|
47
|
+
export function normalizeDimensionsToNanometers(opts) {
|
|
48
|
+
const { dimensionUnit, dimensionX, dimensionY, dimensionZ, ...otherOptions } = opts;
|
|
41
49
|
if (!dimensionUnit || !dimensionX || !dimensionY || !dimensionZ) {
|
|
42
50
|
console.warn('Missing dimension info');
|
|
43
51
|
}
|
|
@@ -48,70 +56,25 @@ function normalizeDimensionsToNanometers(opts) {
|
|
|
48
56
|
console.warn('Dimension was converted to nm units');
|
|
49
57
|
}
|
|
50
58
|
return {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
// The dimension-related fields are formatted differently in the fileDef.options
|
|
60
|
+
// vs. what the viewerState expects.
|
|
61
|
+
dimensions: {
|
|
62
|
+
x: xNm ? [dimensionX, dimensionUnit] : [1, 'nm'],
|
|
63
|
+
y: yNm ? [dimensionY, dimensionUnit] : [1, 'nm'],
|
|
64
|
+
z: zNm ? [dimensionZ, dimensionUnit] : [1, 'nm'],
|
|
65
|
+
},
|
|
66
|
+
// The non-dimension-related options can be passed through without modification.
|
|
67
|
+
...otherOptions,
|
|
54
68
|
};
|
|
55
69
|
}
|
|
56
|
-
export function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
?? loader?.coordinationValues?.fileUid
|
|
65
|
-
?? undefined;
|
|
66
|
-
const { position, projectionOrientation, projectionScale, crossSectionScale } = loader?.options ?? {};
|
|
67
|
-
const isPrecomputed = loader?.fileType.includes('precomputed');
|
|
68
|
-
if (!isPrecomputed) {
|
|
69
|
-
console.warn('Filetype needs to be precomputed');
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
key,
|
|
73
|
-
type: 'segmentation',
|
|
74
|
-
fileUid,
|
|
75
|
-
layout: DEFAULT_NG_PROPS.layout,
|
|
76
|
-
url,
|
|
77
|
-
source: toPrecomputedSource(url),
|
|
78
|
-
name: fileUid ?? key?.name ?? 'segmentation',
|
|
79
|
-
// For precomputed: nm is the unit used
|
|
80
|
-
dimensions: normalizeDimensionsToNanometers(loader?.options),
|
|
81
|
-
// If not provided, no error, but difficult to see the data
|
|
82
|
-
position: Array.isArray(position) && position.length === 3
|
|
83
|
-
? position : DEFAULT_NG_PROPS.position,
|
|
84
|
-
// If not provided, will have a default orientation
|
|
85
|
-
projectionOrientation: Array.isArray(projectionOrientation)
|
|
86
|
-
&& projectionOrientation.length === 4
|
|
87
|
-
? projectionOrientation : DEFAULT_NG_PROPS.projectionOrientation,
|
|
88
|
-
projectionScale: Number.isFinite(projectionScale)
|
|
89
|
-
? projectionScale : DEFAULT_NG_PROPS.projectionScale,
|
|
90
|
-
crossSectionScale: Number.isFinite(crossSectionScale)
|
|
91
|
-
? crossSectionScale : DEFAULT_NG_PROPS.crossSectionScale,
|
|
92
|
-
};
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
export function useExtractOptionsForNg(loaders, dataset, dataType) {
|
|
96
|
-
const extractedEntities = useMemo(() => extractDataTypeEntities(loaders, dataset, dataType), [loaders, dataset, dataType]);
|
|
97
|
-
const layers = useMemo(() => extractedEntities
|
|
98
|
-
.filter(t => t.source)
|
|
99
|
-
.map(t => ({
|
|
100
|
-
type: t.type,
|
|
101
|
-
source: t.source,
|
|
102
|
-
segments: [],
|
|
103
|
-
name: t.name || 'segmentation',
|
|
104
|
-
})), [extractedEntities]);
|
|
105
|
-
const viewerState = useMemo(() => ({
|
|
106
|
-
dimensions: extractedEntities[0]?.dimensions,
|
|
107
|
-
position: extractedEntities[0]?.position,
|
|
108
|
-
crossSectionScale: extractedEntities[0]?.crossSectionScale,
|
|
109
|
-
projectionOrientation: extractedEntities[0]?.projectionOrientation,
|
|
110
|
-
projectionScale: extractedEntities[0]?.projectionScale,
|
|
111
|
-
layers,
|
|
112
|
-
layout: extractedEntities[0].layout,
|
|
113
|
-
}));
|
|
114
|
-
return [viewerState];
|
|
70
|
+
export function toNgLayerName(dataType, layerScope, channelScope = null) {
|
|
71
|
+
if (dataType === DataType.OBS_SEGMENTATIONS) {
|
|
72
|
+
return `obsSegmentations-${layerScope}-${channelScope}`;
|
|
73
|
+
}
|
|
74
|
+
if (dataType === DataType.OBS_POINTS) {
|
|
75
|
+
return `obsPoints-${layerScope}`;
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`Unsupported data type: ${dataType}`);
|
|
115
78
|
}
|
|
116
79
|
/**
|
|
117
80
|
* Get the parameters for NG's viewerstate.
|
|
@@ -125,6 +88,111 @@ export function useExtractOptionsForNg(loaders, dataset, dataType) {
|
|
|
125
88
|
/**
|
|
126
89
|
* @returns [viewerState]
|
|
127
90
|
*/
|
|
128
|
-
export function useNeuroglancerViewerState(
|
|
129
|
-
|
|
91
|
+
export function useNeuroglancerViewerState(theme, segmentationLayerScopes, segmentationChannelScopesByLayer, segmentationLayerCoordination, segmentationChannelCoordination, obsSegmentationsUrls, obsSegmentationsData, pointLayerScopes, pointLayerCoordination, obsPointsUrls, obsPointsData, pointMultiIndicesData) {
|
|
92
|
+
const viewerState = useMemoCustomComparison(() => {
|
|
93
|
+
let result = cloneDeep(DEFAULT_NG_PROPS);
|
|
94
|
+
// ======= SEGMENTATIONS =======
|
|
95
|
+
// Iterate over segmentation layers and channels.
|
|
96
|
+
segmentationLayerScopes.forEach((layerScope) => {
|
|
97
|
+
const layerCoordination = segmentationLayerCoordination[0][layerScope];
|
|
98
|
+
const channelScopes = segmentationChannelScopesByLayer[layerScope] || [];
|
|
99
|
+
const layerData = obsSegmentationsData[layerScope];
|
|
100
|
+
const layerUrl = obsSegmentationsUrls[layerScope]?.[0]?.url;
|
|
101
|
+
if (layerUrl && layerData) {
|
|
102
|
+
const { spatialLayerVisible, } = layerCoordination || {};
|
|
103
|
+
channelScopes.forEach((channelScope) => {
|
|
104
|
+
const channelCoordination = segmentationChannelCoordination[0]?.[layerScope]?.[channelScope];
|
|
105
|
+
const { spatialChannelVisible, } = channelCoordination || {};
|
|
106
|
+
result = {
|
|
107
|
+
...result,
|
|
108
|
+
layers: [
|
|
109
|
+
...result.layers,
|
|
110
|
+
{
|
|
111
|
+
type: 'segmentation',
|
|
112
|
+
source: toPrecomputedSource(layerUrl),
|
|
113
|
+
segments: [],
|
|
114
|
+
name: toNgLayerName(DataType.OBS_SEGMENTATIONS, layerScope, channelScope),
|
|
115
|
+
visible: spatialLayerVisible && spatialChannelVisible, // Both layer and channel
|
|
116
|
+
// visibility must be true for the layer to be visible.
|
|
117
|
+
// TODO: update this to extract specific properties from
|
|
118
|
+
// neuroglancerOptions as needed.
|
|
119
|
+
...(layerData.neuroglancerOptions ?? {}),
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
// ======= POINTS =======
|
|
127
|
+
// Iterate over point layers.
|
|
128
|
+
pointLayerScopes.forEach((layerScope) => {
|
|
129
|
+
const layerCoordination = pointLayerCoordination[0][layerScope];
|
|
130
|
+
const layerData = obsPointsData[layerScope];
|
|
131
|
+
const layerUrl = obsPointsUrls[layerScope]?.[0]?.url;
|
|
132
|
+
const featureIndex = pointMultiIndicesData[layerScope]?.featureIndex;
|
|
133
|
+
if (layerUrl && layerData) {
|
|
134
|
+
const { spatialLayerVisible, spatialLayerOpacity, obsColorEncoding, spatialLayerColor, featureSelection, featureFilterMode, featureColor, } = layerCoordination || {};
|
|
135
|
+
// Dynamically construct the shader based on the color encoding
|
|
136
|
+
// and other coordination values.
|
|
137
|
+
const shader = getPointsShader({
|
|
138
|
+
theme,
|
|
139
|
+
featureIndex,
|
|
140
|
+
spatialLayerOpacity,
|
|
141
|
+
obsColorEncoding,
|
|
142
|
+
spatialLayerColor,
|
|
143
|
+
featureSelection,
|
|
144
|
+
featureFilterMode,
|
|
145
|
+
featureColor,
|
|
146
|
+
featureIndexProp: layerData.neuroglancerOptions?.featureIndexProp,
|
|
147
|
+
pointIndexProp: layerData.neuroglancerOptions?.pointIndexProp,
|
|
148
|
+
});
|
|
149
|
+
result = {
|
|
150
|
+
...result,
|
|
151
|
+
layers: [
|
|
152
|
+
...result.layers,
|
|
153
|
+
{
|
|
154
|
+
type: 'annotation',
|
|
155
|
+
source: {
|
|
156
|
+
url: toPrecomputedSource(layerUrl),
|
|
157
|
+
subsources: {
|
|
158
|
+
default: true,
|
|
159
|
+
},
|
|
160
|
+
enableDefaultSubsources: false,
|
|
161
|
+
},
|
|
162
|
+
tab: 'annotations',
|
|
163
|
+
shader,
|
|
164
|
+
name: toNgLayerName(DataType.OBS_POINTS, layerScope),
|
|
165
|
+
visible: spatialLayerVisible,
|
|
166
|
+
// Options from layerData.neuroglancerOptions
|
|
167
|
+
// like projectionAnnotationSpacing:
|
|
168
|
+
projectionAnnotationSpacing: layerData.neuroglancerOptions
|
|
169
|
+
?.projectionAnnotationSpacing ?? 1.0,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
// TODO: is this needed?
|
|
173
|
+
// The selected layer here will overwrite anything
|
|
174
|
+
// that was previously specified.
|
|
175
|
+
selectedLayer: {
|
|
176
|
+
// size: ? // TODO: is this needed?
|
|
177
|
+
layer: toNgLayerName(DataType.OBS_POINTS, layerScope),
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
return result;
|
|
183
|
+
}, {
|
|
184
|
+
theme,
|
|
185
|
+
segmentationLayerScopes,
|
|
186
|
+
segmentationChannelScopesByLayer,
|
|
187
|
+
segmentationLayerCoordination,
|
|
188
|
+
segmentationChannelCoordination,
|
|
189
|
+
obsSegmentationsUrls,
|
|
190
|
+
obsSegmentationsData,
|
|
191
|
+
pointLayerScopes,
|
|
192
|
+
pointLayerCoordination,
|
|
193
|
+
obsPointsUrls,
|
|
194
|
+
obsPointsData,
|
|
195
|
+
pointMultiIndicesData,
|
|
196
|
+
}, customIsEqualForInitialViewerState);
|
|
197
|
+
return viewerState;
|
|
130
198
|
}
|