@vitessce/neuroglancer 3.6.18 → 3.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/styles.js CHANGED
@@ -23,6 +23,7 @@ const globalNeuroglancerCss = `
23
23
  .neuroglancer-viewer-top-row,
24
24
  .neuroglancer-layer-panel,
25
25
  .neuroglancer-side-panel-column,
26
+ .neuroglancer-display-dimensions-widget,
26
27
  .neuroglancer-data-panel-layout-controls button{
27
28
  display: none !important;
28
29
  }
@@ -1427,7 +1428,8 @@ const globalNeuroglancerStyles = {
1427
1428
  borderColor: '#000',
1428
1429
  borderWidth: '2px',
1429
1430
  },
1430
- '.neuroglancer-panel:focus-within': { borderColor: '#fff' },
1431
+ // Hides the white border around NG view that shows the view is focused
1432
+ // '.neuroglancer-panel:focus-within': { borderColor: '#fff' },
1431
1433
  '.neuroglancer-layer-group-viewer': { outline: '0px' },
1432
1434
  '.neuroglancer-layer-group-viewer-context-menu': {
1433
1435
  flexDirection: 'column',
package/src/utils.js ADDED
@@ -0,0 +1,156 @@
1
+ import {
2
+ Quaternion,
3
+ Euler,
4
+ } from 'three';
5
+
6
+
7
+ // For now deckGl uses degrees, but if changes to radian can change here
8
+ // const VIT_UNITS = 'degrees';
9
+
10
+ export const EPSILON_KEYS_MAPPING_NG = {
11
+ projectionScale: 100,
12
+ projectionOrientation: 2e-2,
13
+ position: 1,
14
+ };
15
+
16
+ // allow smaller pos deltas to pass when zoom changed
17
+ export const SOFT_POS_FACTOR = 0.15;
18
+ // To rotate the y-axis up in NG
19
+ export const Q_Y_UP = [1, 0, 0, 0]; // [x,y,z,w] for 180° about X
20
+
21
+ // ---- Y-up correction: 180° around X so X stays right, Y flips up (Z flips sign, which is OK) ----
22
+ export const multiplyQuat = (a, b) => {
23
+ const [ax, ay, az, aw] = a;
24
+ const [bx, by, bz, bw] = b;
25
+ return [
26
+ aw * bx + ax * bw + ay * bz - az * by,
27
+ aw * by - ax * bz + ay * bw + az * bx,
28
+ aw * bz + ax * by - ay * bx + az * bw,
29
+ aw * bw - ax * bx - ay * by - az * bz,
30
+ ];
31
+ };
32
+
33
+ export const conjQuat = q => ([-q[0], -q[1], -q[2], q[3]]); // inverse for unit quats
34
+
35
+ // Helper function to compute the cosine dot product of two quaternion
36
+ export const quatdotAbs = (a, b) => Math.abs(a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]);
37
+
38
+
39
+ export const rad2deg = r => r * 180 / Math.PI;
40
+ export const deg2rad = d => d * Math.PI / 180;
41
+
42
+ // export const toVitUnits = rad => VIT_UNITS === 'degrees' ? (rad * 180 / Math.PI) : rad;
43
+ // export const fromVitUnits = val => VIT_UNITS === 'degrees' ? (val * Math.PI / 180) : val;
44
+
45
+
46
+ /**
47
+ * Is this a valid viewerState object?
48
+ * @param {object} viewerState
49
+ * @returns {boolean}
50
+ */
51
+ function isValidState(viewerState) {
52
+ const { projectionScale, projectionOrientation, position, dimensions } = viewerState || {};
53
+ return (
54
+ dimensions !== undefined
55
+ && typeof projectionScale === 'number'
56
+ && Array.isArray(projectionOrientation)
57
+ && projectionOrientation.length === 4
58
+ && Array.isArray(position)
59
+ && position.length === 3
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Returns true if the difference is greater than the epsilon for that key.
65
+ * @param {array | number} a Previous viewerState key, i.e., position.
66
+ * @param {array | number } b Next viewerState key, i.e., position.
67
+ * @returns
68
+ */
69
+
70
+ export function valueGreaterThanEpsilon(a, b, epsilon) {
71
+ if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
72
+ return a.some((val, i) => Math.abs(val - b[i]) > epsilon);
73
+ }
74
+ if (typeof a === 'number' && typeof b === 'number') {
75
+ return Math.abs(a - b) > epsilon;
76
+ }
77
+ return undefined;
78
+ }
79
+
80
+ export const nearEq = (a, b, epsilon) => (
81
+ Number.isFinite(a) && Number.isFinite(b) ? Math.abs(a - b) <= epsilon : a === b
82
+ );
83
+
84
+ /**
85
+ * Returns true if the two states are equal, or false if not.
86
+ * @param {object} prevState Previous viewer state.
87
+ * @param {object} nextState Next viewer state.
88
+ * @returns {Boolean} True if any key has changed
89
+ */
90
+
91
+ export function didCameraStateChange(prevState, nextState) {
92
+ if (!isValidState(nextState)) return false;
93
+ return Object.entries(EPSILON_KEYS_MAPPING_NG)
94
+ .some(([key, eps]) => valueGreaterThanEpsilon(
95
+ prevState?.[key],
96
+ nextState?.[key],
97
+ eps,
98
+ ));
99
+ }
100
+
101
+ // To see if any and which cameraState has changed
102
+ // adjust for coupled zoom+position changes
103
+ export function diffCameraState(prev, next) {
104
+ if (!isValidState(next)) return { changed: false, scale: false, pos: false, rot: false };
105
+
106
+ const eps = EPSILON_KEYS_MAPPING_NG;
107
+ const scale = valueGreaterThanEpsilon(prev?.projectionScale,
108
+ next?.projectionScale, eps.projectionScale);
109
+ const posHard = valueGreaterThanEpsilon(prev?.position, next?.position, eps.position);
110
+ const rot = valueGreaterThanEpsilon(prev?.projectionOrientation,
111
+ next?.projectionOrientation, eps.projectionOrientation);
112
+
113
+ // If zoom changed, allow a softer position threshold so zoom+pos travel together.
114
+ const posSoft = !posHard && scale
115
+ && valueGreaterThanEpsilon(prev?.position, next?.position, SOFT_POS_FACTOR);
116
+ const pos = posHard || posSoft;
117
+
118
+ return { changed: scale || pos || rot, scale, pos, rot };
119
+ }
120
+
121
+
122
+ // Convert WebGL's Quaternion rotation to DeckGL's Euler
123
+ export function quaternionToEuler([x, y, z, w]) {
124
+ const quaternion = new Quaternion(x, y, z, w);
125
+ // deck.gl uses Y (yaw), X (pitch), Z (roll)
126
+ // TODO confirm the direction - YXZ
127
+ const euler = new Euler().setFromQuaternion(quaternion, 'YXZ');
128
+ const pitch = euler.x; // X-axis rotation
129
+ const yaw = euler.y; // Y-axis rotation
130
+
131
+ // return [pitch * RAD2DEG, yaw * RAD2DEG];
132
+ return [pitch, yaw];
133
+ }
134
+
135
+
136
+ // Convert DeckGL's rotation in Euler to WebGL's Quaternion
137
+ export function eulerToQuaternion(pitch, yaw, roll = 0) {
138
+ const euler = new Euler(pitch, yaw, roll, 'YXZ'); // rotation order
139
+ const quaternion = new Quaternion().setFromEuler(euler);
140
+ return [quaternion.x, quaternion.y, quaternion.z, quaternion.w];
141
+ }
142
+
143
+
144
+ // Calibrate once from an initial deck zoom and NG projectionScale
145
+ export function makeVitNgZoomCalibrator(initialNgProjectionScale, initialDeckZoom = 0) {
146
+ const base = (initialNgProjectionScale / (2 ** -initialDeckZoom));
147
+ return {
148
+ base,
149
+ vitToNgZoom(z) {
150
+ return (base) * (2 ** -z);
151
+ },
152
+ ngToVitZoom(sNg) {
153
+ return Math.log2(base / (sNg));
154
+ },
155
+ };
156
+ }
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ multiplyQuat,
4
+ conjQuat,
5
+ eulerToQuaternion,
6
+ quaternionToEuler,
7
+ Q_Y_UP,
8
+ } from './utils.js';
9
+
10
+ // To normalize/map the angels between [-π, π]
11
+ const wrap = a => Math.atan2(Math.sin(a), Math.cos(a));
12
+ const close = (a, b, eps = 1e-6) => Math.abs(wrap(a - b)) < eps;
13
+
14
+ describe('Quaternion utilities', () => {
15
+ it('multiplyQuat identity', () => {
16
+ const I = [0, 0, 0, 1];
17
+ const q = eulerToQuaternion(0.2, -0.5, 0.1);
18
+ expect(multiplyQuat(I, q)).toEqual(expect.arrayContaining(q));
19
+ expect(multiplyQuat(q, I)).toEqual(expect.arrayContaining(q));
20
+ });
21
+
22
+ // Tests the expected angle after Q_Y_UP
23
+ it('Y-up flip maps (x, y, z) -> (x, -y, -z)', () => {
24
+ const v = [0.3, 0.4, 0.0];
25
+ const qVit = eulerToQuaternion(...v);
26
+ const qNg = multiplyQuat(Q_Y_UP, qVit);
27
+ const [pitch, yaw] = quaternionToEuler(qNg); // radians
28
+ const ok = (close(pitch, -v[0]) && close(yaw, -v[1]))
29
+ || (close(pitch, -v[0]) && close(yaw, Math.PI - v[1]));
30
+ // Euler angles can give both yaw’ ≈ −yaw and yaw’ ≈ π − yaw
31
+ // Alternative is to compare only Quaternion
32
+ expect(ok).toBe(true);
33
+ });
34
+
35
+ // Tests that applying and reversing Q_Y_UP gives back original orientation
36
+ it('Q_Y_UP round trip', () => {
37
+ const qVit = eulerToQuaternion(0.25, -0.7, 0);
38
+ const qNg = multiplyQuat(Q_Y_UP, qVit);
39
+ const qBack = multiplyQuat(conjQuat(Q_Y_UP), qNg);
40
+ // equal up to sign
41
+ const sameOrNeg = qBack.map((x, i) => Math.abs(x) - Math.abs(qVit[i]));
42
+ sameOrNeg.forEach(d => expect(Math.abs(d)).toBeLessThan(1e-6));
43
+ });
44
+ });