@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/dist/{ReactNeuroglancer-Crquekcy.js → ReactNeuroglancer-C0i-a6Cw.js} +717 -1268
- package/dist/{index-BNCfoEHv.js → index-w8xI9TWU.js} +2211 -478
- package/dist/index.js +1 -1
- package/dist-tsc/Neuroglancer.d.ts +5 -3
- package/dist-tsc/Neuroglancer.d.ts.map +1 -1
- package/dist-tsc/Neuroglancer.js +31 -72
- package/dist-tsc/NeuroglancerSubscriber.d.ts.map +1 -1
- package/dist-tsc/NeuroglancerSubscriber.js +329 -93
- package/dist-tsc/ReactNeuroglancer.d.ts +147 -2
- package/dist-tsc/ReactNeuroglancer.d.ts.map +1 -1
- package/dist-tsc/ReactNeuroglancer.js +819 -5
- package/dist-tsc/styles.d.ts.map +1 -1
- package/dist-tsc/styles.js +3 -1
- package/dist-tsc/utils.d.ts +41 -0
- package/dist-tsc/utils.d.ts.map +1 -0
- package/dist-tsc/utils.js +117 -0
- package/dist-tsc/utils.test.d.ts +2 -0
- package/dist-tsc/utils.test.d.ts.map +1 -0
- package/dist-tsc/utils.test.js +34 -0
- package/package.json +12 -12
- package/src/Neuroglancer.js +32 -91
- package/src/NeuroglancerSubscriber.js +400 -108
- package/src/ReactNeuroglancer.js +912 -6
- package/src/styles.js +3 -1
- package/src/utils.js +156 -0
- package/src/utils.test.js +44 -0
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
|
-
|
|
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
|
+
});
|