@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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { N } from "./index-DvhFVdN_.js";
1
+ import { N } from "./index-anGvS-pL.js";
2
2
  export {
3
3
  N as NeuroglancerSubscriber
4
4
  };
@@ -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":"AAUA;IACE,wBAcC;IAZC,gBAAgC;IAChC,sBAA8C;IAC9C,oCAAuC;IACvC,iBAAuB;IACvB,gDAA4B;IAC5B,2BAAiC;IACjC,sCAA4B;IAsD9B,2CAGC;IAjDD,4BA4CC;IAhDC,0BAAgD;IAChD,iCAA8D;IAsDhE,yCAQC;IAED,sBAoBC;CACF"}
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"}
@@ -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
- return new ChunkWorker();
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
- viewer.inputEventBindings.sliceView.set('at:dblclick0', () => { });
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
- this.prevClickHandler = (event) => {
37
- if (event.button === 0) {
38
- // Wait for mouseState to update
39
- requestAnimationFrame(() => {
40
- const { pickedValue, pickedRenderLayer } = viewer.mouseState;
41
- // Only trigger selection when a segment is clicked rather than any click on the view
42
- if (pickedValue && pickedValue.low !== undefined && pickedRenderLayer) {
43
- this.latestOnSegmentClick?.(pickedValue.low);
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 { onSegmentClick, onSelectHoveredCoords } = this.props;
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":"AAsEA,gEAusBC"}
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
- if (layerSets && layerIndex) {
136
- const { obsSetColor, obsColorEncoding, obsSetSelection, additionalObsSets, } = segmentationChannelCoordination[0][layerScope][channelScope];
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
- // 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]);
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 newLayer0 = {
466
- ...prevLayer,
467
- segments: nextSegments,
468
- segmentColors: cellColorMapping,
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: prevSegments.length === 0 ? [newLayer0, ...(current?.layers?.slice(1)
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
- }, [cellColorMapping, spatialZoom, spatialRotationX, spatialRotationY,
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, segmentationLayerScopes: segmentationLayerScopes, segmentationLayerCoordination: segmentationLayerCoordination, segmentationChannelScopesByLayer: segmentationChannelScopesByLayer, segmentationChannelCoordination: segmentationChannelCoordination }) }), hasLayers ? (_jsx(NeuroglancerComp, { classes: classes, onSegmentClick: onSegmentClick, onSelectHoveredCoords: onSegmentHighlight, viewerState: derivedViewerState, cellColorMapping: cellColorMapping, setViewerState: handleStateUpdate })) : null] }) }));
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
- allKnownIds: Set<any>;
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: (cellColorMapping: any) => void;
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;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;;;;;;;;;;;;MAYE;IAEF,wBAYC;IAVC,iBAAoC;IACpC,YAAkB;IAElB,2BAA8B;IAC9B,yBAA+B;IAC/B,kBAAwB;IACxB,iBAAmB;IACnB,6BAAmC;IACnC,wBAA6C;IAC7C,sBAA4B;IAG9B;;;;MASE;IAGF,+BAWE;IAGF,mCAKE;IAGF,uDASE;IAGF,0DAgCE;IAEF,0BA+GC;IAED,yDAgIC;IAED,6BAUC;IAcD,mCAQC;IAED,0DAyCE;IAEF,yCAOE;IAEF,0BAsCE;IA5BI,mCAAyB;IA+B/B,sCAWE;IAEF,qCAUE;IAEF,sBAOC;CACF;;qBAh3Ba,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;;;;eAEV,KAAK,CAAC,MAAM,CAAC"}
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.allKnownIds = new Set();
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 = (cellColorMapping) => {
431
+ applyColorsAndVisibility = (cellColorMappingByLayer) => {
427
432
  if (!this.viewer)
428
433
  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)
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, idx) => {
449
- // if only one layer, take that or check layer.type === 'segmentation'.
450
- if (idx === 0 || layer?.type === 'segmentation') {
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
- // TODO: This is purely for debugging and we need to remove it.
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 (cellColorMapping && Object.keys(cellColorMapping).length) {
637
- this.applyColorsAndVisibility(cellColorMapping);
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.keys(prevProps.cellColorMapping).length : 0;
645
- const currSize = cellColorMapping ? Object.keys(cellColorMapping).length : 0;
646
- const mappingRefChanged = prevProps.cellColorMapping !== cellColorMapping;
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(cellColorMapping);
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,yXAiJC"}
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"}