@vitessce/neuroglancer 3.6.0 → 3.6.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.
@@ -1,4 +1,4 @@
1
- import { g as getDefaultExportFromCjs, c as commonjsGlobal, a as getAugmentedNamespace, r as requirePropTypes } from "./index-DmHg0YSX.js";
1
+ import { g as getDefaultExportFromCjs, c as commonjsGlobal, a as getAugmentedNamespace, r as requirePropTypes } from "./index-C1Dm5JHD.js";
2
2
  import React__default from "react";
3
3
  function _mergeNamespaces(n, m) {
4
4
  for (var i = 0; i < m.length; i++) {
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { N } from "./index-DmHg0YSX.js";
1
+ import { N } from "./index-C1Dm5JHD.js";
2
2
  export {
3
3
  N as NeuroglancerSubscriber
4
4
  };
@@ -1,2 +1,15 @@
1
- export function Neuroglancer(props: any): JSX.Element;
1
+ export class Neuroglancer {
2
+ constructor(props: any);
3
+ bundleRoot: any;
4
+ viewerState: any;
5
+ justReceivedExternalUpdate: boolean;
6
+ prevElement: any;
7
+ prevClickHandler: ((event: any) => void) | null;
8
+ prevMouseStateChanged: any;
9
+ prevHoverHandler: (() => void) | null;
10
+ onViewerStateChanged(nextState: any): void;
11
+ onRef(viewerRef: any): void;
12
+ UNSAFE_componentWillUpdate(prevProps: any): void;
13
+ render(): JSX.Element;
14
+ }
2
15
  //# sourceMappingURL=Neuroglancer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Neuroglancer.d.ts","sourceRoot":"","sources":["../src/Neuroglancer.js"],"names":[],"mappings":"AAiBA,sDA+BC"}
1
+ {"version":3,"file":"Neuroglancer.d.ts","sourceRoot":"","sources":["../src/Neuroglancer.js"],"names":[],"mappings":"AAkEA;IACE,wBAeC;IAZC,gBAAgC;IAEhC,iBAAoC;IACpC,oCAAuC;IAEvC,iBAAuB;IACvB,gDAA4B;IAC5B,2BAAiC;IACjC,sCAA4B;IAkD9B,2CASC;IArDD,4BA0CC;IAaD,iDAQC;IAED,sBAqBC;CACF"}
@@ -1,28 +1,132 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useCallback, useMemo, Suspense } from 'react';
2
+ /* eslint-disable react-refresh/only-export-components */
3
+ import React, { PureComponent, Suspense } from 'react';
3
4
  import { ChunkWorker } from '@vitessce/neuroglancer-workers';
4
- import { useStyles, NeuroglancerGlobalStyles } from './styles.js';
5
- // We lazy load the Neuroglancer component,
6
- // because the non-dynamic import causes problems for Vitest,
7
- // as the package appears contain be a mix of CommonJS and ESM syntax.
5
+ import { isEqualWith, pick } from 'lodash-es';
6
+ import { NeuroglancerGlobalStyles } from './styles.js';
8
7
  const LazyReactNeuroglancer = React.lazy(async () => {
9
8
  const ReactNeuroglancer = await import('@janelia-flyem/react-neuroglancer');
10
9
  return ReactNeuroglancer;
11
10
  });
12
- // Reference: https://github.com/developit/jsdom-worker/issues/14#issuecomment-1268070123
13
11
  function createWorker() {
14
12
  return new ChunkWorker();
15
13
  }
16
- export function Neuroglancer(props) {
17
- const { viewerState, onViewerStateChanged, } = props;
18
- const { classes } = useStyles();
19
- const bundleRoot = useMemo(() => createWorker(), []);
20
- const handleStateChanged = useCallback((newState) => {
21
- if (JSON.stringify(newState) !== JSON.stringify(viewerState)) {
22
- if (onViewerStateChanged) {
23
- onViewerStateChanged(newState);
14
+ /**
15
+ * Is this a valid viewerState object?
16
+ * @param {object} viewerState
17
+ * @returns {boolean}
18
+ */
19
+ function isValidState(viewerState) {
20
+ const { projectionScale, projectionOrientation, position, dimensions } = viewerState || {};
21
+ return (dimensions !== undefined
22
+ && typeof projectionScale === 'number'
23
+ && Array.isArray(projectionOrientation)
24
+ && projectionOrientation.length === 4
25
+ && Array.isArray(position)
26
+ && position.length === 3);
27
+ }
28
+ // TODO: Do we want to use the same epsilon value
29
+ // for every viewstate property being compared?
30
+ const EPSILON = 1e-7;
31
+ const VIEWSTATE_KEYS = ['projectionScale', 'projectionOrientation', 'position'];
32
+ // Custom numeric comparison function
33
+ // for isEqualWith, to be able to set a custom epsilon.
34
+ function customizer(a, b) {
35
+ if (typeof a === 'number' && typeof b === 'number') {
36
+ // Returns true if the values are equivalent, else false.
37
+ return Math.abs(a - b) > EPSILON;
38
+ }
39
+ // Return undefined to fallback to the default
40
+ // comparison function.
41
+ return undefined;
42
+ }
43
+ /**
44
+ * Returns true if the two states are equal, or false if not.
45
+ * @param {object} prevState Previous viewer state.
46
+ * @param {object} nextState Next viewer state.
47
+ * @returns
48
+ */
49
+ function compareViewerState(prevState, nextState) {
50
+ if (isValidState(nextState)) {
51
+ // Subset the viewerState objects to only the keys
52
+ // that we want to use for comparison.
53
+ const prevSubset = pick(prevState, VIEWSTATE_KEYS);
54
+ const nextSubset = pick(nextState, VIEWSTATE_KEYS);
55
+ return isEqualWith(prevSubset, nextSubset, customizer);
56
+ }
57
+ return true;
58
+ }
59
+ export class Neuroglancer extends PureComponent {
60
+ constructor(props) {
61
+ super(props);
62
+ this.bundleRoot = createWorker();
63
+ this.viewerState = props.viewerState;
64
+ this.justReceivedExternalUpdate = false;
65
+ this.prevElement = null;
66
+ this.prevClickHandler = null;
67
+ this.prevMouseStateChanged = null;
68
+ this.prevHoverHandler = null;
69
+ this.onViewerStateChanged = this.onViewerStateChanged.bind(this);
70
+ this.onRef = this.onRef.bind(this);
71
+ }
72
+ onRef(viewerRef) {
73
+ // Here, we have access to the viewerRef.viewer object,
74
+ // which we can use to add/remove event handlers.
75
+ const { onSegmentClick, onSelectHoveredCoords, } = this.props;
76
+ if (viewerRef) {
77
+ // Mount
78
+ const { viewer } = viewerRef;
79
+ this.prevElement = viewer.element;
80
+ this.prevMouseStateChanged = viewer.mouseState.changed;
81
+ this.prevClickHandler = (event) => {
82
+ if (event.button === 0) {
83
+ setTimeout(() => {
84
+ const { pickedValue } = viewer.mouseState;
85
+ if (pickedValue && pickedValue?.low) {
86
+ onSegmentClick(pickedValue?.low);
87
+ }
88
+ }, 100);
89
+ }
90
+ };
91
+ viewer.element.addEventListener('mousedown', this.prevClickHandler);
92
+ this.prevHoverHandler = () => {
93
+ if (viewer.mouseState.pickedValue !== undefined) {
94
+ const pickedSegment = viewer.mouseState.pickedValue;
95
+ onSelectHoveredCoords(pickedSegment?.low);
96
+ }
97
+ };
98
+ viewer.mouseState.changed.add(this.prevHoverHandler);
99
+ }
100
+ else {
101
+ // Unmount (viewerRef is null)
102
+ if (this.prevElement && this.prevClickHandler) {
103
+ this.prevElement.removeEventListener('mousedown', this.prevClickHandler);
24
104
  }
105
+ if (this.prevMouseStateChanged && this.prevHoverHandler) {
106
+ this.prevMouseStateChanged.remove(this.prevHoverHandler);
107
+ }
108
+ }
109
+ }
110
+ onViewerStateChanged(nextState) {
111
+ const { setViewerState } = this.props;
112
+ const { viewerState: prevState } = this;
113
+ if (!this.justReceivedExternalUpdate && !compareViewerState(prevState, nextState)) {
114
+ this.viewerState = nextState;
115
+ this.justReceivedExternalUpdate = false;
116
+ setViewerState(nextState);
117
+ }
118
+ }
119
+ UNSAFE_componentWillUpdate(prevProps) {
120
+ if (!compareViewerState(this.viewerState, prevProps.viewerState)) {
121
+ this.viewerState = prevProps.viewerState;
122
+ this.justReceivedExternalUpdate = true;
123
+ setTimeout(() => {
124
+ this.justReceivedExternalUpdate = false;
125
+ }, 100);
25
126
  }
26
- }, [onViewerStateChanged, viewerState]);
27
- 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: handleStateChanged, bundleRoot: bundleRoot }) }) })] }));
127
+ }
128
+ render() {
129
+ const { classes, } = this.props;
130
+ 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: this.viewerState, onViewerStateChanged: this.onViewerStateChanged, bundleRoot: this.bundleRoot, ref: this.onRef }) }) })] }));
131
+ }
28
132
  }
