@vitessce/neuroglancer 4.0.0-test.0 → 4.0.0-test.2

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.
@@ -1,12 +1,14 @@
1
1
  /* eslint-disable react-refresh/only-export-components */
2
2
  import React, { PureComponent, Suspense } from 'react';
3
- import { ChunkWorker } from '@vitessce/neuroglancer-workers';
3
+ import { ChunkWorker, AsyncComputationWorker } from '@vitessce/neuroglancer-workers';
4
4
  import { NeuroglancerGlobalStyles } from './styles.js';
5
5
 
6
6
  const LazyReactNeuroglancer = React.lazy(() => import('./ReactNeuroglancer.js'));
7
7
 
8
8
  function createWorker() {
9
- return new ChunkWorker();
9
+ const worker = new ChunkWorker();
10
+ worker.AsyncComputationWorker = AsyncComputationWorker;
11
+ return worker;
10
12
  }
11
13
  export class NeuroglancerComp extends PureComponent {
12
14
  constructor(props) {
@@ -31,13 +33,35 @@ export class NeuroglancerComp extends PureComponent {
31
33
  // Mount
32
34
  const { viewer } = viewerRef;
33
35
  this.prevMouseStateChanged = viewer.mouseState.changed;
34
- viewer.inputEventBindings.sliceView.set('at:dblclick0', () => {});
36
+ // For now, can omit the sliceView bindings, as we only use perspectiveView
37
+ // viewer.inputEventBindings.sliceView.set('at:dblclick0', () => {});
35
38
  viewer.inputEventBindings.perspectiveView.set('at:dblclick0', () => {});
36
39
 
37
- // To disable space interaction causing 4panels layout
40
+ // Disable space interaction to prevent triggering 4panels layout.
38
41
  viewer.inputEventBindings.sliceView.set('at:space', () => {});
39
42
  viewer.inputEventBindings.perspectiveView.set('at:space', () => {});
40
43
 
44
+ // Remap plain wheel to ctrl+wheel (zoom) action
45
+ // by traversing the parent binding maps.
46
+ const remapWheelToZoom = (map) => {
47
+ if (map.bindings) {
48
+ const ctrlWheelAction = map.bindings.get('at:control+wheel');
49
+ if (ctrlWheelAction) {
50
+ // Replace plain wheel with the zoom action
51
+ map.bindings.set('at:wheel', ctrlWheelAction);
52
+ const ctrlWheelBubble = map.bindings.get('bubble:control+wheel');
53
+ if (ctrlWheelBubble) {
54
+ map.bindings.set('bubble:wheel', ctrlWheelBubble);
55
+ }
56
+ }
57
+ }
58
+ if (map.parents) {
59
+ map.parents.forEach(p => remapWheelToZoom(p));
60
+ }
61
+ };
62
+
63
+ remapWheelToZoom(viewer.inputEventBindings.perspectiveView);
64
+
41
65
  this.prevHoverHandler = () => {
42
66
  if (viewer.mouseState.pickedValue !== undefined) {
43
67
  const pickedSegment = viewer.mouseState.pickedValue;
@@ -68,7 +92,7 @@ export class NeuroglancerComp extends PureComponent {
68
92
  }
69
93
 
70
94
  render() {
71
- const { classes, viewerState, cellColorMapping } = this.props;
95
+ const { classes, viewerState, cellColorMapping, onLayerLoadingChange } = this.props;
72
96
 
73
97
  return (
74
98
  <>
@@ -79,6 +103,7 @@ export class NeuroglancerComp extends PureComponent {
79
103
  brainMapsClientId="NOT_A_VALID_ID"
80
104
  viewerState={viewerState}
81
105
  onViewerStateChanged={this.onViewerStateChanged}
106
+ onLayerLoadingChange={onLayerLoadingChange}
82
107
  bundleRoot={this.bundleRoot}
83
108
  cellColorMapping={cellColorMapping}
84
109
  ref={this.onRef}
@@ -79,6 +79,7 @@ export function NeuroglancerSubscriber(props) {
79
79
  downloadButtonVisible,
80
80
  removeGridComponent,
81
81
  theme,
82
+ showAxisLines = false,
82
83
  title = 'Spatial',
83
84
  subtitle = 'Powered by Neuroglancer',
84
85
  helpText = ViewHelpMapping.NEUROGLANCER,
@@ -207,6 +208,7 @@ export function NeuroglancerSubscriber(props) {
207
208
  CoordinationType.TOOLTIPS_VISIBLE,
208
209
  CoordinationType.TOOLTIP_CROSSHAIRS_VISIBLE,
209
210
  CoordinationType.LEGEND_VISIBLE,
211
+ CoordinationType.SPATIAL_POINT_STROKE_WIDTH,
210
212
  ],
211
213
  coordinationScopes,
212
214
  coordinationScopesBy,
@@ -283,13 +285,41 @@ export function NeuroglancerSubscriber(props) {
283
285
  segmentationChannelScopesByLayer?.[layerScope]?.forEach((channelScope) => {
284
286
  const { obsSets: layerSets, obsIndex: layerIndex } = obsSegmentationsSetsData
285
287
  ?.[layerScope]?.[channelScope] || {};
286
- if (layerSets && layerIndex) {
287
- const {
288
- obsSetColor,
289
- obsColorEncoding,
290
- obsSetSelection,
291
- additionalObsSets,
292
- } = segmentationChannelCoordination[0][layerScope][channelScope];
288
+ const {
289
+ obsSetColor,
290
+ obsColorEncoding,
291
+ obsSetSelection,
292
+ additionalObsSets,
293
+ spatialChannelColor,
294
+ spatialChannelOpacity,
295
+ } = segmentationChannelCoordination[0][layerScope][channelScope];
296
+ if (obsColorEncoding === 'spatialChannelColor') {
297
+ // All segments get the same static channel color
298
+ if (layerIndex && spatialChannelColor) {
299
+ const hex = rgbToHex(spatialChannelColor);
300
+ const ngCellColors = {};
301
+
302
+ if (obsSetSelection?.length > 0) {
303
+ // Only color the segments belonging to selected sets.
304
+ const mergedCellSets = mergeObsSets(layerSets, additionalObsSets);
305
+ const selectedIds = new Set();
306
+ obsSetSelection.forEach((setPath) => {
307
+ const rootNode = mergedCellSets?.tree?.find(n => n.name === setPath[0]);
308
+ const leafNode = setPath.length > 1
309
+ ? rootNode?.children?.find(n => n.name === setPath[1])
310
+ : rootNode;
311
+ leafNode?.set?.forEach(([id]) => selectedIds.add(String(id)));
312
+ });
313
+ layerIndex.forEach((id) => {
314
+ if (selectedIds.has(String(id))) {
315
+ ngCellColors[id] = hex;
316
+ }
317
+ });
318
+ }
319
+ result[layerScope][channelScope] = ngCellColors;
320
+ result[layerScope].opacity = spatialChannelOpacity ?? 1.0;
321
+ }
322
+ } else if (layerSets && layerIndex) {
293
323
  const mergedCellSets = mergeObsSets(layerSets, additionalObsSets);
294
324
  const cellColors = getCellColors({
295
325
  cellSets: mergedCellSets,
@@ -303,14 +333,8 @@ export function NeuroglancerSubscriber(props) {
303
333
  cellColors.forEach((color, i) => {
304
334
  ngCellColors[i] = rgbToHex(color);
305
335
  });
306
- /* // TODO: Is this necessary?
307
- const obsColorIndices = treeToCellSetColorIndicesBySetNames(
308
- mergedLayerSets,
309
- obsSetSelection,
310
- obsSetColor,
311
- );
312
- */
313
336
  result[layerScope][channelScope] = ngCellColors;
337
+ result[layerScope].opacity = spatialChannelOpacity ?? 1.0;
314
338
  }
315
339
  });
316
340
  });
@@ -329,6 +353,7 @@ export function NeuroglancerSubscriber(props) {
329
353
  // Obtain the Neuroglancer viewerState object.
330
354
  const initalViewerState = useNeuroglancerViewerState(
331
355
  theme,
356
+ showAxisLines,
332
357
  segmentationLayerScopes,
333
358
  segmentationChannelScopesByLayer,
334
359
  segmentationLayerCoordination,
@@ -383,6 +408,9 @@ export function NeuroglancerSubscriber(props) {
383
408
  orbit: spatialRotationOrbit,
384
409
  });
385
410
 
411
+ // Track layer loading state for showing loading indicator
412
+ const [isLayersLoaded, setIsLayersLoaded] = useState(false);
413
+
386
414
  // Track the last coord values we saw, and only mark "vitessce"
387
415
  // when *those* actually change. This prevents cell set renders
388
416
  // from spoofing the source.
@@ -540,14 +568,19 @@ export function NeuroglancerSubscriber(props) {
540
568
  setCellColorEncoding, setCellSetColor, setCellSetSelection,
541
569
  ]);
542
570
 
543
- // Get the ultimate cellColorMapping to pass to NeuroglancerComp as a prop.
544
- // For now, we take the first layer and channel for cell colors.
545
- const cellColorMapping = useMemo(() => (segmentationColorMapping
546
- ?.[segmentationLayerScopes?.[0]]
547
- ?.[segmentationChannelScopesByLayer?.[segmentationLayerScopes?.[0]]?.[0]]
548
- ?? {}
549
- ), [segmentationColorMapping]);
571
+ // Get the ultimate cellColorMapping for each layer to pass to NeuroglancerComp as a prop.
550
572
 
573
+ const cellColorMappingByLayer = useMemo(() => {
574
+ const result = {};
575
+ segmentationLayerScopes?.forEach((layerScope) => {
576
+ const channelScope = segmentationChannelScopesByLayer?.[layerScope]?.[0];
577
+ result[layerScope] = {
578
+ colors: segmentationColorMapping?.[layerScope]?.[channelScope] ?? {},
579
+ opacity: segmentationColorMapping?.[layerScope]?.opacity ?? 1.0,
580
+ };
581
+ });
582
+ return result;
583
+ }, [segmentationColorMapping, segmentationLayerScopes, segmentationChannelScopesByLayer]);
551
584
 
552
585
  // TODO: try to simplify using useMemoCustomComparison?
553
586
  // This would allow us to refactor a lot of the checking-for-changes logic into a comparison function,
@@ -560,9 +593,6 @@ export function NeuroglancerSubscriber(props) {
560
593
  return current;
561
594
  }
562
595
 
563
- const nextSegments = Object.keys(cellColorMapping);
564
- const prevLayer = current?.layers?.[0] || {};
565
- const prevSegments = prevLayer.segments || [];
566
596
  const { projectionScale, projectionOrientation, position } = current;
567
597
 
568
598
  // Did Vitessce coords change vs the *previous* render?
@@ -699,20 +729,24 @@ export function NeuroglancerSubscriber(props) {
699
729
  lastInteractionSource.current = null;
700
730
  }
701
731
 
702
- const newLayer0 = {
703
- ...prevLayer,
704
- segments: nextSegments,
705
- segmentColors: cellColorMapping,
706
- };
707
-
732
+ const updatedLayers = current?.layers?.map((layer, idx) => {
733
+ const layerScope = segmentationLayerScopes?.[idx];
734
+ const layerColorMapping = cellColorMappingByLayer?.[layerScope]?.colors ?? {};
735
+ const layerSegments = Object.keys(layerColorMapping);
736
+ return {
737
+ ...layer,
738
+ segments: layerSegments,
739
+ segmentColors: layerColorMapping,
740
+ objectAlpha: cellColorMappingByLayer?.[layerScope]?.opacity ?? 1.0,
741
+ };
742
+ }) ?? [];
708
743
 
709
744
  const updated = {
710
745
  ...current,
711
746
  projectionScale: nextProjectionScale,
712
747
  projectionOrientation: nextOrientation,
713
748
  position: nextPosition,
714
- layers: prevSegments.length === 0 ? [newLayer0, ...(current?.layers?.slice(1)
715
- || [])] : current?.layers,
749
+ layers: updatedLayers,
716
750
  };
717
751
 
718
752
  latestViewerStateRef.current = updated;
@@ -728,7 +762,7 @@ export function NeuroglancerSubscriber(props) {
728
762
  };
729
763
 
730
764
  return updated;
731
- }, [cellColorMapping, spatialZoom, spatialRotationX, spatialRotationY,
765
+ }, [cellColorMappingByLayer, spatialZoom, spatialRotationX, spatialRotationY,
732
766
  spatialRotationZ, spatialTargetX, spatialTargetY, initalViewerState,
733
767
  latestViewerStateIteration]);
734
768
 
@@ -736,6 +770,10 @@ export function NeuroglancerSubscriber(props) {
736
770
  setCellHighlight(String(obsId));
737
771
  }, [setCellHighlight]);
738
772
 
773
+ const handleLayerLoadingChange = useCallback((isLoaded) => {
774
+ setIsLayersLoaded(isLoaded);
775
+ }, []);
776
+
739
777
  // TODO: if all cells are deselected, a black view is shown, rather we want to show empty NG view?
740
778
  // if (!cellColorMapping || Object.keys(cellColorMapping).length === 0) {
741
779
  // return;
@@ -745,6 +783,7 @@ export function NeuroglancerSubscriber(props) {
745
783
  // console.log(derivedViewerState);
746
784
 
747
785
  return (
786
+
748
787
  <TitleInfo
749
788
  title={title}
750
789
  info={subtitle}
@@ -754,34 +793,43 @@ export function NeuroglancerSubscriber(props) {
754
793
  closeButtonVisible={closeButtonVisible}
755
794
  downloadButtonVisible={downloadButtonVisible}
756
795
  removeGridComponent={removeGridComponent}
757
- isReady={isReady}
796
+ isReady={isReady && isLayersLoaded}
758
797
  errors={errors}
759
798
  withPadding={false}
760
799
  guideUrl={GUIDE_URL}
761
800
  >
762
- <div style={{ position: 'relative', width: '100%', height: '100%' }} ref={containerRef}>
763
- <div style={{ position: 'absolute', top: 0, right: 0, zIndex: 50 }}>
764
- <MultiLegend
765
- theme="dark"
766
- maxHeight={ngHeight}
767
- segmentationLayerScopes={segmentationLayerScopes}
768
- segmentationLayerCoordination={segmentationLayerCoordination}
769
- segmentationChannelScopesByLayer={segmentationChannelScopesByLayer}
770
- segmentationChannelCoordination={segmentationChannelCoordination}
771
- />
772
- </div>
801
+ {hasLayers ? (
802
+ <div style={{ position: 'relative', width: '100%', height: '100%' }} ref={containerRef}>
803
+ <div style={{ position: 'absolute', top: 0, right: 0, zIndex: 50 }}>
804
+ <MultiLegend
805
+ theme="dark"
806
+ maxHeight={ngHeight}
807
+
808
+ // Segmentations
809
+ segmentationLayerScopes={segmentationLayerScopes}
810
+ segmentationLayerCoordination={segmentationLayerCoordination}
811
+ segmentationChannelScopesByLayer={segmentationChannelScopesByLayer}
812
+ segmentationChannelCoordination={segmentationChannelCoordination}
813
+
814
+ // Points
815
+ pointLayerScopes={pointLayerScopes}
816
+ pointLayerCoordination={pointLayerCoordination}
817
+ pointMultiIndicesData={pointMultiIndicesData}
818
+ />
819
+ </div>
773
820
 
774
- {hasLayers ? (
775
821
  <NeuroglancerComp
776
822
  classes={classes}
777
823
  onSegmentClick={onSegmentClick}
778
824
  onSelectHoveredCoords={onSegmentHighlight}
779
825
  viewerState={derivedViewerState}
780
- cellColorMapping={cellColorMapping}
826
+ cellColorMapping={cellColorMappingByLayer}
781
827
  setViewerState={handleStateUpdate}
828
+ onLayerLoadingChange={handleLayerLoadingChange}
782
829
  />
783
- ) : null}
784
- </div>
830
+ </div>
831
+ ) : null}
785
832
  </TitleInfo>
833
+
786
834
  );
787
835
  }
@@ -67,6 +67,10 @@ let viewerNoKey;
67
67
  * @property {() => void} onSelectionDetailsStateChanged
68
68
  * A function of the form `() => {}` to respond to selection changes in the viewer.
69
69
  * @property {() => void} onViewerStateChanged
70
+ * @property {(isLoaded: boolean) => void} onLayerLoadingChange
71
+ * A function of the form `(isLoaded) => {}`, called when layer loading state changes.
72
+ * The `isLoaded` argument will be `true` when all segmentation layers have finished loading
73
+ * their data sources, or `false` when layers are still loading.
70
74
  *
71
75
  * @property {Array<Object>} callbacks
72
76
  * // ngServer: string,
@@ -411,6 +415,7 @@ export default class Neuroglancer extends React.Component {
411
415
  onVisibleChanged: null,
412
416
  onSelectionDetailsStateChanged: null,
413
417
  onViewerStateChanged: null,
418
+ onLayerLoadingChange: null,
414
419
  key: null,
415
420
  callbacks: [],
416
421
  ngServer: 'https://neuroglancer-demo.appspot.com/',
@@ -427,7 +432,7 @@ export default class Neuroglancer extends React.Component {
427
432
  this.disposers = [];
428
433
  this.prevColorOverrides = new Set();
429
434
  this.overrideColorsById = Object.create(null);
430
- this.allKnownIds = new Set();
435
+ this.allKnownIdsByLayer = {};
431
436
  }
432
437
 
433
438
  minimalPoseSnapshot = () => {
@@ -476,30 +481,36 @@ export default class Neuroglancer extends React.Component {
476
481
  };
477
482
 
478
483
  /* To add colors to the segments, turning unselected to grey */
479
- applyColorsAndVisibility = (cellColorMapping) => {
484
+ applyColorsAndVisibility = (cellColorMappingByLayer) => {
480
485
  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)
486
+ // Build full color table per layer
497
487
  const baseLayers = (this.props.viewerState?.layers)
498
488
  ?? (this.viewer.state.toJSON().layers || []);
499
489
 
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') {
490
+ const newLayers = baseLayers.map((layer) => {
491
+ // Match layerScope by checking if the NG layer name contains the scope key.
492
+ // NG layer names are of the form:
493
+ // "obsSegmentations-init_A_obsSegmentations_0-init_A_obsSegmentations_0"
494
+ const layerScope = Object.keys(cellColorMappingByLayer).find(scope => layer.name?.includes(scope));
495
+
496
+ const selected = { ...(cellColorMappingByLayer[layerScope]?.colors || {}) };
497
+
498
+ // Track all known IDs for this layer scope
499
+ if (!this.allKnownIdsByLayer) this.allKnownIdsByLayer = {};
500
+ if (!this.allKnownIdsByLayer[layerScope]) {
501
+ this.allKnownIdsByLayer[layerScope] = new Set();
502
+ }
503
+ for (const id of Object.keys(selected)) {
504
+ this.allKnownIdsByLayer[layerScope].add(id);
505
+ }
506
+
507
+ // Build a full color table: selected keep their hex, others grey
508
+ const fullSegmentColors = {};
509
+ for (const id of this.allKnownIdsByLayer[layerScope] || []) {
510
+ fullSegmentColors[id] = selected[id] || GREY_HEX;
511
+ }
512
+
513
+ if (layer.type === 'segmentation') {
503
514
  return { ...layer, segmentColors: fullSegmentColors };
504
515
  }
505
516
  return layer;
@@ -619,12 +630,46 @@ export default class Neuroglancer extends React.Component {
619
630
  viewerNoKey = this.viewer;
620
631
  }
621
632
 
622
- // TODO: This is purely for debugging and we need to remove it.
633
+ const { visibleChunksChanged } = this.viewer.chunkQueueManager;
634
+ let firstChunkLoaded = false;
635
+ this.disposers.push(visibleChunksChanged.add(() => {
636
+ if (!firstChunkLoaded) {
637
+ for (const layer of this.viewer.layerManager.managedLayers) {
638
+ if (layer.layer instanceof SegmentationUserLayer) {
639
+ const hasVisibleChunk = layer.layer.renderLayers?.some((rl) => {
640
+ const {
641
+ numVisibleChunksAvailable,
642
+ numVisibleChunksNeeded,
643
+ } = rl.layerChunkProgressInfo || {};
644
+ if (!numVisibleChunksNeeded || !numVisibleChunksAvailable) return false;
645
+ // Neuroglancer only shows chunks when a certain % is loaded.
646
+ // The 0.25 is from testing different values, can be reduced to 0.2 to shorten loader time
647
+ return (numVisibleChunksAvailable / numVisibleChunksNeeded) > 0.25;
648
+ });
649
+ if (hasVisibleChunk) {
650
+ firstChunkLoaded = true;
651
+ // Two frames to avoid flash while the following two happens
652
+ // Neuroglancer issues WebGL draw calls
653
+ requestAnimationFrame(() => {
654
+ // GPU has painted, pixels visible on screen
655
+ requestAnimationFrame(() => {
656
+ this.props.onLayerLoadingChange?.(true);
657
+ });
658
+ });
659
+ return;
660
+ }
661
+ }
662
+ }
663
+ }
664
+ }));
665
+ this.disposers.push(() => { firstChunkLoaded = false; });
666
+
667
+ // TODO: This is purely for debugging - exposes the NG viewer to be tested via console
623
668
  // window.viewer = this.viewer;
624
669
  }
625
670
 
626
671
  componentDidUpdate(prevProps, prevState) {
627
- const { viewerState, cellColorMapping } = this.props;
672
+ const { viewerState, cellColorMapping: cellColorMappingByLayer } = this.props;
628
673
  // The restoreState() call clears the 'selected' (hovered on) segment, which is needed
629
674
  // by Neuroglancer's code to toggle segment visibilty on a mouse click. To free the user
630
675
  // from having to move the mouse before clicking, save the selected segment and restore
@@ -661,6 +706,27 @@ export default class Neuroglancer extends React.Component {
661
706
  if (layer.layer instanceof SegmentationUserLayer) {
662
707
  const { segmentSelectionState } = layer.layer.displayState;
663
708
  segmentSelectionState.set(selectedSegments[layer.name]);
709
+ const layerScope = Object.keys(cellColorMappingByLayer).find(
710
+ scope => layer.name?.includes(scope),
711
+ );
712
+ if (layerScope) {
713
+ const opacity = cellColorMappingByLayer[layerScope]?.opacity ?? 1.0;
714
+ layer.layer.displayState.objectAlpha.value = opacity;
715
+ }
716
+ }
717
+ // Update annotation layer shaders from viewerState config,
718
+ // skipping update if shader is unchanged to avoid costly re-renders
719
+ if (layer.layer instanceof AnnotationUserLayer) {
720
+ const matchingLayer = (viewerState?.layers || []).find(
721
+ l => l.name === layer.name,
722
+ );
723
+ if (matchingLayer?.shader) {
724
+ /* eslint-disable-next-line no-underscore-dangle */
725
+ const currentShader = layer.layer.annotationDisplayState.shader.value_;
726
+ if (currentShader !== matchingLayer.shader) {
727
+ layer.layer.annotationDisplayState.shader.value = matchingLayer.shader;
728
+ }
729
+ }
664
730
  }
665
731
  }
666
732
 
@@ -702,8 +768,8 @@ export default class Neuroglancer extends React.Component {
702
768
  this.withoutEmitting(() => {
703
769
  const layers = Array.isArray(viewerState.layers) ? viewerState.layers : [];
704
770
  this.viewer.state.restoreState({ layers });
705
- if (cellColorMapping && Object.keys(cellColorMapping).length) {
706
- this.applyColorsAndVisibility(cellColorMapping);
771
+ if (cellColorMappingByLayer && Object.keys(cellColorMappingByLayer).length) {
772
+ this.applyColorsAndVisibility(cellColorMappingByLayer);
707
773
  }
708
774
  });
709
775
  }
@@ -711,13 +777,16 @@ export default class Neuroglancer extends React.Component {
711
777
  // If colors changed (but layers didn’t): re-apply colors
712
778
  // this was to avid NG randomly assigning colors to the segments by resetting them
713
779
  const prevSize = prevProps.cellColorMapping
714
- ? Object.keys(prevProps.cellColorMapping).length : 0;
715
- const currSize = cellColorMapping ? Object.keys(cellColorMapping).length : 0;
716
- const mappingRefChanged = prevProps.cellColorMapping !== cellColorMapping;
780
+ ? Object.values(prevProps.cellColorMapping)
781
+ .reduce((acc, v) => acc + Object.keys(v?.colors || {}).length, 0) : 0;
782
+ const currSize = cellColorMappingByLayer
783
+ ? Object.values(cellColorMappingByLayer)
784
+ .reduce((acc, v) => acc + Object.keys(v?.colors || {}).length, 0) : 0;
785
+ const mappingRefChanged = prevProps.cellColorMapping !== this.props.cellColorMapping;
717
786
  if (!this.didLayersChange(prevVS, viewerState)
718
787
  && (mappingRefChanged || prevSize !== currSize)) {
719
788
  this.withoutEmitting(() => {
720
- this.applyColorsAndVisibility(cellColorMapping);
789
+ this.applyColorsAndVisibility(cellColorMappingByLayer);
721
790
  });
722
791
  }
723
792
 
@@ -725,7 +794,7 @@ export default class Neuroglancer extends React.Component {
725
794
  // We only restore layers (not pose) when sources change OR on the first time segments appear.
726
795
  const stripSegFields = layers => (layers || []).map((l) => {
727
796
  if (!l) return l;
728
- const { segments, segmentColors, ...rest } = l;
797
+ const { segments, segmentColors, objectAlpha, ...rest } = l;
729
798
  return rest; // ignore segments + segmentColors for comparison
730
799
  });
731
800
 
@@ -106,6 +106,7 @@ export function toNgLayerName(dataType, layerScope, channelScope = null) {
106
106
  */
107
107
  export function useNeuroglancerViewerState(
108
108
  theme,
109
+ showAxisLines,
109
110
  segmentationLayerScopes,
110
111
  segmentationChannelScopesByLayer,
111
112
  segmentationLayerCoordination,
@@ -140,20 +141,35 @@ export function useNeuroglancerViewerState(
140
141
  const {
141
142
  spatialChannelVisible,
142
143
  } = channelCoordination || {};
144
+ const { source: ngSource, ...otherNgOptions } = layerData.neuroglancerOptions ?? {};
145
+
146
+ // Build source: if neuroglancerOptions has subsources
147
+ const hasNgSourceOptions = layerData.neuroglancerOptions?.subsources
148
+ || layerData.neuroglancerOptions?.enableDefaultSubsources !== undefined;
149
+
150
+ const source = hasNgSourceOptions
151
+ ? {
152
+ url: toPrecomputedSource(layerUrl),
153
+ subsources: layerData.neuroglancerOptions.subsources,
154
+ enableDefaultSubsources: layerData.neuroglancerOptions.enableDefaultSubsources
155
+ ?? false,
156
+ }
157
+ : toPrecomputedSource(layerUrl);
158
+
143
159
  result = {
144
160
  ...result,
145
161
  layers: [
146
162
  ...result.layers,
147
163
  {
148
164
  type: 'segmentation',
149
- source: toPrecomputedSource(layerUrl),
165
+ source,
150
166
  segments: [],
151
167
  name: toNgLayerName(DataType.OBS_SEGMENTATIONS, layerScope, channelScope),
152
168
  visible: spatialLayerVisible && spatialChannelVisible, // Both layer and channel
153
169
  // visibility must be true for the layer to be visible.
154
170
  // TODO: update this to extract specific properties from
155
171
  // neuroglancerOptions as needed.
156
- ...(layerData.neuroglancerOptions ?? {}),
172
+ ...otherNgOptions,
157
173
  },
158
174
  ],
159
175
  };
@@ -180,6 +196,7 @@ export function useNeuroglancerViewerState(
180
196
  featureSelection,
181
197
  featureFilterMode,
182
198
  featureColor,
199
+ spatialPointStrokeWidth,
183
200
  } = layerCoordination || {};
184
201
 
185
202
  // Dynamically construct the shader based on the color encoding
@@ -196,6 +213,7 @@ export function useNeuroglancerViewerState(
196
213
 
197
214
  featureIndexProp: layerData.neuroglancerOptions?.featureIndexProp,
198
215
  pointIndexProp: layerData.neuroglancerOptions?.pointIndexProp,
216
+ pointMarkerBorderWidth: spatialPointStrokeWidth ?? 0.0,
199
217
  });
200
218
 
201
219
  result = {
@@ -235,6 +253,7 @@ export function useNeuroglancerViewerState(
235
253
  return result;
236
254
  }, {
237
255
  theme,
256
+ showAxisLines,
238
257
  segmentationLayerScopes,
239
258
  segmentationChannelScopesByLayer,
240
259
  segmentationLayerCoordination,