@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.
@@ -1 +1 @@
1
- {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../src/styles.js"],"names":[],"mappings":"AAmkFA,gIAcC;AA7kFD;;cAiBe,CAAC;;;kBAIP,eAAsB;gBACxB,WAAU;eAGf,WAAU;EApBR"}
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../src/styles.js"],"names":[],"mappings":"AAqkFA,gIAcC;AA/kFD;;cAiBe,CAAC;;;kBAIP,eAAsB;gBAChB,WAAU;eAAsB,WACvC;EAlBJ"}
@@ -21,6 +21,7 @@ const globalNeuroglancerCss = `
21
21
  .neuroglancer-viewer-top-row,
22
22
  .neuroglancer-layer-panel,
23
23
  .neuroglancer-side-panel-column,
24
+ .neuroglancer-display-dimensions-widget,
24
25
  .neuroglancer-data-panel-layout-controls button{
25
26
  display: none !important;
26
27
  }
@@ -1423,7 +1424,8 @@ const globalNeuroglancerStyles = {
1423
1424
  borderColor: '#000',
1424
1425
  borderWidth: '2px',
1425
1426
  },
1426
- '.neuroglancer-panel:focus-within': { borderColor: '#fff' },
1427
+ // Hides the white border around NG view that shows the view is focused
1428
+ // '.neuroglancer-panel:focus-within': { borderColor: '#fff' },
1427
1429
  '.neuroglancer-layer-group-viewer': { outline: '0px' },
1428
1430
  '.neuroglancer-layer-group-viewer-context-menu': {
1429
1431
  flexDirection: 'column',
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Returns true if the difference is greater than the epsilon for that key.
3
+ * @param {array | number} a Previous viewerState key, i.e., position.
4
+ * @param {array | number } b Next viewerState key, i.e., position.
5
+ * @returns
6
+ */
7
+ export function valueGreaterThanEpsilon(a: array | number, b: array | number, epsilon: any): boolean | undefined;
8
+ /**
9
+ * Returns true if the two states are equal, or false if not.
10
+ * @param {object} prevState Previous viewer state.
11
+ * @param {object} nextState Next viewer state.
12
+ * @returns {Boolean} True if any key has changed
13
+ */
14
+ export function didCameraStateChange(prevState: object, nextState: object): boolean;
15
+ export function diffCameraState(prev: any, next: any): {
16
+ changed: boolean | undefined;
17
+ scale: boolean | undefined;
18
+ pos: boolean | undefined;
19
+ rot: boolean | undefined;
20
+ };
21
+ export function quaternionToEuler([x, y, z, w]: [any, any, any, any]): any[];
22
+ export function eulerToQuaternion(pitch: any, yaw: any, roll?: number): any[];
23
+ export function makeVitNgZoomCalibrator(initialNgProjectionScale: any, initialDeckZoom?: number): {
24
+ base: number;
25
+ vitToNgZoom(z: any): number;
26
+ ngToVitZoom(sNg: any): number;
27
+ };
28
+ export namespace EPSILON_KEYS_MAPPING_NG {
29
+ let projectionScale: number;
30
+ let projectionOrientation: number;
31
+ let position: number;
32
+ }
33
+ export const SOFT_POS_FACTOR: 0.15;
34
+ export const Q_Y_UP: number[];
35
+ export function multiplyQuat(a: any, b: any): number[];
36
+ export function conjQuat(q: any): any[];
37
+ export function quatdotAbs(a: any, b: any): number;
38
+ export function rad2deg(r: any): number;
39
+ export function deg2rad(d: any): number;
40
+ export function nearEq(a: any, b: any, epsilon: any): boolean;
41
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.js"],"names":[],"mappings":"AA8DA;;;;;GAKG;AAEH,2CALW,KAAK,GAAG,MAAM,KACd,KAAK,GAAG,MAAM,qCAYxB;AAMD;;;;;GAKG;AAEH,gDALW,MAAM,aACN,MAAM,WAYhB;AAID;;;;;EAgBC;AAID,6EAUC;AAID,8EAIC;AAID;;;;EAWC;;;;;;AA3ID,mCAAoC;AAEpC,8BAAmC;AAG5B,uDASN;AAEM,wCAAmD;AAGnD,mDAA4F;AAG5F,wCAAsC;AACtC,wCAAsC;AAwCtC,8DAEN"}
@@ -0,0 +1,117 @@
1
+ import { Quaternion, Euler, } from 'three';
2
+ // For now deckGl uses degrees, but if changes to radian can change here
3
+ // const VIT_UNITS = 'degrees';
4
+ export const EPSILON_KEYS_MAPPING_NG = {
5
+ projectionScale: 100,
6
+ projectionOrientation: 2e-2,
7
+ position: 1,
8
+ };
9
+ // allow smaller pos deltas to pass when zoom changed
10
+ export const SOFT_POS_FACTOR = 0.15;
11
+ // To rotate the y-axis up in NG
12
+ export const Q_Y_UP = [1, 0, 0, 0]; // [x,y,z,w] for 180° about X
13
+ // ---- Y-up correction: 180° around X so X stays right, Y flips up (Z flips sign, which is OK) ----
14
+ export const multiplyQuat = (a, b) => {
15
+ const [ax, ay, az, aw] = a;
16
+ const [bx, by, bz, bw] = b;
17
+ return [
18
+ aw * bx + ax * bw + ay * bz - az * by,
19
+ aw * by - ax * bz + ay * bw + az * bx,
20
+ aw * bz + ax * by - ay * bx + az * bw,
21
+ aw * bw - ax * bx - ay * by - az * bz,
22
+ ];
23
+ };
24
+ export const conjQuat = q => ([-q[0], -q[1], -q[2], q[3]]); // inverse for unit quats
25
+ // Helper function to compute the cosine dot product of two quaternion
26
+ export const quatdotAbs = (a, b) => Math.abs(a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]);
27
+ export const rad2deg = r => r * 180 / Math.PI;
28
+ export const deg2rad = d => d * Math.PI / 180;
29
+ // export const toVitUnits = rad => VIT_UNITS === 'degrees' ? (rad * 180 / Math.PI) : rad;
30
+ // export const fromVitUnits = val => VIT_UNITS === 'degrees' ? (val * Math.PI / 180) : val;
31
+ /**
32
+ * Is this a valid viewerState object?
33
+ * @param {object} viewerState
34
+ * @returns {boolean}
35
+ */
36
+ function isValidState(viewerState) {
37
+ const { projectionScale, projectionOrientation, position, dimensions } = viewerState || {};
38
+ return (dimensions !== undefined
39
+ && typeof projectionScale === 'number'
40
+ && Array.isArray(projectionOrientation)
41
+ && projectionOrientation.length === 4
42
+ && Array.isArray(position)
43
+ && position.length === 3);
44
+ }
45
+ /**
46
+ * Returns true if the difference is greater than the epsilon for that key.
47
+ * @param {array | number} a Previous viewerState key, i.e., position.
48
+ * @param {array | number } b Next viewerState key, i.e., position.
49
+ * @returns
50
+ */
51
+ export function valueGreaterThanEpsilon(a, b, epsilon) {
52
+ if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
53
+ return a.some((val, i) => Math.abs(val - b[i]) > epsilon);
54
+ }
55
+ if (typeof a === 'number' && typeof b === 'number') {
56
+ return Math.abs(a - b) > epsilon;
57
+ }
58
+ return undefined;
59
+ }
60
+ export const nearEq = (a, b, epsilon) => (Number.isFinite(a) && Number.isFinite(b) ? Math.abs(a - b) <= epsilon : a === b);
61
+ /**
62
+ * Returns true if the two states are equal, or false if not.
63
+ * @param {object} prevState Previous viewer state.
64
+ * @param {object} nextState Next viewer state.
65
+ * @returns {Boolean} True if any key has changed
66
+ */
67
+ export function didCameraStateChange(prevState, nextState) {
68
+ if (!isValidState(nextState))
69
+ return false;
70
+ return Object.entries(EPSILON_KEYS_MAPPING_NG)
71
+ .some(([key, eps]) => valueGreaterThanEpsilon(prevState?.[key], nextState?.[key], eps));
72
+ }
73
+ // To see if any and which cameraState has changed
74
+ // adjust for coupled zoom+position changes
75
+ export function diffCameraState(prev, next) {
76
+ if (!isValidState(next))
77
+ return { changed: false, scale: false, pos: false, rot: false };
78
+ const eps = EPSILON_KEYS_MAPPING_NG;
79
+ const scale = valueGreaterThanEpsilon(prev?.projectionScale, next?.projectionScale, eps.projectionScale);
80
+ const posHard = valueGreaterThanEpsilon(prev?.position, next?.position, eps.position);
81
+ const rot = valueGreaterThanEpsilon(prev?.projectionOrientation, next?.projectionOrientation, eps.projectionOrientation);
82
+ // If zoom changed, allow a softer position threshold so zoom+pos travel together.
83
+ const posSoft = !posHard && scale
84
+ && valueGreaterThanEpsilon(prev?.position, next?.position, SOFT_POS_FACTOR);
85
+ const pos = posHard || posSoft;
86
+ return { changed: scale || pos || rot, scale, pos, rot };
87
+ }
88
+ // Convert WebGL's Quaternion rotation to DeckGL's Euler
89
+ export function quaternionToEuler([x, y, z, w]) {
90
+ const quaternion = new Quaternion(x, y, z, w);
91
+ // deck.gl uses Y (yaw), X (pitch), Z (roll)
92
+ // TODO confirm the direction - YXZ
93
+ const euler = new Euler().setFromQuaternion(quaternion, 'YXZ');
94
+ const pitch = euler.x; // X-axis rotation
95
+ const yaw = euler.y; // Y-axis rotation
96
+ // return [pitch * RAD2DEG, yaw * RAD2DEG];
97
+ return [pitch, yaw];
98
+ }
99
+ // Convert DeckGL's rotation in Euler to WebGL's Quaternion
100
+ export function eulerToQuaternion(pitch, yaw, roll = 0) {
101
+ const euler = new Euler(pitch, yaw, roll, 'YXZ'); // rotation order
102
+ const quaternion = new Quaternion().setFromEuler(euler);
103
+ return [quaternion.x, quaternion.y, quaternion.z, quaternion.w];
104
+ }
105
+ // Calibrate once from an initial deck zoom and NG projectionScale
106
+ export function makeVitNgZoomCalibrator(initialNgProjectionScale, initialDeckZoom = 0) {
107
+ const base = (initialNgProjectionScale / (2 ** -initialDeckZoom));
108
+ return {
109
+ base,
110
+ vitToNgZoom(z) {
111
+ return (base) * (2 ** -z);
112
+ },
113
+ ngToVitZoom(sNg) {
114
+ return Math.log2(base / (sNg));
115
+ },
116
+ };
117
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=utils.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.test.d.ts","sourceRoot":"","sources":["../src/utils.test.js"],"names":[],"mappings":""}
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { multiplyQuat, conjQuat, eulerToQuaternion, quaternionToEuler, Q_Y_UP, } from './utils.js';
3
+ // To normalize/map the angels between [-π, π]
4
+ const wrap = a => Math.atan2(Math.sin(a), Math.cos(a));
5
+ const close = (a, b, eps = 1e-6) => Math.abs(wrap(a - b)) < eps;
6
+ describe('Quaternion utilities', () => {
7
+ it('multiplyQuat identity', () => {
8
+ const I = [0, 0, 0, 1];
9
+ const q = eulerToQuaternion(0.2, -0.5, 0.1);
10
+ expect(multiplyQuat(I, q)).toEqual(expect.arrayContaining(q));
11
+ expect(multiplyQuat(q, I)).toEqual(expect.arrayContaining(q));
12
+ });
13
+ // Tests the expected angle after Q_Y_UP
14
+ it('Y-up flip maps (x, y, z) -> (x, -y, -z)', () => {
15
+ const v = [0.3, 0.4, 0.0];
16
+ const qVit = eulerToQuaternion(...v);
17
+ const qNg = multiplyQuat(Q_Y_UP, qVit);
18
+ const [pitch, yaw] = quaternionToEuler(qNg); // radians
19
+ const ok = (close(pitch, -v[0]) && close(yaw, -v[1]))
20
+ || (close(pitch, -v[0]) && close(yaw, Math.PI - v[1]));
21
+ // Euler angles can give both yaw’ ≈ −yaw and yaw’ ≈ π − yaw
22
+ // Alternative is to compare only Quaternion
23
+ expect(ok).toBe(true);
24
+ });
25
+ // Tests that applying and reversing Q_Y_UP gives back original orientation
26
+ it('Q_Y_UP round trip', () => {
27
+ const qVit = eulerToQuaternion(0.25, -0.7, 0);
28
+ const qNg = multiplyQuat(Q_Y_UP, qVit);
29
+ const qBack = multiplyQuat(conjQuat(Q_Y_UP), qNg);
30
+ // equal up to sign
31
+ const sameOrNeg = qBack.map((x, i) => Math.abs(x) - Math.abs(qVit[i]));
32
+ sameOrNeg.forEach(d => expect(Math.abs(d)).toBeLessThan(1e-6));
33
+ });
34
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitessce/neuroglancer",
3
- "version": "3.6.18",
3
+ "version": "3.7.1",
4
4
  "author": "Gehlenborg Lab",
5
5
  "homepage": "http://vitessce.io",
6
6
  "repository": {
@@ -16,27 +16,27 @@
16
16
  "dist-tsc"
17
17
  ],
18
18
  "dependencies": {
19
- "@janelia-flyem/react-neuroglancer": "2.5.0",
20
19
  "@janelia-flyem/neuroglancer": "2.37.5",
21
20
  "lodash-es": "^4.17.21",
22
- "@vitessce/neuroglancer-workers": "3.6.18",
23
- "@vitessce/styles": "3.6.18",
24
- "@vitessce/constants-internal": "3.6.18",
25
- "@vitessce/vit-s": "3.6.18",
26
- "@vitessce/sets-utils": "3.6.18",
27
- "@vitessce/utils": "3.6.18",
28
- "@vitessce/tooltip": "3.6.18"
21
+ "three": "^0.154.0",
22
+ "react": "^18.0.0",
23
+ "@vitessce/neuroglancer-workers": "3.7.1",
24
+ "@vitessce/styles": "3.7.1",
25
+ "@vitessce/constants-internal": "3.7.1",
26
+ "@vitessce/vit-s": "3.7.1",
27
+ "@vitessce/sets-utils": "3.7.1",
28
+ "@vitessce/utils": "3.7.1",
29
+ "@vitessce/tooltip": "3.7.1"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@testing-library/jest-dom": "^6.6.3",
32
33
  "@testing-library/react": "^16.3.0",
33
- "react": "^18.0.0",
34
34
  "react-dom": "^18.0.0",
35
- "vite": "^6.3.5",
35
+ "vite": "^7.0.0",
36
36
  "vitest": "^3.1.4"
37
37
  },
38
38
  "scripts": {
39
- "bundle": "pnpm exec vite build -c ../../../scripts/vite.config.js",
39
+ "bundle": "pnpm exec vite build -c ../../../scripts/vite.config.mjs",
40
40
  "test": "pnpm exec vitest --run"
41
41
  },
42
42
  "module": "dist/index.js",
@@ -1,7 +1,6 @@
1
1
  /* eslint-disable react-refresh/only-export-components */
2
2
  import React, { PureComponent, Suspense } from 'react';
3
3
  import { ChunkWorker } from '@vitessce/neuroglancer-workers';
4
- import { isEqualWith, pick } from 'lodash-es';
5
4
  import { NeuroglancerGlobalStyles } from './styles.js';
6
5
 
7
6
  const LazyReactNeuroglancer = React.lazy(() => import('./ReactNeuroglancer.js'));
@@ -9,145 +8,86 @@ const LazyReactNeuroglancer = React.lazy(() => import('./ReactNeuroglancer.js'))
9
8
  function createWorker() {
10
9
  return new ChunkWorker();
11
10
  }
12
-
13
- /**
14
- * Is this a valid viewerState object?
15
- * @param {object} viewerState
16
- * @returns {boolean}
17
- */
18
- function isValidState(viewerState) {
19
- const { projectionScale, projectionOrientation, position, dimensions } = viewerState || {};
20
- return (
21
- dimensions !== undefined
22
- && typeof projectionScale === 'number'
23
- && Array.isArray(projectionOrientation)
24
- && projectionOrientation.length === 4
25
- && Array.isArray(position)
26
- && position.length === 3
27
- );
28
- }
29
-
30
- // TODO: Do we want to use the same epsilon value
31
- // for every viewstate property being compared?
32
- const EPSILON = 1e-7;
33
- const VIEWSTATE_KEYS = ['projectionScale', 'projectionOrientation', 'position'];
34
-
35
- // Custom numeric comparison function
36
- // for isEqualWith, to be able to set a custom epsilon.
37
- function customizer(a, b) {
38
- if (typeof a === 'number' && typeof b === 'number') {
39
- // Returns true if the values are equivalent, else false.
40
- return Math.abs(a - b) > EPSILON;
41
- }
42
- // Return undefined to fallback to the default
43
- // comparison function.
44
- return undefined;
45
- }
46
-
47
- /**
48
- * Returns true if the two states are equal, or false if not.
49
- * @param {object} prevState Previous viewer state.
50
- * @param {object} nextState Next viewer state.
51
- * @returns
52
- */
53
- function compareViewerState(prevState, nextState) {
54
- if (isValidState(nextState)) {
55
- // Subset the viewerState objects to only the keys
56
- // that we want to use for comparison.
57
- const prevSubset = pick(prevState, VIEWSTATE_KEYS);
58
- const nextSubset = pick(nextState, VIEWSTATE_KEYS);
59
- return isEqualWith(prevSubset, nextSubset, customizer);
60
- }
61
- return true;
62
- }
63
-
64
- export class Neuroglancer extends PureComponent {
11
+ export class NeuroglancerComp extends PureComponent {
65
12
  constructor(props) {
66
13
  super(props);
67
-
68
14
  this.bundleRoot = createWorker();
69
-
70
- this.viewerState = props.viewerState;
15
+ this.cellColorMapping = props.cellColorMapping;
71
16
  this.justReceivedExternalUpdate = false;
72
-
73
17
  this.prevElement = null;
74
18
  this.prevClickHandler = null;
75
19
  this.prevMouseStateChanged = null;
76
20
  this.prevHoverHandler = null;
77
-
78
21
  this.onViewerStateChanged = this.onViewerStateChanged.bind(this);
79
22
  this.onRef = this.onRef.bind(this);
23
+ // To avoid closure for onSegmentClick(), to update the selection
24
+ this.latestOnSegmentClick = props.onSegmentClick;
25
+ this.latestOnSelectHoveredCoords = props.onSelectHoveredCoords;
80
26
  }
81
27
 
82
28
  onRef(viewerRef) {
83
29
  // Here, we have access to the viewerRef.viewer object,
84
30
  // which we can use to add/remove event handlers.
85
- const {
86
- onSegmentClick,
87
- onSelectHoveredCoords,
88
- } = this.props;
89
31
 
90
32
  if (viewerRef) {
91
33
  // Mount
92
34
  const { viewer } = viewerRef;
93
35
  this.prevElement = viewer.element;
94
36
  this.prevMouseStateChanged = viewer.mouseState.changed;
37
+ viewer.inputEventBindings.sliceView.set('at:dblclick0', () => {});
38
+ viewer.inputEventBindings.perspectiveView.set('at:dblclick0', () => {});
95
39
  this.prevClickHandler = (event) => {
96
40
  if (event.button === 0) {
97
- setTimeout(() => {
98
- const { pickedValue } = viewer.mouseState;
99
- if (pickedValue && pickedValue?.low) {
100
- onSegmentClick(pickedValue?.low);
41
+ // Wait for mouseState to update
42
+ requestAnimationFrame(() => {
43
+ const { pickedValue, pickedRenderLayer } = viewer.mouseState;
44
+ // Only trigger selection when a segment is clicked rather than any click on the view
45
+ if (pickedValue && pickedValue.low !== undefined && pickedRenderLayer) {
46
+ this.latestOnSegmentClick?.(pickedValue.low);
101
47
  }
102
- }, 100);
48
+ });
103
49
  }
104
50
  };
105
- viewer.element.addEventListener('mousedown', this.prevClickHandler);
106
-
107
51
  this.prevHoverHandler = () => {
108
52
  if (viewer.mouseState.pickedValue !== undefined) {
109
53
  const pickedSegment = viewer.mouseState.pickedValue;
110
- onSelectHoveredCoords(pickedSegment?.low);
54
+ this.latestOnSelectHoveredCoords?.(pickedSegment?.low);
111
55
  }
112
56
  };
113
-
57
+ viewer.element.addEventListener('mouseup', this.prevClickHandler);
114
58
  viewer.mouseState.changed.add(this.prevHoverHandler);
115
59
  } else {
116
60
  // Unmount (viewerRef is null)
117
61
  if (this.prevElement && this.prevClickHandler) {
118
- this.prevElement.removeEventListener('mousedown', this.prevClickHandler);
62
+ this.prevElement.removeEventListener('mouseup', this.prevClickHandler);
63
+ this.prevClickHandler = null;
119
64
  }
120
65
  if (this.prevMouseStateChanged && this.prevHoverHandler) {
121
66
  this.prevMouseStateChanged.remove(this.prevHoverHandler);
67
+ this.prevHoverHandler = null;
122
68
  }
69
+ this.prevElement = null;
70
+ this.prevMouseStateChanged = null;
123
71
  }
124
72
  }
125
73
 
126
74
  onViewerStateChanged(nextState) {
127
75
  const { setViewerState } = this.props;
128
- const { viewerState: prevState } = this;
129
-
130
- if (!this.justReceivedExternalUpdate && !compareViewerState(prevState, nextState)) {
131
- this.viewerState = nextState;
132
- this.justReceivedExternalUpdate = false;
133
- setViewerState(nextState);
134
- }
76
+ setViewerState(nextState);
135
77
  }
136
78
 
137
- UNSAFE_componentWillUpdate(prevProps) {
138
- if (!compareViewerState(this.viewerState, prevProps.viewerState)) {
139
- this.viewerState = prevProps.viewerState;
140
- this.justReceivedExternalUpdate = true;
141
- setTimeout(() => {
142
- this.justReceivedExternalUpdate = false;
143
- }, 100);
79
+ componentDidUpdate(prevProps) {
80
+ const { onSegmentClick, onSelectHoveredCoords } = this.props;
81
+ if (prevProps.onSegmentClick !== onSegmentClick) {
82
+ this.latestOnSegmentClick = onSegmentClick;
83
+ }
84
+ if (prevProps.onSelectHoveredCoords !== onSelectHoveredCoords) {
85
+ this.latestOnSelectHoveredCoords = onSelectHoveredCoords;
144
86
  }
145
87
  }
146
88
 
147
89
  render() {
148
- const {
149
- classes,
150
- } = this.props;
90
+ const { classes, viewerState, cellColorMapping } = this.props;
151
91
 
152
92
  return (
153
93
  <>
@@ -156,9 +96,10 @@ export class Neuroglancer extends PureComponent {
156
96
  <Suspense fallback={<div>Loading...</div>}>
157
97
  <LazyReactNeuroglancer
158
98
  brainMapsClientId="NOT_A_VALID_ID"
159
- viewerState={this.viewerState}
99
+ viewerState={viewerState}
160
100
  onViewerStateChanged={this.onViewerStateChanged}
161
101
  bundleRoot={this.bundleRoot}
102
+ cellColorMapping={cellColorMapping}
162
103
  ref={this.onRef}
163
104
  />
164
105
  </Suspense>