@vitessce/neuroglancer 3.6.18 → 3.7.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/{ReactNeuroglancer-Crquekcy.js → ReactNeuroglancer-C0i-a6Cw.js} +717 -1268
- package/dist/{index-BNCfoEHv.js → index-w8xI9TWU.js} +2211 -478
- package/dist/index.js +1 -1
- package/dist-tsc/Neuroglancer.d.ts +5 -3
- package/dist-tsc/Neuroglancer.d.ts.map +1 -1
- package/dist-tsc/Neuroglancer.js +31 -72
- package/dist-tsc/NeuroglancerSubscriber.d.ts.map +1 -1
- package/dist-tsc/NeuroglancerSubscriber.js +329 -93
- package/dist-tsc/ReactNeuroglancer.d.ts +147 -2
- package/dist-tsc/ReactNeuroglancer.d.ts.map +1 -1
- package/dist-tsc/ReactNeuroglancer.js +819 -5
- package/dist-tsc/styles.d.ts.map +1 -1
- package/dist-tsc/styles.js +3 -1
- package/dist-tsc/utils.d.ts +41 -0
- package/dist-tsc/utils.d.ts.map +1 -0
- package/dist-tsc/utils.js +117 -0
- package/dist-tsc/utils.test.d.ts +2 -0
- package/dist-tsc/utils.test.d.ts.map +1 -0
- package/dist-tsc/utils.test.js +34 -0
- package/package.json +12 -12
- package/src/Neuroglancer.js +32 -91
- package/src/NeuroglancerSubscriber.js +400 -108
- package/src/ReactNeuroglancer.js +912 -6
- package/src/styles.js +3 -1
- package/src/utils.js +156 -0
- package/src/utils.test.js +44 -0
|
@@ -1,6 +1,820 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/* *****
|
|
3
|
+
This react wrapper is originally developed by janelia-flyem group now called neuroglancerHub
|
|
4
|
+
code source: https://github.com/neuroglancerhub/react-neuroglancer/blob/master/src/index.jsx
|
|
5
|
+
|
|
6
|
+
The following modifications were added for Vitessce integration and
|
|
7
|
+
are marked with a comment referring to Vitessce
|
|
8
|
+
1. applyColorsAndVisibility() adds the cellColorMapping (prop) from the cell-set selection
|
|
9
|
+
2. componentDidMount() and componentDidUpdate() renders and updates the viewerState using
|
|
10
|
+
the restoreState() for updating the camera setting and segments.
|
|
11
|
+
3. A set of functions to avoid frequent state updates and provide smoother animations
|
|
12
|
+
within the rendering cycling using requestAnimationFrame()
|
|
13
|
+
*/
|
|
14
|
+
/* eslint-disable max-len, consistent-return, react/destructuring-assignment, class-methods-use-this, no-restricted-syntax, no-continue, no-unused-vars, react/forbid-prop-types, no-dupe-keys */
|
|
2
15
|
import React from 'react';
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
16
|
+
import { AnnotationUserLayer } from '@janelia-flyem/neuroglancer/dist/module/neuroglancer/annotation/user_layer.js';
|
|
17
|
+
import { getObjectColor } from '@janelia-flyem/neuroglancer/dist/module/neuroglancer/segmentation_display_state/frontend.js';
|
|
18
|
+
import { SegmentationUserLayer } from '@janelia-flyem/neuroglancer/dist/module/neuroglancer/segmentation_user_layer.js';
|
|
19
|
+
import { serializeColor } from '@janelia-flyem/neuroglancer/dist/module/neuroglancer/util/color.js';
|
|
20
|
+
import { setupDefaultViewer } from '@janelia-flyem/neuroglancer';
|
|
21
|
+
import { Uint64 } from '@janelia-flyem/neuroglancer/dist/module/neuroglancer/util/uint64.js';
|
|
22
|
+
import { urlSafeParse } from '@janelia-flyem/neuroglancer/dist/module/neuroglancer/util/json.js';
|
|
23
|
+
/* eslint-disable max-len */
|
|
24
|
+
// import { encodeFragment } from '@janelia-flyem/neuroglancer/dist/module/neuroglancer/ui/url_hash_binding';
|
|
25
|
+
import { diffCameraState } from './utils.js';
|
|
26
|
+
// TODO: Grey color used by Vitessce - maybe set globally
|
|
27
|
+
const GREY_HEX = '#323232';
|
|
28
|
+
const viewersKeyed = {};
|
|
29
|
+
let viewerNoKey;
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {Object} NgProps
|
|
32
|
+
* @property {number} perspectiveZoom
|
|
33
|
+
* @property {Object} viewerState
|
|
34
|
+
* @property {string} brainMapsClientId
|
|
35
|
+
* @property {string} key
|
|
36
|
+
* @property {Object<string, string>} cellColorMapping
|
|
37
|
+
*
|
|
38
|
+
* @property {Array} eventBindingsToUpdate
|
|
39
|
+
* An array of event bindings to change in Neuroglancer. The array format is as follows:
|
|
40
|
+
* [[old-event1, new-event1], [old-event2], old-event3]
|
|
41
|
+
* Here, `old-event1`'s will be unbound and its action will be re-bound to `new-event1`.
|
|
42
|
+
* The bindings for `old-event2` and `old-event3` will be removed.
|
|
43
|
+
* Neuroglancer has its own syntax for event descriptors, and here are some examples:
|
|
44
|
+
* 'keya', 'shift+keyb' 'control+keyc', 'digit4', 'space', 'arrowleft', 'comma', 'period',
|
|
45
|
+
* 'minus', 'equal', 'bracketleft'.
|
|
46
|
+
*
|
|
47
|
+
* @property {(segment:any|null, layer:any) => void} onSelectedChanged
|
|
48
|
+
* A function of the form `(segment, layer) => {}`, called each time there is a change to
|
|
49
|
+
* the segment the user has 'selected' (i.e., hovered over) in Neuroglancer.
|
|
50
|
+
* The `segment` argument will be a Neuroglancer `Uint64` with the ID of the now-selected
|
|
51
|
+
* segment, or `null` if no segment is now selected.
|
|
52
|
+
* The `layer` argument will be a Neuroglaner `ManagedUserLayer`, whose `layer` property
|
|
53
|
+
* will be a Neuroglancer `SegmentationUserLayer`.
|
|
54
|
+
*
|
|
55
|
+
* @property {(segments:any, layer:any) => void} onVisibleChanged
|
|
56
|
+
* A function of the form `(segments, layer) => {}`, called each time there is a change to
|
|
57
|
+
* the segments the user has designated as 'visible' (i.e., double-clicked on) in Neuroglancer.
|
|
58
|
+
* The `segments` argument will be a Neuroglancer `Uint64Set` whose elements are `Uint64`
|
|
59
|
+
* instances for the IDs of the now-visible segments.
|
|
60
|
+
* The `layer` argument will be a Neuroglaner `ManagedUserLayer`, whose `layer` property
|
|
61
|
+
* will be a Neuroglancer `SegmentationUserLayer`.
|
|
62
|
+
*
|
|
63
|
+
* @property {() => void} onSelectionDetailsStateChanged
|
|
64
|
+
* A function of the form `() => {}` to respond to selection changes in the viewer.
|
|
65
|
+
* @property {() => void} onViewerStateChanged
|
|
66
|
+
*
|
|
67
|
+
* @property {Array<Object>} callbacks
|
|
68
|
+
* // ngServer: string,
|
|
69
|
+
*/
|
|
70
|
+
// Adopted from neuroglancer/ui/url_hash_binding.ts
|
|
71
|
+
export function parseUrlHash(url) {
|
|
72
|
+
let state = null;
|
|
73
|
+
let s = url.replace(/^[^#]+/, '');
|
|
74
|
+
if (s === '' || s === '#' || s === '#!') {
|
|
75
|
+
s = '#!{}';
|
|
76
|
+
}
|
|
77
|
+
if (s.startsWith('#!+')) {
|
|
78
|
+
s = s.slice(3);
|
|
79
|
+
// Firefox always %-encodes the URL even if it is not typed that way.
|
|
80
|
+
s = decodeURIComponent(s);
|
|
81
|
+
state = urlSafeParse(s);
|
|
82
|
+
}
|
|
83
|
+
else if (s.startsWith('#!')) {
|
|
84
|
+
s = s.slice(2);
|
|
85
|
+
s = decodeURIComponent(s);
|
|
86
|
+
state = urlSafeParse(s);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
throw new Error('URL hash is expected to be of the form \'#!{...}\' or \'#!+{...}\'.');
|
|
90
|
+
}
|
|
91
|
+
return state;
|
|
92
|
+
}
|
|
93
|
+
export function getNeuroglancerViewerState(key) {
|
|
94
|
+
const v = key ? viewersKeyed[key] : viewerNoKey;
|
|
95
|
+
return v ? v.state.toJSON() : {};
|
|
96
|
+
}
|
|
97
|
+
export function getNeuroglancerColor(idStr, key) {
|
|
98
|
+
try {
|
|
99
|
+
const id = Uint64.parseString(idStr);
|
|
100
|
+
const v = key ? viewersKeyed[key] : viewerNoKey;
|
|
101
|
+
if (v) {
|
|
102
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
103
|
+
for (const layer of v.layerManager.managedLayers) {
|
|
104
|
+
if (layer.layer instanceof SegmentationUserLayer) {
|
|
105
|
+
const { displayState } = layer.layer;
|
|
106
|
+
const colorVec = getObjectColor(displayState, id);
|
|
107
|
+
// To get the true color, undo how getObjectColor() indicates hovering.
|
|
108
|
+
if (displayState.segmentSelectionState.isSelected(id)) {
|
|
109
|
+
for (let i = 0; i < 3; i += 1) {
|
|
110
|
+
colorVec[i] = (colorVec[i] - 0.5) / 0.5;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const colorStr = serializeColor(colorVec);
|
|
114
|
+
return colorStr;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// suppress eslint no-empty
|
|
121
|
+
}
|
|
122
|
+
return '';
|
|
123
|
+
}
|
|
124
|
+
export function closeSelectionTab(key) {
|
|
125
|
+
const v = key ? viewersKeyed[key] : viewerNoKey;
|
|
126
|
+
if (v && v.closeSelectionTab) {
|
|
127
|
+
v.closeSelectionTab();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export function getLayerManager(key) {
|
|
131
|
+
const v = key ? viewersKeyed[key] : viewerNoKey;
|
|
132
|
+
if (v) {
|
|
133
|
+
return v.layerManager;
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
export function getManagedLayer(key, name) {
|
|
138
|
+
const layerManager = getLayerManager(key);
|
|
139
|
+
if (layerManager) {
|
|
140
|
+
return layerManager.managedLayers.filter(layer => layer.name === name)[0];
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
export function getAnnotationLayer(key, name) {
|
|
145
|
+
const layer = getManagedLayer(key, name);
|
|
146
|
+
if (layer && layer.layer instanceof AnnotationUserLayer) {
|
|
147
|
+
return layer.layer;
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
export function getAnnotationSource(key, name) {
|
|
152
|
+
const layer = getAnnotationLayer(key, name);
|
|
153
|
+
/* eslint-disable-next-line no-underscore-dangle */
|
|
154
|
+
if (layer && layer.dataSources && layer.dataSources[0].loadState_) {
|
|
155
|
+
/* eslint-disable-next-line no-underscore-dangle */
|
|
156
|
+
const { dataSource } = layer.dataSources[0].loadState_;
|
|
157
|
+
if (dataSource) {
|
|
158
|
+
return dataSource.subsources[0].subsource.annotation;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
export function addLayerSignalRemover(key, name, remover) {
|
|
164
|
+
const layerManager = getLayerManager(key);
|
|
165
|
+
if (layerManager && name && remover) {
|
|
166
|
+
if (!layerManager.customSignalHandlerRemovers) {
|
|
167
|
+
layerManager.customSignalHandlerRemovers = {};
|
|
168
|
+
}
|
|
169
|
+
if (!layerManager.customSignalHandlerRemovers[name]) {
|
|
170
|
+
layerManager.customSignalHandlerRemovers[name] = [];
|
|
171
|
+
}
|
|
172
|
+
layerManager.customSignalHandlerRemovers[name].push(remover);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export function unsubscribeLayersChangedSignals(layerManager, signalKey) {
|
|
176
|
+
if (layerManager) {
|
|
177
|
+
if (layerManager.customSignalHandlerRemovers) {
|
|
178
|
+
if (layerManager.customSignalHandlerRemovers[signalKey]) {
|
|
179
|
+
layerManager.customSignalHandlerRemovers[signalKey].forEach((remover) => {
|
|
180
|
+
remover();
|
|
181
|
+
});
|
|
182
|
+
// eslint-disable-next-line no-param-reassign
|
|
183
|
+
delete layerManager.customSignalHandlerRemovers[signalKey];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export function configureLayersChangedSignals(key, layerConfig) {
|
|
189
|
+
const layerManager = getLayerManager(key);
|
|
190
|
+
if (layerManager) {
|
|
191
|
+
const { layerName } = layerConfig;
|
|
192
|
+
unsubscribeLayersChangedSignals(layerManager, layerName);
|
|
193
|
+
if (layerConfig.process) {
|
|
194
|
+
const recordRemover = remover => addLayerSignalRemover(undefined, layerName, remover);
|
|
195
|
+
recordRemover(layerManager.layersChanged.add(() => {
|
|
196
|
+
const layer = getManagedLayer(undefined, layerName);
|
|
197
|
+
if (layer) {
|
|
198
|
+
layerConfig.process(layer);
|
|
199
|
+
}
|
|
200
|
+
}));
|
|
201
|
+
const layer = getManagedLayer(undefined, layerName);
|
|
202
|
+
if (layer) {
|
|
203
|
+
layerConfig.process(layer);
|
|
204
|
+
}
|
|
205
|
+
return () => {
|
|
206
|
+
if (layerConfig.cancel) {
|
|
207
|
+
layerConfig.cancel();
|
|
208
|
+
}
|
|
209
|
+
unsubscribeLayersChangedSignals(layerManager, layerName);
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return layerConfig.cancel;
|
|
214
|
+
}
|
|
215
|
+
function configureAnnotationSource(source, props, recordRemover) {
|
|
216
|
+
if (source && !source.signalReady) {
|
|
217
|
+
if (props.onAnnotationAdded) {
|
|
218
|
+
recordRemover(source.childAdded.add((annotation) => {
|
|
219
|
+
props.onAnnotationAdded(annotation);
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
if (props.onAnnotationDeleted) {
|
|
223
|
+
recordRemover(source.childDeleted.add((id) => {
|
|
224
|
+
props.onAnnotationDeleted(id);
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
if (props.onAnnotationUpdated) {
|
|
228
|
+
recordRemover(source.childUpdated.add((annotation) => {
|
|
229
|
+
props.onAnnotationUpdated(annotation);
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
if (props.onAnnotationChanged && source.referencesChanged) {
|
|
233
|
+
recordRemover(source.referencesChanged.add(props.onAnnotationChanged));
|
|
234
|
+
}
|
|
235
|
+
// eslint-disable-next-line no-param-reassign
|
|
236
|
+
source.signalReady = true;
|
|
237
|
+
recordRemover(() => {
|
|
238
|
+
// eslint-disable-next-line no-param-reassign
|
|
239
|
+
source.signalReady = false;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function getLoadedDataSource(layer) {
|
|
244
|
+
/* eslint-disable-next-line no-underscore-dangle */
|
|
245
|
+
if (layer.dataSources
|
|
246
|
+
&& layer.dataSources.length > 0
|
|
247
|
+
/* eslint-disable-next-line no-underscore-dangle */
|
|
248
|
+
&& layer.dataSources[0].loadState_
|
|
249
|
+
/* eslint-disable-next-line no-underscore-dangle */
|
|
250
|
+
&& layer.dataSources[0].loadState_.dataSource) {
|
|
251
|
+
/* eslint-disable-next-line no-underscore-dangle */
|
|
252
|
+
return layer.dataSources[0].loadState_.dataSource;
|
|
253
|
+
}
|
|
254
|
+
/* eslint-disable-consistent-return */
|
|
255
|
+
}
|
|
256
|
+
function getAnnotationSourceFromLayer(layer) {
|
|
257
|
+
const dataSource = getLoadedDataSource(layer);
|
|
258
|
+
if (dataSource) {
|
|
259
|
+
return dataSource.subsources[0].subsource.annotation;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function configureAnnotationSourceChange(annotationLayer, props, recordRemover) {
|
|
263
|
+
const configure = () => {
|
|
264
|
+
const source = getAnnotationSourceFromLayer(annotationLayer);
|
|
265
|
+
if (source) {
|
|
266
|
+
configureAnnotationSource(source, props, recordRemover);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const sourceChanged = annotationLayer.dataSourcesChanged;
|
|
270
|
+
if (sourceChanged && !sourceChanged.signalReady) {
|
|
271
|
+
recordRemover(sourceChanged.add(configure));
|
|
272
|
+
sourceChanged.signalReady = true;
|
|
273
|
+
recordRemover(() => {
|
|
274
|
+
sourceChanged.signalReady = false;
|
|
275
|
+
});
|
|
276
|
+
configure();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
export function configureAnnotationLayer(layer, props, recordRemover) {
|
|
280
|
+
if (layer) {
|
|
281
|
+
// eslint-disable-next-line no-param-reassign
|
|
282
|
+
layer.expectingExternalTable = true;
|
|
283
|
+
if (layer.selectedAnnotation
|
|
284
|
+
&& !layer.selectedAnnotation.changed.signalReady) {
|
|
285
|
+
if (props.onAnnotationSelectionChanged) {
|
|
286
|
+
recordRemover(layer.selectedAnnotation.changed.add(() => {
|
|
287
|
+
props.onAnnotationSelectionChanged(layer.selectedAnnotation.value);
|
|
288
|
+
}));
|
|
289
|
+
recordRemover(() => {
|
|
290
|
+
// eslint-disable-next-line no-param-reassign
|
|
291
|
+
layer.selectedAnnotation.changed.signalReady = false;
|
|
292
|
+
});
|
|
293
|
+
// eslint-disable-next-line no-param-reassign
|
|
294
|
+
layer.selectedAnnotation.changed.signalReady = true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
configureAnnotationSourceChange(layer, props, recordRemover);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
export function configureAnnotationLayerChanged(layer, props, recordRemover) {
|
|
301
|
+
if (!layer.layerChanged.signalReady) {
|
|
302
|
+
const remover = layer.layerChanged.add(() => {
|
|
303
|
+
configureAnnotationLayer(layer.layer, props, recordRemover);
|
|
304
|
+
});
|
|
305
|
+
// eslint-disable-next-line no-param-reassign
|
|
306
|
+
layer.layerChanged.signalReady = true;
|
|
307
|
+
recordRemover(remover);
|
|
308
|
+
recordRemover(() => {
|
|
309
|
+
// eslint-disable-next-line no-param-reassign
|
|
310
|
+
layer.layerChanged.signalReady = false;
|
|
311
|
+
});
|
|
312
|
+
configureAnnotationLayer(layer.layer, props, recordRemover);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
export function getAnnotationSelectionHost(key) {
|
|
316
|
+
const viewer = key ? viewersKeyed[key] : viewerNoKey;
|
|
317
|
+
if (viewer) {
|
|
318
|
+
if (viewer.selectionDetailsState) {
|
|
319
|
+
return 'viewer';
|
|
320
|
+
}
|
|
321
|
+
return 'layer';
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
export function getSelectedAnnotationId(key, layerName) {
|
|
326
|
+
const viewer = key ? viewersKeyed[key] : viewerNoKey;
|
|
327
|
+
if (viewer) {
|
|
328
|
+
if (viewer.selectionDetailsState) {
|
|
329
|
+
// New neurolgancer version
|
|
330
|
+
// v.selectionDetailsState.value.layers[0].layer.managedLayer.name
|
|
331
|
+
if (viewer.selectionDetailsState.value) {
|
|
332
|
+
const { layers } = viewer.selectionDetailsState.value;
|
|
333
|
+
if (layers) {
|
|
334
|
+
const layer = layers.find(_layer => _layer.layer.managedLayer.name === layerName);
|
|
335
|
+
if (layer && layer.state) {
|
|
336
|
+
return layer.state.annotationId;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
const layer = getAnnotationLayer(undefined, layerName);
|
|
343
|
+
if (layer && layer.selectedAnnotation && layer.selectedAnnotation.value) {
|
|
344
|
+
return layer.selectedAnnotation.value.id;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
/** @extends {React.Component<NgProps>} */
|
|
351
|
+
export default class Neuroglancer extends React.Component {
|
|
352
|
+
static defaultProps = {
|
|
353
|
+
perspectiveZoom: 20,
|
|
354
|
+
eventBindingsToUpdate: null,
|
|
355
|
+
brainMapsClientId: 'NOT_A_VALID_ID',
|
|
356
|
+
viewerState: null,
|
|
357
|
+
onSelectedChanged: null,
|
|
358
|
+
onVisibleChanged: null,
|
|
359
|
+
onSelectionDetailsStateChanged: null,
|
|
360
|
+
onViewerStateChanged: null,
|
|
361
|
+
key: null,
|
|
362
|
+
callbacks: [],
|
|
363
|
+
ngServer: 'https://neuroglancer-demo.appspot.com/',
|
|
364
|
+
};
|
|
365
|
+
constructor(props) {
|
|
366
|
+
super(props);
|
|
367
|
+
this.ngContainer = React.createRef();
|
|
368
|
+
this.viewer = null;
|
|
369
|
+
/* ** Vitessce Integration update start ** */
|
|
370
|
+
this.muteViewerChanged = false;
|
|
371
|
+
this.prevVisibleIds = new Set();
|
|
372
|
+
this.prevColorMap = null;
|
|
373
|
+
this.disposers = [];
|
|
374
|
+
this.prevColorOverrides = new Set();
|
|
375
|
+
this.overrideColorsById = Object.create(null);
|
|
376
|
+
this.allKnownIds = new Set();
|
|
377
|
+
}
|
|
378
|
+
minimalPoseSnapshot = () => {
|
|
379
|
+
const v = this.viewer;
|
|
380
|
+
const projScale = v.projectionScale?.value;
|
|
381
|
+
const projQuat = v.projectionOrientation?.orientation;
|
|
382
|
+
return {
|
|
383
|
+
position: Array.from(v.position.value || []),
|
|
384
|
+
projectionScale: projScale,
|
|
385
|
+
projectionOrientation: Array.from(projQuat || []),
|
|
386
|
+
};
|
|
387
|
+
};
|
|
388
|
+
// Coalesce many NG changes → one upstream update per frame.
|
|
389
|
+
scheduleEmit = () => {
|
|
390
|
+
let raf = null;
|
|
391
|
+
return () => {
|
|
392
|
+
if (this.muteViewerChanged)
|
|
393
|
+
return; // muted when we push changes
|
|
394
|
+
if (raf !== null)
|
|
395
|
+
return;
|
|
396
|
+
raf = requestAnimationFrame(() => {
|
|
397
|
+
raf = null;
|
|
398
|
+
// console.log('Minimal', this.minimalPoseSnapshot())
|
|
399
|
+
this.props.onViewerStateChanged?.(this.minimalPoseSnapshot());
|
|
400
|
+
});
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
// Guard to mute outgoing emits we are programmatically making changes
|
|
404
|
+
withoutEmitting = (fn) => {
|
|
405
|
+
this.muteViewerChanged = true;
|
|
406
|
+
try {
|
|
407
|
+
fn();
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
requestAnimationFrame(() => { this.muteViewerChanged = false; });
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
// Only consider actual changes in camera settings, i.e., position/rotation/zoom
|
|
414
|
+
didLayersChange = (prevVS, nextVS) => {
|
|
415
|
+
const stripColors = layers => (layers || []).map((l) => {
|
|
416
|
+
if (!l)
|
|
417
|
+
return l;
|
|
418
|
+
const { segmentColors, ...rest } = l;
|
|
419
|
+
return rest;
|
|
420
|
+
});
|
|
421
|
+
const prevLayers = stripColors(prevVS?.layers);
|
|
422
|
+
const nextLayers = stripColors(nextVS?.layers);
|
|
423
|
+
return JSON.stringify(prevLayers) !== JSON.stringify(nextLayers);
|
|
424
|
+
};
|
|
425
|
+
/* To add colors to the segments, turning unselected to grey */
|
|
426
|
+
applyColorsAndVisibility = (cellColorMapping) => {
|
|
427
|
+
if (!this.viewer)
|
|
428
|
+
return;
|
|
429
|
+
// Track all ids we've ever seen so we can grey the ones
|
|
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)
|
|
446
|
+
const baseLayers = (this.props.viewerState?.layers)
|
|
447
|
+
?? (this.viewer.state.toJSON().layers || []);
|
|
448
|
+
const newLayers = baseLayers.map((layer, idx) => {
|
|
449
|
+
// if only one layer, take that or check layer.type === 'segmentation'.
|
|
450
|
+
if (idx === 0 || layer?.type === 'segmentation') {
|
|
451
|
+
return { ...layer, segmentColors: fullSegmentColors };
|
|
452
|
+
}
|
|
453
|
+
return layer;
|
|
454
|
+
});
|
|
455
|
+
this.withoutEmitting(() => {
|
|
456
|
+
this.viewer.state.restoreState({ layers: newLayers });
|
|
457
|
+
});
|
|
458
|
+
/* ** Vitessce integration update end ** */
|
|
459
|
+
};
|
|
460
|
+
componentDidMount() {
|
|
461
|
+
const { viewerState, brainMapsClientId, eventBindingsToUpdate, callbacks,
|
|
462
|
+
// ngServer,
|
|
463
|
+
key, bundleRoot, } = this.props;
|
|
464
|
+
this.viewer = setupDefaultViewer({
|
|
465
|
+
brainMapsClientId,
|
|
466
|
+
target: this.ngContainer.current,
|
|
467
|
+
bundleRoot,
|
|
468
|
+
});
|
|
469
|
+
this.setCallbacks(callbacks);
|
|
470
|
+
if (eventBindingsToUpdate) {
|
|
471
|
+
this.updateEventBindings(eventBindingsToUpdate);
|
|
472
|
+
}
|
|
473
|
+
this.viewer.expectingExternalUI = true;
|
|
474
|
+
// if (ngServer) {
|
|
475
|
+
// this.viewer.makeUrlFromState = (state) => {
|
|
476
|
+
// const newState = { ...state };
|
|
477
|
+
// if (state.layers) {
|
|
478
|
+
// // Do not include clio annotation layers
|
|
479
|
+
// newState.layers = state.layers.filter((layer) => {
|
|
480
|
+
// if (layer.source) {
|
|
481
|
+
// const sourceUrl = layer.source.url || layer.source;
|
|
482
|
+
// if (typeof sourceUrl === 'string') {
|
|
483
|
+
// return !sourceUrl.startsWith('clio://');
|
|
484
|
+
// }
|
|
485
|
+
// }
|
|
486
|
+
// return true;
|
|
487
|
+
// });
|
|
488
|
+
// }
|
|
489
|
+
// return `${ngServer}/#!${encodeFragment(JSON.stringify(newState))}`;
|
|
490
|
+
// };
|
|
491
|
+
// }
|
|
492
|
+
if (this.viewer.selectionDetailsState) {
|
|
493
|
+
this.viewer.selectionDetailsState.changed.add(this.selectionDetailsStateChanged);
|
|
494
|
+
}
|
|
495
|
+
this.viewer.layerManager.layersChanged.add(this.layersChanged);
|
|
496
|
+
/* ** Vitessce Integration update start ** */
|
|
497
|
+
const emit = this.scheduleEmit();
|
|
498
|
+
// Disposers to unsubscribe handles for NG signals to prevent leaks/duplicates.
|
|
499
|
+
this.disposers.push(this.viewer.projectionScale.changed.add(emit));
|
|
500
|
+
this.disposers.push(this.viewer.projectionOrientation.changed.add(emit));
|
|
501
|
+
this.disposers.push(this.viewer.position.changed.add(emit));
|
|
502
|
+
// Initial restore ONLY if provided
|
|
503
|
+
if (viewerState) {
|
|
504
|
+
// restore state only when all the changes are added -
|
|
505
|
+
// avoids calling .changed() for each change and leads to smooth updates
|
|
506
|
+
this.withoutEmitting(() => {
|
|
507
|
+
this.viewer.state.restoreState(viewerState);
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
/* ** Vitessce Integration update end ** */
|
|
511
|
+
// if (viewerState) {
|
|
512
|
+
// const newViewerState = viewerState;
|
|
513
|
+
// if (newViewerState.projectionScale === null) {
|
|
514
|
+
// delete newViewerState.projectionScale;
|
|
515
|
+
// }
|
|
516
|
+
// if (newViewerState.crossSectionScale === null) {
|
|
517
|
+
// delete newViewerState.crossSectionScale;
|
|
518
|
+
// }
|
|
519
|
+
// if (newViewerState.projectionOrientation === null) {
|
|
520
|
+
// delete newViewerState.projectionOrientation;
|
|
521
|
+
// }
|
|
522
|
+
// if (newViewerState.crossSectionOrientation === null) {
|
|
523
|
+
// delete newViewerState.crossSectionOrientation;
|
|
524
|
+
// }
|
|
525
|
+
// this.viewer.state.restoreState(newViewerState);
|
|
526
|
+
// } else {
|
|
527
|
+
// this.viewer.state.restoreState({
|
|
528
|
+
// layers: {
|
|
529
|
+
// grayscale: {
|
|
530
|
+
// type: "image",
|
|
531
|
+
// source:
|
|
532
|
+
// "dvid://https://flyem.dvid.io/ab6e610d4fe140aba0e030645a1d7229/grayscalejpeg"
|
|
533
|
+
// },
|
|
534
|
+
// segmentation: {
|
|
535
|
+
// type: "segmentation",
|
|
536
|
+
// source:
|
|
537
|
+
// "dvid://https://flyem.dvid.io/d925633ed0974da78e2bb5cf38d01f4d/segmentation"
|
|
538
|
+
// }
|
|
539
|
+
// },
|
|
540
|
+
// perspectiveZoom,
|
|
541
|
+
// navigation: {
|
|
542
|
+
// zoomFactor: 8
|
|
543
|
+
// }
|
|
544
|
+
// });
|
|
545
|
+
// }
|
|
546
|
+
// Make the Neuroglancer viewer accessible from getNeuroglancerViewerState().
|
|
547
|
+
// That function can be used to synchronize an external Redux store with any
|
|
548
|
+
// state changes made internally by the viewer.
|
|
549
|
+
if (key) {
|
|
550
|
+
viewersKeyed[key] = this.viewer;
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
viewerNoKey = this.viewer;
|
|
554
|
+
}
|
|
555
|
+
// TODO: This is purely for debugging and we need to remove it.
|
|
556
|
+
// window.viewer = this.viewer;
|
|
557
|
+
}
|
|
558
|
+
componentDidUpdate(prevProps, prevState) {
|
|
559
|
+
const { viewerState, cellColorMapping } = this.props;
|
|
560
|
+
// The restoreState() call clears the 'selected' (hovered on) segment, which is needed
|
|
561
|
+
// by Neuroglancer's code to toggle segment visibilty on a mouse click. To free the user
|
|
562
|
+
// from having to move the mouse before clicking, save the selected segment and restore
|
|
563
|
+
// it after restoreState().
|
|
564
|
+
const selectedSegments = {};
|
|
565
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
566
|
+
for (const layer of this.viewer.layerManager.managedLayers) {
|
|
567
|
+
if (layer.layer instanceof SegmentationUserLayer) {
|
|
568
|
+
const { segmentSelectionState } = layer.layer.displayState;
|
|
569
|
+
selectedSegments[layer.name] = segmentSelectionState.selectedSegment;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
// if (viewerState) {
|
|
573
|
+
// let newViewerState = { ...viewerState };
|
|
574
|
+
// let restoreStates = [
|
|
575
|
+
// () => {
|
|
576
|
+
// this.viewer.state.restoreState(newViewerState);
|
|
577
|
+
// },
|
|
578
|
+
// ];
|
|
579
|
+
// if (viewerState.projectionScale === null) {
|
|
580
|
+
// delete newViewerState.projectionScale;
|
|
581
|
+
// restoreStates.push(() => {
|
|
582
|
+
// this.viewer.projectionScale.reset();
|
|
583
|
+
// });
|
|
584
|
+
// }
|
|
585
|
+
// if (viewerState.crossSectionScale === null) {
|
|
586
|
+
// delete newViewerState.crossSectionScale;
|
|
587
|
+
// }
|
|
588
|
+
// restoreStates.forEach((restore) => restore());
|
|
589
|
+
// }
|
|
590
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
591
|
+
for (const layer of this.viewer.layerManager.managedLayers) {
|
|
592
|
+
if (layer.layer instanceof SegmentationUserLayer) {
|
|
593
|
+
const { segmentSelectionState } = layer.layer.displayState;
|
|
594
|
+
segmentSelectionState.set(selectedSegments[layer.name]);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// For some reason setting position to an empty array doesn't reset
|
|
598
|
+
// the position in the viewer. This should handle those cases by looking
|
|
599
|
+
// for the empty position array and calling the position reset function if
|
|
600
|
+
// found.
|
|
601
|
+
// if ('position' in viewerState) {
|
|
602
|
+
// if (Array.isArray(viewerState.position)) {
|
|
603
|
+
// if (viewerState.position.length === 0) {
|
|
604
|
+
// this.viewer.position.reset();
|
|
605
|
+
// }
|
|
606
|
+
// }
|
|
607
|
+
// }
|
|
608
|
+
/* ** Vitessce Integration update start ** */
|
|
609
|
+
if (!viewerState)
|
|
610
|
+
return;
|
|
611
|
+
// updates NG's viewerstate by calling `restoreState() for segment and position changes separately
|
|
612
|
+
const prevVS = prevProps.viewerState;
|
|
613
|
+
const camState = diffCameraState(prevVS, viewerState);
|
|
614
|
+
// Restore pose ONLY if it actually changed
|
|
615
|
+
if (camState.changed) {
|
|
616
|
+
const patch = {};
|
|
617
|
+
if (camState.scale) {
|
|
618
|
+
patch.projectionScale = viewerState.projectionScale;
|
|
619
|
+
// Couple position with zoom even if it didn’t cross the hard epsilon
|
|
620
|
+
if (Array.isArray(viewerState.position))
|
|
621
|
+
patch.position = viewerState.position;
|
|
622
|
+
}
|
|
623
|
+
else if (camState.pos) {
|
|
624
|
+
patch.position = viewerState.position;
|
|
625
|
+
}
|
|
626
|
+
if (camState.rot)
|
|
627
|
+
patch.projectionOrientation = viewerState.projectionOrientation;
|
|
628
|
+
// Restore the state with updated camera setting/position changes
|
|
629
|
+
this.withoutEmitting(() => this.viewer.state.restoreState(patch));
|
|
630
|
+
}
|
|
631
|
+
// If layers changed (segment list / sources etc.): restore ONLY layers, then colors
|
|
632
|
+
if (this.didLayersChange(prevVS, viewerState)) {
|
|
633
|
+
this.withoutEmitting(() => {
|
|
634
|
+
const layers = Array.isArray(viewerState.layers) ? viewerState.layers : [];
|
|
635
|
+
this.viewer.state.restoreState({ layers });
|
|
636
|
+
if (cellColorMapping && Object.keys(cellColorMapping).length) {
|
|
637
|
+
this.applyColorsAndVisibility(cellColorMapping);
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
// If colors changed (but layers didn’t): re-apply colors
|
|
642
|
+
// this was to avid NG randomly assigning colors to the segments by resetting them
|
|
643
|
+
const prevSize = prevProps.cellColorMapping
|
|
644
|
+
? Object.keys(prevProps.cellColorMapping).length : 0;
|
|
645
|
+
const currSize = cellColorMapping ? Object.keys(cellColorMapping).length : 0;
|
|
646
|
+
const mappingRefChanged = prevProps.cellColorMapping !== cellColorMapping;
|
|
647
|
+
if (!this.didLayersChange(prevVS, viewerState)
|
|
648
|
+
&& (mappingRefChanged || prevSize !== currSize)) {
|
|
649
|
+
this.withoutEmitting(() => {
|
|
650
|
+
this.applyColorsAndVisibility(cellColorMapping);
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
// Treat "real" layer source/type changes differently from segment list changes.
|
|
654
|
+
// We only restore layers (not pose) when sources change OR on the first time segments appear.
|
|
655
|
+
const stripSegFields = layers => (layers || []).map((l) => {
|
|
656
|
+
if (!l)
|
|
657
|
+
return l;
|
|
658
|
+
const { segments, segmentColors, ...rest } = l;
|
|
659
|
+
return rest; // ignore segments + segmentColors for comparison
|
|
660
|
+
});
|
|
661
|
+
const prevLayers = prevProps.viewerState?.layers;
|
|
662
|
+
const nextLayers = viewerState?.layers;
|
|
663
|
+
const prevCore = JSON.stringify(stripSegFields(prevLayers));
|
|
664
|
+
const nextCore = JSON.stringify(stripSegFields(nextLayers));
|
|
665
|
+
const sourcesChanged = prevCore !== nextCore; // real structural change?
|
|
666
|
+
const prevSegCount = (prevLayers && prevLayers[0] && Array.isArray(prevLayers[0].segments))
|
|
667
|
+
? prevLayers[0].segments.length : 0;
|
|
668
|
+
const nextSegCount = (nextLayers && nextLayers[0] && Array.isArray(nextLayers[0].segments))
|
|
669
|
+
? nextLayers[0].segments.length : 0;
|
|
670
|
+
// first-time seeding – from 0 segments → N segments
|
|
671
|
+
const initialSegmentsAdded = prevSegCount === 0 && nextSegCount > 0;
|
|
672
|
+
if (sourcesChanged || initialSegmentsAdded) {
|
|
673
|
+
this.withoutEmitting(() => {
|
|
674
|
+
// restore only the layers to avoid clobbering pose/rotation/zoom.
|
|
675
|
+
this.viewer.state.restoreState({ layers: nextLayers });
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
/* ** Vitessce Integration update end ** */
|
|
679
|
+
}
|
|
680
|
+
componentWillUnmount() {
|
|
681
|
+
/* eslint-disable no-empty */
|
|
682
|
+
this.disposers.forEach((off) => { try {
|
|
683
|
+
off();
|
|
684
|
+
}
|
|
685
|
+
catch { } });
|
|
686
|
+
this.disposers = [];
|
|
687
|
+
const { key } = this.props;
|
|
688
|
+
if (key) {
|
|
689
|
+
delete viewersKeyed[key];
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
viewerNoKey = undefined;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
/* setCallbacks allows us to set a callback on a neuroglancer event
|
|
696
|
+
* each callback created should be in the format:
|
|
697
|
+
* [
|
|
698
|
+
* {
|
|
699
|
+
* name: 'unique-name',
|
|
700
|
+
* event: 'the neuroglancer event to target, eg: click0, keyt',
|
|
701
|
+
* function: (slice) => { slice.whatever }
|
|
702
|
+
* },
|
|
703
|
+
* {...}
|
|
704
|
+
* ]
|
|
705
|
+
*
|
|
706
|
+
*/
|
|
707
|
+
setCallbacks(callbacks) {
|
|
708
|
+
callbacks.forEach((callback) => {
|
|
709
|
+
this.viewer.bindCallback(callback.name, callback.function);
|
|
710
|
+
this.viewer.inputEventBindings.sliceView.set(callback.event, callback.name);
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
updateEventBindings = (eventBindingsToUpdate) => {
|
|
714
|
+
const root = this.viewer.inputEventBindings;
|
|
715
|
+
const traverse = (current) => {
|
|
716
|
+
const replace = (eaMap, event0, event1) => {
|
|
717
|
+
const action = eaMap.get(event0);
|
|
718
|
+
if (action) {
|
|
719
|
+
eaMap.delete(event0);
|
|
720
|
+
if (event1) {
|
|
721
|
+
eaMap.set(event1, action);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
const eventActionMap = current.bindings;
|
|
726
|
+
eventBindingsToUpdate.forEach((oldNewBinding) => {
|
|
727
|
+
const eventOldBase = Array.isArray(oldNewBinding)
|
|
728
|
+
? oldNewBinding[0]
|
|
729
|
+
: oldNewBinding;
|
|
730
|
+
const eventOldA = `at:${eventOldBase}`;
|
|
731
|
+
const eventNewA = oldNewBinding[1]
|
|
732
|
+
? `at:${oldNewBinding[1]}`
|
|
733
|
+
: undefined;
|
|
734
|
+
replace(eventActionMap, eventOldA, eventNewA);
|
|
735
|
+
const eventOldB = `bubble:${eventOldBase}`;
|
|
736
|
+
const eventNewB = oldNewBinding[1]
|
|
737
|
+
? `bubble:${oldNewBinding[1]}`
|
|
738
|
+
: undefined;
|
|
739
|
+
replace(eventActionMap, eventOldB, eventNewB);
|
|
740
|
+
});
|
|
741
|
+
current.parents.forEach((parent) => {
|
|
742
|
+
traverse(parent);
|
|
743
|
+
});
|
|
744
|
+
};
|
|
745
|
+
traverse(root.global);
|
|
746
|
+
traverse(root.perspectiveView);
|
|
747
|
+
traverse(root.sliceView);
|
|
748
|
+
};
|
|
749
|
+
selectionDetailsStateChanged = () => {
|
|
750
|
+
if (this.viewer) {
|
|
751
|
+
const { onSelectionDetailsStateChanged } = this.props;
|
|
752
|
+
if (onSelectionDetailsStateChanged) {
|
|
753
|
+
onSelectionDetailsStateChanged();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
layersChanged = () => {
|
|
758
|
+
if (this.handlerRemovers) {
|
|
759
|
+
// If change handlers have been added already, call the function to remove each one,
|
|
760
|
+
// so there won't be duplicates when new handlers are added below.
|
|
761
|
+
this.handlerRemovers.forEach(remover => remover());
|
|
762
|
+
}
|
|
763
|
+
if (this.viewer) {
|
|
764
|
+
const { onSelectedChanged, onVisibleChanged } = this.props;
|
|
765
|
+
if (onSelectedChanged || onVisibleChanged) {
|
|
766
|
+
this.handlerRemovers = [];
|
|
767
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
768
|
+
for (const layer of this.viewer.layerManager.managedLayers) {
|
|
769
|
+
if (layer.layer instanceof SegmentationUserLayer) {
|
|
770
|
+
const { segmentSelectionState } = layer.layer.displayState;
|
|
771
|
+
const { visibleSegments } = layer.layer.displayState.segmentationGroupState.value;
|
|
772
|
+
if (segmentSelectionState && onSelectedChanged) {
|
|
773
|
+
// Bind the layer so it will be an argument to the handler when called.
|
|
774
|
+
const selectedChanged = this.selectedChanged.bind(undefined, layer);
|
|
775
|
+
const remover = segmentSelectionState.changed.add(selectedChanged);
|
|
776
|
+
this.handlerRemovers.push(remover);
|
|
777
|
+
layer.registerDisposer(remover);
|
|
778
|
+
}
|
|
779
|
+
if (visibleSegments && onVisibleChanged) {
|
|
780
|
+
const visibleChanged = this.visibleChanged.bind(undefined, layer);
|
|
781
|
+
const remover = visibleSegments.changed.add(visibleChanged);
|
|
782
|
+
this.handlerRemovers.push(remover);
|
|
783
|
+
layer.registerDisposer(remover);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
/* ** Vitessce Integration update start ** */
|
|
791
|
+
selectedChanged = (layer) => {
|
|
792
|
+
if (!this.viewer)
|
|
793
|
+
return;
|
|
794
|
+
const { onSelectedChanged } = this.props;
|
|
795
|
+
if (onSelectedChanged) {
|
|
796
|
+
const { segmentSelectionState } = layer.layer.displayState;
|
|
797
|
+
if (!segmentSelectionState)
|
|
798
|
+
return;
|
|
799
|
+
const selected = segmentSelectionState.selectedSegment;
|
|
800
|
+
if (selected) {
|
|
801
|
+
onSelectedChanged(selected, layer);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
visibleChanged = (layer) => {
|
|
806
|
+
if (this.viewer) {
|
|
807
|
+
const { onVisibleChanged } = this.props;
|
|
808
|
+
if (onVisibleChanged) {
|
|
809
|
+
const { visibleSegments } = layer.layer.displayState.segmentationGroupState.value;
|
|
810
|
+
if (visibleSegments) {
|
|
811
|
+
onVisibleChanged(visibleSegments, layer);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
render() {
|
|
817
|
+
const { perspectiveZoom } = this.props;
|
|
818
|
+
return (_jsx("div", { className: "neuroglancer-container", ref: this.ngContainer, children: _jsxs("p", { children: ["Neuroglancer here with zoom ", perspectiveZoom] }) }));
|
|
819
|
+
}
|
|
820
|
+
}
|