@@ -1 +1 @@
1
- {"version":3,"file":"NeuroglancerSubscriber.d.ts","sourceRoot":"","sources":["../src/NeuroglancerSubscriber.js"],"names":[],"mappings":"AAOA,gEA0BC"}
1
+ {"version":3,"file":"NeuroglancerSubscriber.d.ts","sourceRoot":"","sources":["../src/NeuroglancerSubscriber.js"],"names":[],"mappings":"AAgEA,gEAqKC"}
@@ -1,9 +1,124 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import React from 'react';
3
- import { TitleInfo, } from '@vitessce/vit-s';
4
- import { ViewHelpMapping } from '@vitessce/constants-internal';
2
+ /* eslint-disable no-unused-vars */
3
+ import React, { useCallback, useMemo } from 'react';
4
+ import { TitleInfo, useCoordination, useObsSetsData, useLoaders, useObsEmbeddingData, } from '@vitessce/vit-s';
5
+ import { ViewHelpMapping, ViewType, COMPONENT_COORDINATION_TYPES, } from '@vitessce/constants-internal';
6
+ import { mergeObsSets, getCellColors, setObsSelection } from '@vitessce/sets-utils';
5
7
  import { Neuroglancer } from './Neuroglancer.js';
8
+ import { useStyles } from './styles.js';
9
+ const NEUROGLANCER_ZOOM_BASIS = 16;
10
+ function mapVitessceToNeuroglancer(zoom) {
11
+ return NEUROGLANCER_ZOOM_BASIS * (2 ** -zoom);
12
+ }
13
+ function mapNeuroglancerToVitessce(projectionScale) {
14
+ return -Math.log2(projectionScale / NEUROGLANCER_ZOOM_BASIS);
15
+ }
16
+ function quaternionToEuler([x, y, z, w]) {
17
+ // X-axis rotation (Roll)
18
+ const thetaX = Math.atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y));
19
+ // Y-axis rotation (Pitch)
20
+ const sinp = 2 * (w * y - z * x);
21
+ const thetaY = Math.abs(sinp) >= 1 ? Math.sign(sinp) * (Math.PI / 2) : Math.asin(sinp);
22
+ // Convert to degrees as Vitessce expects degrees?
23
+ return [thetaX * (180 / Math.PI), thetaY * (180 / Math.PI)];
24
+ }
25
+ function eulerToQuaternion(thetaX, thetaY) {
26
+ // Convert Euler angles (X, Y rotations) to quaternion
27
+ const halfThetaX = thetaX / 2;
28
+ const halfThetaY = thetaY / 2;
29
+ const sinX = Math.sin(halfThetaX);
30
+ const cosX = Math.cos(halfThetaX);
31
+ const sinY = Math.sin(halfThetaY);
32
+ const cosY = Math.cos(halfThetaY);
33
+ return [
34
+ sinX * cosY,
35
+ cosX * sinY,
36
+ sinX * sinY,
37
+ cosX * cosY,
38
+ ];
39
+ }
40
+ function normalizeQuaternion(q) {
41
+ const length = Math.sqrt((q[0] ** 2) + (q[1] ** 2) + (q[2] ** 2) + (q[3] ** 2));
42
+ return q.map(value => value / length);
43
+ }
6
44
  export function NeuroglancerSubscriber(props) {
7
- const { closeButtonVisible, downloadButtonVisible, removeGridComponent, theme, title = 'Neuroglancer', viewerState: viewerStateInitial = null, helpText = ViewHelpMapping.NEUROGLANCER, } = props;
8
- return (_jsx(TitleInfo, { title: title, helpText: helpText, isSpatial: true, theme: theme, closeButtonVisible: closeButtonVisible, downloadButtonVisible: downloadButtonVisible, removeGridComponent: removeGridComponent, isReady: true, children: viewerStateInitial && _jsx(Neuroglancer, { viewerState: viewerStateInitial }) }));
45
+ const { coordinationScopes, closeButtonVisible, downloadButtonVisible, removeGridComponent, theme, title = 'Neuroglancer', helpText = ViewHelpMapping.NEUROGLANCER, viewerState: initialViewerState, } = props;
46
+ const [{ dataset, obsType, spatialZoom, spatialTargetX, spatialTargetY, spatialRotationX, spatialRotationY,
47
+ // spatialRotationZ,
48
+ // spatialRotationOrbit,
49
+ // spatialOrbitAxis,
50
+ embeddingType: mapping, obsSetSelection: cellSetSelection, additionalObsSets: additionalCellSets, obsSetColor: cellSetColor, }, { setAdditionalObsSets: setAdditionalCellSets, setObsSetColor: setCellSetColor, setObsColorEncoding: setCellColorEncoding, setObsSetSelection: setCellSetSelection, setObsHighlight: setCellHighlight, setSpatialTargetX: setTargetX, setSpatialTargetY: setTargetY, setSpatialRotationX: setRotationX, setSpatialRotationY: setRotationY,
51
+ // setSpatialRotationZ: setRotationZ,
52
+ // setSpatialRotationOrbit: setRotationOrbit,
53
+ setSpatialZoom: setZoom, }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.NEUROGLANCER], coordinationScopes);
54
+ const { classes } = useStyles();
55
+ const loaders = useLoaders();
56
+ const [{ obsSets: cellSets }] = useObsSetsData(loaders, dataset, false, { setObsSetSelection: setCellSetSelection, setObsSetColor: setCellSetColor }, { cellSetSelection, obsSetColor: cellSetColor }, { obsType });
57
+ const [{ obsIndex }] = useObsEmbeddingData(loaders, dataset, true, {}, {}, { obsType, embeddingType: mapping });
58
+ const handleStateUpdate = useCallback((newState) => {
59
+ const { projectionScale, projectionOrientation, position } = newState;
60
+ setZoom(mapNeuroglancerToVitessce(projectionScale));
61
+ const vitessceEularMapping = quaternionToEuler(projectionOrientation);
62
+ // TODO: support z rotation on SpatialView?
63
+ setRotationX(vitessceEularMapping[0]);
64
+ setRotationY(vitessceEularMapping[1]);
65
+ // Note: To pan in Neuroglancer, use shift+leftKey+drag
66
+ setTargetX(position[0]);
67
+ setTargetY(position[1]);
68
+ }, [setZoom, setTargetX, setTargetY, setRotationX, setRotationY]);
69
+ const onSegmentClick = useCallback((value) => {
70
+ if (value) {
71
+ const selectedCellIds = [String(value)];
72
+ setObsSelection(selectedCellIds, additionalCellSets, cellSetColor, setCellSetSelection, setAdditionalCellSets, setCellSetColor, setCellColorEncoding, 'Selection ', `: based on selected segments ${value}`);
73
+ }
74
+ }, [additionalCellSets, cellSetColor, setAdditionalCellSets,
75
+ setCellColorEncoding, setCellSetColor, setCellSetSelection,
76
+ ]);
77
+ const mergedCellSets = useMemo(() => mergeObsSets(cellSets, additionalCellSets), [cellSets, additionalCellSets]);
78
+ const cellColors = useMemo(() => getCellColors({
79
+ cellSets: mergedCellSets,
80
+ cellSetSelection,
81
+ cellSetColor,
82
+ obsIndex,
83
+ theme,
84
+ }), [mergedCellSets, theme,
85
+ cellSetColor, cellSetSelection, obsIndex]);
86
+ const rgbToHex = useCallback(rgb => (typeof rgb === 'string' ? rgb
87
+ : `#${rgb.map(c => c.toString(16).padStart(2, '0')).join('')}`), []);
88
+ const cellColorMapping = useMemo(() => {
89
+ const colorCellMapping = {};
90
+ cellColors.forEach((color, cell) => {
91
+ colorCellMapping[cell] = rgbToHex(color);
92
+ });
93
+ return colorCellMapping;
94
+ }, [cellColors, rgbToHex]);
95
+ const derivedViewerState = useMemo(() => ({
96
+ ...initialViewerState,
97
+ layers: initialViewerState.layers.map((layer, index) => (index === 0
98
+ ? {
99
+ ...layer,
100
+ segments: Object.keys(cellColorMapping).map(String),
101
+ segmentColors: cellColorMapping,
102
+ }
103
+ : layer)),
104
+ }), [cellColorMapping, initialViewerState]);
105
+ const derivedViewerState2 = useMemo(() => {
106
+ if (typeof spatialZoom === 'number' && typeof spatialTargetX === 'number') {
107
+ const projectionScale = mapVitessceToNeuroglancer(spatialZoom);
108
+ const position = [spatialTargetX, spatialTargetY, derivedViewerState.position[2]];
109
+ const projectionOrientation = normalizeQuaternion(eulerToQuaternion(spatialRotationX, spatialRotationY));
110
+ return {
111
+ ...derivedViewerState,
112
+ projectionScale,
113
+ position,
114
+ projectionOrientation,
115
+ };
116
+ }
117
+ return derivedViewerState;
118
+ }, [derivedViewerState, spatialZoom, spatialTargetX,
119
+ spatialTargetY, spatialRotationX, spatialRotationY]);
120
+ const onSegmentHighlight = useCallback((obsId) => {
121
+ setCellHighlight(String(obsId));
122
+ }, [obsIndex, setCellHighlight]);
123
+ return (_jsx(TitleInfo, { title: title, helpText: helpText, isSpatial: true, theme: theme, closeButtonVisible: closeButtonVisible, downloadButtonVisible: downloadButtonVisible, removeGridComponent: removeGridComponent, isReady: true, withPadding: false, children: _jsx(Neuroglancer, { classes: classes, onSegmentClick: onSegmentClick, onSelectHoveredCoords: onSegmentHighlight, viewerState: derivedViewerState2, setViewerState: handleStateUpdate }) }));
9
124
  }
