@vitessce/neuroglancer 3.9.5 → 3.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/{ReactNeuroglancer-BCg93QGV.js → ReactNeuroglancer-pv4bM8Yp.js} +43 -26
  2. package/dist/index-BEPd2Tds.js +37856 -0
  3. package/dist/index.js +1 -1
  4. package/dist-tsc/Neuroglancer.d.ts +0 -2
  5. package/dist-tsc/Neuroglancer.d.ts.map +1 -1
  6. package/dist-tsc/Neuroglancer.js +26 -26
  7. package/dist-tsc/NeuroglancerSubscriber.d.ts.map +1 -1
  8. package/dist-tsc/NeuroglancerSubscriber.js +219 -53
  9. package/dist-tsc/ReactNeuroglancer.d.ts +2 -2
  10. package/dist-tsc/ReactNeuroglancer.d.ts.map +1 -1
  11. package/dist-tsc/ReactNeuroglancer.js +31 -28
  12. package/dist-tsc/data-hook-ng-utils.d.ts +18 -20
  13. package/dist-tsc/data-hook-ng-utils.d.ts.map +1 -1
  14. package/dist-tsc/data-hook-ng-utils.js +136 -68
  15. package/dist-tsc/shader-utils.d.ts +126 -0
  16. package/dist-tsc/shader-utils.d.ts.map +1 -0
  17. package/dist-tsc/shader-utils.js +547 -0
  18. package/dist-tsc/shader-utils.test.d.ts +2 -0
  19. package/dist-tsc/shader-utils.test.d.ts.map +1 -0
  20. package/dist-tsc/shader-utils.test.js +364 -0
  21. package/dist-tsc/use-memo-custom-comparison.d.ts +14 -0
  22. package/dist-tsc/use-memo-custom-comparison.d.ts.map +1 -0
  23. package/dist-tsc/use-memo-custom-comparison.js +150 -0
  24. package/package.json +9 -8
  25. package/src/Neuroglancer.js +31 -26
  26. package/src/NeuroglancerSubscriber.js +361 -81
  27. package/src/README.md +28 -0
  28. package/src/ReactNeuroglancer.js +34 -27
  29. package/src/data-hook-ng-utils.js +178 -78
  30. package/src/shader-utils.js +653 -0
  31. package/src/shader-utils.test.js +432 -0
  32. package/src/use-memo-custom-comparison.js +190 -0
  33. package/dist/index-Wdrc02VW.js +0 -32390
  34. package/dist-tsc/data-hook-ng-utils.test.d.ts +0 -2
  35. package/dist-tsc/data-hook-ng-utils.test.d.ts.map +0 -1
  36. package/dist-tsc/data-hook-ng-utils.test.js +0 -35
  37. package/src/data-hook-ng-utils.test.js +0 -52
@@ -427,7 +427,7 @@ export default class Neuroglancer extends React.Component {
427
427
  this.disposers = [];
428
428
  this.prevColorOverrides = new Set();
429
429
  this.overrideColorsById = Object.create(null);
430
- this.allKnownIds = new Set();
430
+ this.allKnownIdsByLayer = {};
431
431
  }
432
432
 