@@ -1112,7 +1112,7 @@ const globalNeuroglancerStyles = {
1112
1112
  overflowY: 'scroll',
1113
1113
  wordWrap: 'break-word',
1114
1114
  },
1115
- '.neuroglancer-multiline-autocomplete-completion:nth-child(even):not(.neuroglancer-multiline-autocomplete-completion-active)': {
1115
+ '.neuroglancer-multiline-autocomplete-completion:nth-of-type(even):not(.neuroglancer-multiline-autocomplete-completion-active)': {
1116
1116
  backgroundColor: '#2b2b2b',
1117
1117
  },
1118
1118
  '.neuroglancer-multiline-autocomplete-completion:hover': {
@@ -1414,7 +1414,7 @@ const globalNeuroglancerStyles = {
1414
1414
  backgroundClip: 'border-box',
1415
1415
  backgroundColor: '#8080ff80',
1416
1416
  },
1417
- '.neuroglancer-stack-layout-drop-placeholder:first-child, .neuroglancer-stack-layout-drop-placeholder:last-child': {
1417
+ '.neuroglancer-stack-layout-drop-placeholder:first-of-type, .neuroglancer-stack-layout-drop-placeholder:last-of-type': {
1418
1418
  display: 'none',
1419
1419
  },
1420
1420
  '.neuroglancer-panel': { flex: 1 },
@@ -1560,7 +1560,7 @@ const globalNeuroglancerStyles = {
1560
1560
  alignItems: 'center',
1561
1561
  },
1562
1562
  '.neuroglancer-position-widget input:disabled': { pointerEvents: 'none' },
1563
- '.neuroglancer-position-widget .neuroglancer-copy-button:first-child': {
1563
+ '.neuroglancer-position-widget .neuroglancer-copy-button:first-of-type': {
1564
1564
  display: 'none',
1565
1565
  },
1566
1566
  '.neuroglancer-position-dimension-coordinate, .neuroglancer-position-dimension-name, .neuroglancer-position-dimension-scale': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitessce/neuroglancer",
3
- "version": "3.6.0",
3
+ "version": "3.6.1",
4
4
  "author": "Gehlenborg Lab",
5
5
  "homepage": "http://vitessce.io",
6
6
  "repository": {
@@ -17,10 +17,14 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "@janelia-flyem/react-neuroglancer": "^2.5.0",
20
- "@vitessce/neuroglancer-workers": "3.6.0",
21
- "@vitessce/styles": "3.6.0",
22
- "@vitessce/constants-internal": "3.6.0",
23
- "@vitessce/vit-s": "3.6.0"
20
+ "lodash-es": "^4.17.21",
21
+ "@vitessce/neuroglancer-workers": "3.6.1",
22
+ "@vitessce/styles": "3.6.1",
23
+ "@vitessce/constants-internal": "3.6.1",
24
+ "@vitessce/vit-s": "3.6.1",
25
+ "@vitessce/sets-utils": "3.6.1",
26
+ "@vitessce/utils": "3.6.1",
27
+ "@vitessce/tooltip": "3.6.1"
24
28
  },
25
29
  "devDependencies": {
26
30
  "@testing-library/jest-dom": "^6.6.3",
@@ -1,49 +1,172 @@
1
- import React, { useCallback, useMemo, Suspense } from 'react';
1
+ /* eslint-disable react-refresh/only-export-components */
2
+ import React, { PureComponent, Suspense } from 'react';
2
3
  import { ChunkWorker } from '@vitessce/neuroglancer-workers';
3
- import { useStyles, NeuroglancerGlobalStyles } from './styles.js';
4
+ import { isEqualWith, pick } from 'lodash-es';
5
+ import { NeuroglancerGlobalStyles } from './styles.js';
4
6
 
5
- // We lazy load the Neuroglancer component,
6
- // because the non-dynamic import causes problems for Vitest,
7
- // as the package appears contain be a mix of CommonJS and ESM syntax.
8
7
  const LazyReactNeuroglancer = React.lazy(async () => {
9
8
  const ReactNeuroglancer = await import('@janelia-flyem/react-neuroglancer');
10
9
  return ReactNeuroglancer;
11
10
  });
12
11
 
13
- // Reference: https://github.com/developit/jsdom-worker/issues/14#issuecomment-1268070123
14
12
  function createWorker() {
15
13
  return new ChunkWorker();
16
14
  }
17
15
 
18
- export function Neuroglancer(props) {
19
- const {
20
- viewerState,
21
- onViewerStateChanged,
22
- } = props;
23
- const { classes } = useStyles();
24
- const bundleRoot = useMemo(() => createWorker(), []);
25
-
26
- const handleStateChanged = useCallback((newState) => {
27
- if (JSON.stringify(newState) !== JSON.stringify(viewerState)) {
28
- if (onViewerStateChanged) {
29
- onViewerStateChanged(newState);
16
+ /**
17
+ * Is this a valid viewerState object?
18
+ * @param {object} viewerState
19
+ * @returns {boolean}
20
+ */
21
+ function isValidState(viewerState) {
22
+ const { projectionScale, projectionOrientation, position, dimensions } = viewerState || {};
23
+ return (
24
+ dimensions !== undefined
25
+ && typeof projectionScale === 'number'
26
+ && Array.isArray(projectionOrientation)
27
+ && projectionOrientation.length === 4
28
+ && Array.isArray(position)
29
+ && position.length === 3
30
+ );
31
+ }
32
+
33
+ // TODO: Do we want to use the same epsilon value
34
+ // for every viewstate property being compared?
35
+ const EPSILON = 1e-7;
36
+ const VIEWSTATE_KEYS = ['projectionScale', 'projectionOrientation', 'position'];
37
+
38
+ // Custom numeric comparison function
39
+ // for isEqualWith, to be able to set a custom epsilon.
40
+ function customizer(a, b) {
41
+ if (typeof a === 'number' && typeof b === 'number') {
42
+ // Returns true if the values are equivalent, else false.
43
+ return Math.abs(a - b) > EPSILON;
44
+ }
45
+ // Return undefined to fallback to the default
46
+ // comparison function.
47
+ return undefined;
48
+ }
49
+
50
+ /**
51
+ * Returns true if the two states are equal, or false if not.
52
+ * @param {object} prevState Previous viewer state.
53
+ * @param {object} nextState Next viewer state.
54
+ * @returns
55
+ */
56
+ function compareViewerState(prevState, nextState) {
57
+ if (isValidState(nextState)) {
58
+ // Subset the viewerState objects to only the keys
59
+ // that we want to use for comparison.
60
+ const prevSubset = pick(prevState, VIEWSTATE_KEYS);
61
+ const nextSubset = pick(nextState, VIEWSTATE_KEYS);
62
+ return isEqualWith(prevSubset, nextSubset, customizer);
63
+ }
64
+ return true;
65
+ }
66
+
67
+ export class Neuroglancer extends PureComponent {
68
+ constructor(props) {
69
+ super(props);
70
+
71
+ this.bundleRoot = createWorker();
72
+
73
+ this.viewerState = props.viewerState;
74
+ this.justReceivedExternalUpdate = false;
75
+
76
+ this.prevElement = null;
77
+ this.prevClickHandler = null;
78
+ this.prevMouseStateChanged = null;
79
+ this.prevHoverHandler = null;
80
+
81
+ this.onViewerStateChanged = this.onViewerStateChanged.bind(this);
82
+ this.onRef = this.onRef.bind(this);
83
+ }
84
+
85
+ onRef(viewerRef) {
86
+ // Here, we have access to the viewerRef.viewer object,
87
+ // which we can use to add/remove event handlers.
88
+ const {
89
+ onSegmentClick,
90
+ onSelectHoveredCoords,
91
+ } = this.props;
92
+
93
+ if (viewerRef) {
94
+ // Mount
95
+ const { viewer } = viewerRef;
96
+ this.prevElement = viewer.element;
97
+ this.prevMouseStateChanged = viewer.mouseState.changed;
98
+ this.prevClickHandler = (event) => {
99
+ if (event.button === 0) {
100
+ setTimeout(() => {
101
+ const { pickedValue } = viewer.mouseState;
102
+ if (pickedValue && pickedValue?.low) {
103
+ onSegmentClick(pickedValue?.low);
104
+ }
105
+ }, 100);
106
+ }
107
+ };
108
+ viewer.element.addEventListener('mousedown', this.prevClickHandler);
109
+
110
+ this.prevHoverHandler = () => {
111
+ if (viewer.mouseState.pickedValue !== undefined) {
112
+ const pickedSegment = viewer.mouseState.pickedValue;
113
+ onSelectHoveredCoords(pickedSegment?.low);
114
+ }
115
+ };
116
+
117
+ viewer.mouseState.changed.add(this.prevHoverHandler);
118
+ } else {
119
+ // Unmount (viewerRef is null)
120
+ if (this.prevElement && this.prevClickHandler) {
121
+ this.prevElement.removeEventListener('mousedown', this.prevClickHandler);
122
+ }
123
+ if (this.prevMouseStateChanged && this.prevHoverHandler) {
124
+ this.prevMouseStateChanged.remove(this.prevHoverHandler);
30
125
  }
31
126
  }
32
- }, [onViewerStateChanged, viewerState]);
127
+ }
33
128
 
34
- return (
35
- <>
36
- <NeuroglancerGlobalStyles classes={classes} />
37
- <div className={classes.neuroglancerWrapper}>
38
- <Suspense fallback={<div>Loading...</div>}>
39
- <LazyReactNeuroglancer
40
- brainMapsClientId="NOT_A_VALID_ID"
41
- viewerState={viewerState}
42
- onViewerStateChanged={handleStateChanged}
43
- bundleRoot={bundleRoot}
44
- />
45
- </Suspense>
46
- </div>
47
- </>
48
- );
129
+ onViewerStateChanged(nextState) {
130
+ const { setViewerState } = this.props;
131
+ const { viewerState: prevState } = this;
132
+
133
+ if (!this.justReceivedExternalUpdate && !compareViewerState(prevState, nextState)) {
134
+ this.viewerState = nextState;
135
+ this.justReceivedExternalUpdate = false;
136
+ setViewerState(nextState);
137
+ }
138
+ }
139
+
140
+ UNSAFE_componentWillUpdate(prevProps) {
141
+ if (!compareViewerState(this.viewerState, prevProps.viewerState)) {
142
+ this.viewerState = prevProps.viewerState;
143
+ this.justReceivedExternalUpdate = true;
144
+ setTimeout(() => {
145
+ this.justReceivedExternalUpdate = false;
146
+ }, 100);
147
+ }
148
+ }
149
+
150
+ render() {
151
+ const {
152
+ classes,
153
+ } = this.props;
154
+
155
+ return (
156
+ <>
157
+ <NeuroglancerGlobalStyles classes={classes} />
158
+ <div className={classes.neuroglancerWrapper}>
159
+ <Suspense fallback={<div>Loading...</div>}>
160
+ <LazyReactNeuroglancer
161
+ brainMapsClientId="NOT_A_VALID_ID"
162
+ viewerState={this.viewerState}
163
+ onViewerStateChanged={this.onViewerStateChanged}
164
+ bundleRoot={this.bundleRoot}
165
+ ref={this.onRef}
166
+ />
167
+ </Suspense>
168
+ </div>
169
+ </>
170
+ );
171
+ }
49
172
  }