433
433
  minimalPoseSnapshot = () => {
@@ -476,30 +476,36 @@ export default class Neuroglancer extends React.Component {
476
476
  };
477
477
 
478
478
  /* To add colors to the segments, turning unselected to grey */
479
- applyColorsAndVisibility = (cellColorMapping) => {
479
+ applyColorsAndVisibility = (cellColorMappingByLayer) => {
480
480
  if (!this.viewer) return;
481
- // Track all ids we've ever seen so we can grey the ones
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)
481
+ // Build full color table per layer
497
482
  const baseLayers = (this.props.viewerState?.layers)
498
483
  ?? (this.viewer.state.toJSON().layers || []);
499
484
 
500
- const newLayers = baseLayers.map((layer, idx) => {
501
- // if only one layer, take that or check layer.type === 'segmentation'.
502
- if (idx === 0 || layer?.type === 'segmentation') {
485
+ const newLayers = baseLayers.map((layer) => {
486
+ // Match layerScope by checking if the NG layer name contains the scope key.
487
+ // NG layer names are of the form:
488
+ // "obsSegmentations-init_A_obsSegmentations_0-init_A_obsSegmentations_0"
489
+ const layerScope = Object.keys(cellColorMappingByLayer).find(scope => layer.name?.includes(scope));
490
+
491
+ const selected = { ...(cellColorMappingByLayer[layerScope] || {}) };
492
+
493
+ // Track all known IDs for this layer scope
494
+ if (!this.allKnownIdsByLayer) this.allKnownIdsByLayer = {};
495
+ if (!this.allKnownIdsByLayer[layerScope]) {
496
+ this.allKnownIdsByLayer[layerScope] = new Set();
497
+ }
498
+ for (const id of Object.keys(selected)) {
499
+ this.allKnownIdsByLayer[layerScope].add(id);
500
+ }
501
+
502
+ // Build a full color table: selected keep their hex, others grey
503
+ const fullSegmentColors = {};
504
+ for (const id of this.allKnownIdsByLayer[layerScope] || []) {
505
+ fullSegmentColors[id] = selected[id] || GREY_HEX;
506
+ }
507
+
508
+ if (layer.type === 'segmentation') {
503
509
  return { ...layer, segmentColors: fullSegmentColors };
504
510
  }
505
511
  return layer;
@@ -624,7 +630,7 @@ export default class Neuroglancer extends React.Component {
624
630
  }
625
631
 
626
632
  componentDidUpdate(prevProps, prevState) {
627
- const { viewerState, cellColorMapping } = this.props;
633
+ const { viewerState, cellColorMapping: cellColorMappingByLayer } = this.props;
628
634
  // The restoreState() call clears the 'selected' (hovered on) segment, which is needed
629
635
  // by Neuroglancer's code to toggle segment visibilty on a mouse click. To free the user
630
636
  // from having to move the mouse before clicking, save the selected segment and restore
@@ -702,8 +708,8 @@ export default class Neuroglancer extends React.Component {
702
708
  this.withoutEmitting(() => {
703
709
  const layers = Array.isArray(viewerState.layers) ? viewerState.layers : [];
704
710
  this.viewer.state.restoreState({ layers });
705
- if (cellColorMapping && Object.keys(cellColorMapping).length) {
706
- this.applyColorsAndVisibility(cellColorMapping);
711
+ if (cellColorMappingByLayer && Object.keys(cellColorMappingByLayer).length) {
712
+ this.applyColorsAndVisibility(cellColorMappingByLayer);
707
713
  }
708
714
  });
709
715
  }
@@ -712,12 +718,13 @@ export default class Neuroglancer extends React.Component {
712
718
  // this was to avid NG randomly assigning colors to the segments by resetting them
713
719
  const prevSize = prevProps.cellColorMapping
714
720
  ? Object.keys(prevProps.cellColorMapping).length : 0;
715
- const currSize = cellColorMapping ? Object.keys(cellColorMapping).length : 0;
716
- const mappingRefChanged = prevProps.cellColorMapping !== cellColorMapping;
721
+ const currSize = cellColorMappingByLayer
722
+ ? Object.keys(cellColorMappingByLayer).length : 0;
723
+ const mappingRefChanged = prevProps.cellColorMapping !== this.props.cellColorMapping;
717
724
  if (!this.didLayersChange(prevVS, viewerState)
718
725
  && (mappingRefChanged || prevSize !== currSize)) {
719
726
  this.withoutEmitting(() => {
720
- this.applyColorsAndVisibility(cellColorMapping);
727
+ this.applyColorsAndVisibility(cellColorMappingByLayer);
721
728
  });
722
729
  }
723
730
 
@@ -1,5 +1,7 @@
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
 
4
6
 
5
7
  export const DEFAULT_NG_PROPS = {
@@ -8,11 +10,17 @@ export const DEFAULT_NG_PROPS = {
8
10
  projectionOrientation: [0, 0, 0, 1],
9
11
  projectionScale: 1024,
10
12
  crossSectionScale: 1,
13
+ dimensions: {
14
+ x: [1, 'nm'],
15
+ y: [1, 'nm'],
16
+ z: [1, 'nm'],
17
+ },
18
+ layers: [],
11
19
  };
12
20
 
13
21
  function toPrecomputedSource(url) {
14
22
  if (!url) {
15
- return undefined;
23
+ throw new Error('toPrecomputedSource: URL is required');
16
24
  }
17
25
  return `precomputed://${url}`;
18
26
  }
@@ -43,8 +51,14 @@ function isInNanometerRange(value, unit, minNm = 1, maxNm = 100) {
43
51
  * @param {object} opts
44
52
  * @returns {{ x:[number,'nm'], y:[number,'nm'], z:[number,'nm'] }}
45
53
  */
46
- function normalizeDimensionsToNanometers(opts) {
47
- const { dimensionUnit, dimensionX, dimensionY, dimensionZ } = opts;
54
+ export function normalizeDimensionsToNanometers(opts) {
55
+ const {
56
+ dimensionUnit,
57
+ dimensionX,
58
+ dimensionY,
59
+ dimensionZ,
60
+ ...otherOptions
61
+ } = opts;
48
62
 
49
63
  if (!dimensionUnit || !dimensionX || !dimensionY || !dimensionZ) {
50
64
  console.warn('Missing dimension info');
@@ -56,82 +70,28 @@ function normalizeDimensionsToNanometers(opts) {
56
70
  console.warn('Dimension was converted to nm units');
57
71
  }
58
72
  return {
59
- x: xNm ? [dimensionX, dimensionUnit] : [1, 'nm'],
60
- y: yNm ? [dimensionY, dimensionUnit] : [1, 'nm'],
61
- z: zNm ? [dimensionZ, dimensionUnit] : [1, 'nm'],
73
+ // The dimension-related fields are formatted differently in the fileDef.options
74
+ // vs. what the viewerState expects.
75
+ dimensions: {
76
+ x: xNm ? [dimensionX, dimensionUnit] : [1, 'nm'],
77
+ y: yNm ? [dimensionY, dimensionUnit] : [1, 'nm'],
78
+ z: zNm ? [dimensionZ, dimensionUnit] : [1, 'nm'],
79
+ },
80
+ // The non-dimension-related options can be passed through without modification.
81
+ ...otherOptions,
62
82
  };
63
83
  }
64
84
 
65
- export function extractDataTypeEntities(loaders, dataset, dataType) {
66
- const datasetEntry = loaders?.[dataset];
67
- const internMap = datasetEntry?.loaders?.[dataType];
68
- if (!internMap || typeof internMap.entries !== 'function') return [];
69
-
70
- return Array.from(internMap.entries()).map(([key, loader]) => {
71
- const url = loader?.url ?? loader?.dataSource?.url ?? undefined;
72
- const fileUid = key?.fileUid
73
- ?? loader?.coordinationValues?.fileUid
74
- ?? undefined;
75
-
76
- const { position, projectionOrientation,
77
- projectionScale, crossSectionScale } = loader?.options ?? {};
78
- const isPrecomputed = loader?.fileType.includes('precomputed');
79
- if (!isPrecomputed) {
80
- console.warn('Filetype needs to be precomputed');
81
- }
82
- return {
83
- key,
84
- type: 'segmentation',
85
- fileUid,
86
- layout: DEFAULT_NG_PROPS.layout,
87
- url,
88
- source: toPrecomputedSource(url),
89
- name: fileUid ?? key?.name ?? 'segmentation',
90
- // For precomputed: nm is the unit used
91
- dimensions: normalizeDimensionsToNanometers(loader?.options),
92
- // If not provided, no error, but difficult to see the data
93
- position: Array.isArray(position) && position.length === 3
94
- ? position : DEFAULT_NG_PROPS.position,
95
- // If not provided, will have a default orientation
96
- projectionOrientation: Array.isArray(projectionOrientation)
97
- && projectionOrientation.length === 4
98
- ? projectionOrientation : DEFAULT_NG_PROPS.projectionOrientation,
99
- projectionScale: Number.isFinite(projectionScale)
100
- ? projectionScale : DEFAULT_NG_PROPS.projectionScale,
101
- crossSectionScale: Number.isFinite(crossSectionScale)
102
- ? crossSectionScale : DEFAULT_NG_PROPS.crossSectionScale,
103
- };
104
- });
105
- }
106
-
107
- export function useExtractOptionsForNg(loaders, dataset, dataType) {
108
- const extractedEntities = useMemo(
109
- () => extractDataTypeEntities(loaders, dataset, dataType),
110
- [loaders, dataset, dataType],
111
- );
112
- const layers = useMemo(() => extractedEntities
113
- .filter(t => t.source)
114
- .map(t => ({
115
- type: t.type,
116
- source: t.source,
117
- segments: [],
118
- name: t.name || 'segmentation',
119
- })), [extractedEntities]);
120
-
121
- const viewerState = useMemo(() => ({
122
- dimensions: extractedEntities[0]?.dimensions,
123
- position: extractedEntities[0]?.position,
124
- crossSectionScale: extractedEntities[0]?.crossSectionScale,
125
- projectionOrientation: extractedEntities[0]?.projectionOrientation,
126
- projectionScale: extractedEntities[0]?.projectionScale,
127
- layers,
128
- layout: extractedEntities[0].layout,
129
- }));
130
-
131
- return [viewerState];
85
+ export function toNgLayerName(dataType, layerScope, channelScope = null) {
86
+ if (dataType === DataType.OBS_SEGMENTATIONS) {
87
+ return `obsSegmentations-${layerScope}-${channelScope}`;
88
+ }
89
+ if (dataType === DataType.OBS_POINTS) {
90
+ return `obsPoints-${layerScope}`;
91
+ }
92
+ throw new Error(`Unsupported data type: ${dataType}`);
132
93
  }
133
94
 
134
-
135
95
  /**
136
96
  * Get the parameters for NG's viewerstate.
137
97
  * @param {object} loaders The object mapping
@@ -145,8 +105,148 @@ export function useExtractOptionsForNg(loaders, dataset, dataType) {
145
105
  * @returns [viewerState]
146
106
  */
147
107
  export function useNeuroglancerViewerState(
148
- loaders, dataset, isRequired,
149
- coordinationSetters, initialCoordinationValues, matchOn,
108
+ theme,
109
+ segmentationLayerScopes,
110
+ segmentationChannelScopesByLayer,
111
+ segmentationLayerCoordination,
112
+ segmentationChannelCoordination,
113
+ obsSegmentationsUrls,
114
+ obsSegmentationsData,
115
+ pointLayerScopes,
116
+ pointLayerCoordination,
117
+ obsPointsUrls,
118
+ obsPointsData,
119
+ pointMultiIndicesData,
150
120
  ) {
151
- return useExtractOptionsForNg(loaders, dataset, DataType.OBS_SEGMENTATIONS, matchOn);
121
+ const viewerState = useMemoCustomComparison(() => {
122
+ let result = cloneDeep(DEFAULT_NG_PROPS);
123
+
124
+ // ======= SEGMENTATIONS =======
125
+
126
+ // Iterate over segmentation layers and channels.
127
+ segmentationLayerScopes.forEach((layerScope) => {
128
+ const layerCoordination = segmentationLayerCoordination[0][layerScope];
129
+ const channelScopes = segmentationChannelScopesByLayer[layerScope] || [];
130
+ const layerData = obsSegmentationsData[layerScope];
131
+ const layerUrl = obsSegmentationsUrls[layerScope]?.[0]?.url;
132
+
133
+ if (layerUrl && layerData) {
134
+ const {
135
+ spatialLayerVisible,
136
+ } = layerCoordination || {};
137
+ channelScopes.forEach((channelScope) => {
138
+ const channelCoordination = segmentationChannelCoordination[0]
139
+ ?.[layerScope]?.[channelScope];
140
+ const {
141
+ spatialChannelVisible,
142
+ } = channelCoordination || {};
143
+ result = {
144
+ ...result,
145
+ layers: [
146
+ ...result.layers,
147
+ {
148
+ type: 'segmentation',
149
+ source: toPrecomputedSource(layerUrl),
150
+ segments: [],
151
+ name: toNgLayerName(DataType.OBS_SEGMENTATIONS, layerScope, channelScope),
152
+ visible: spatialLayerVisible && spatialChannelVisible, // Both layer and channel
153
+ // visibility must be true for the layer to be visible.
154
+ // TODO: update this to extract specific properties from
155
+ // neuroglancerOptions as needed.
156
+ ...(layerData.neuroglancerOptions ?? {}),
157
+ },
158
+ ],
159
+ };
160
+ });
161
+ }
162
+ });
163
+
164
+ // ======= POINTS =======
165
+
166
+ // Iterate over point layers.
167
+ pointLayerScopes.forEach((layerScope) => {
168
+ const layerCoordination = pointLayerCoordination[0][layerScope];
169
+ const layerData = obsPointsData[layerScope];
170
+ const layerUrl = obsPointsUrls[layerScope]?.[0]?.url;
171
+
172
+ const featureIndex = pointMultiIndicesData[layerScope]?.featureIndex;
173
+
174
+ if (layerUrl && layerData) {
175
+ const {
176
+ spatialLayerVisible,
177
+ spatialLayerOpacity,
178
+ obsColorEncoding,
179
+ spatialLayerColor,
180
+ featureSelection,
181
+ featureFilterMode,
182
+ featureColor,
183
+ } = layerCoordination || {};
184
+
185
+ // Dynamically construct the shader based on the color encoding
186
+ // and other coordination values.
187
+ const shader = getPointsShader({
188
+ theme,
189
+ featureIndex,
190
+ spatialLayerOpacity,
191
+ obsColorEncoding,
192
+ spatialLayerColor,
193
+ featureSelection,
194
+ featureFilterMode,
195
+ featureColor,
196
+
197
+ featureIndexProp: layerData.neuroglancerOptions?.featureIndexProp,
198
+ pointIndexProp: layerData.neuroglancerOptions?.pointIndexProp,
199
+ });
200
+
201
+ result = {
202
+ ...result,
203
+ layers: [
204
+ ...result.layers,
205
+ {
206
+ type: 'annotation',
207
+ source: {
208
+ url: toPrecomputedSource(layerUrl),
209
+ subsources: {
210
+ default: true,
211
+ },
212
+ enableDefaultSubsources: false,
213
+ },
214
+ tab: 'annotations',
215
+ shader,
216
+ name: toNgLayerName(DataType.OBS_POINTS, layerScope),
217
+ visible: spatialLayerVisible,
218
+ // Options from layerData.neuroglancerOptions
219
+ // like projectionAnnotationSpacing:
220
+ projectionAnnotationSpacing: layerData.neuroglancerOptions
221
+ ?.projectionAnnotationSpacing ?? 1.0,
222
+ },
223
+ ],
224
+
225
+ // TODO: is this needed?
226
+ // The selected layer here will overwrite anything
227
+ // that was previously specified.
228
+ selectedLayer: {
229
+ // size: ? // TODO: is this needed?
230
+ layer: toNgLayerName(DataType.OBS_POINTS, layerScope),
231
+ },
232
+ };
233
+ }
234
+ });
235
+ return result;
236
+ }, {
237
+ theme,
238
+ segmentationLayerScopes,
239
+ segmentationChannelScopesByLayer,
240
+ segmentationLayerCoordination,
241
+ segmentationChannelCoordination,
242
+ obsSegmentationsUrls,
243
+ obsSegmentationsData,
244
+ pointLayerScopes,
245
+ pointLayerCoordination,
246
+ obsPointsUrls,
247
+ obsPointsData,
248
+ pointMultiIndicesData,
249
+ }, customIsEqualForInitialViewerState);
250
+
251
+ return viewerState;
152
252
  }