@vitessce/neuroglancer 3.9.5 → 3.9.6

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.
@@ -0,0 +1,432 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+
3
+ import {
4
+ getSpatialLayerColorShader,
5
+ getSpatialLayerColorWithSelectionShader,
6
+ getSpatialLayerColorFilteredShader,
7
+ getGeneSelectionNoSelectionShader,
8
+ getGeneSelectionWithSelectionShader,
9
+ getGeneSelectionFilteredShader,
10
+ getRandomByFeatureShader,
11
+ getRandomByFeatureWithSelectionShader,
12
+ getRandomByFeatureFilteredShader,
13
+ getRandomPerPointShader,
14
+ getRandomPerPointWithSelectionShader,
15
+ getRandomPerPointFilteredShader,
16
+ } from './shader-utils.js';
17
+
18
+ // Mock the @vitessce/utils module before importing the module under test.
19
+ vi.mock('@vitessce/utils', () => ({
20
+ PALETTE: [
21
+ [255, 0, 0],
22
+ [0, 255, 0],
23
+ [0, 0, 255],
24
+ ],
25
+ getDefaultColor: theme => (theme === 'dark' ? [128, 128, 128] : [200, 200, 200]),
26
+ }));
27
+
28
+ /**
29
+ * Helper: compare two shader strings line-by-line, ignoring
30
+ * leading/trailing whitespace on each line and ignoring empty lines.
31
+ */
32
+ function expectShaderEqual(actual, expected) {
33
+ const normalize = s => s
34
+ .split('\n')
35
+ .map(line => line.trim())
36
+ .filter(line => line.length > 0);
37
+ expect(normalize(actual)).toEqual(normalize(expected));
38
+ }
39
+
40
+ // ============================================================
41
+ // Case 1: spatialLayerColor
42
+ // ============================================================
43
+
44
+ describe('getSpatialLayerColorShader', () => {
45
+ it('generates a shader that sets all points to the static color', () => {
46
+ const result = getSpatialLayerColorShader([255, 128, 0], 0.8);
47
+ const expected = `
48
+ void main() {
49
+ setColor(vec4(1, 0.5019607843137255, 0, 0.8));
50
+ }
51
+ `;
52
+ expectShaderEqual(result, expected);
53
+ });
54
+
55
+ it('handles zero opacity', () => {
56
+ const result = getSpatialLayerColorShader([0, 0, 0], 0);
57
+ const expected = `
58
+ void main() {
59
+ setColor(vec4(0, 0, 0, 0));
60
+ }
61
+ `;
62
+ expectShaderEqual(result, expected);
63
+ });
64
+
65
+ it('handles full white at full opacity', () => {
66
+ const result = getSpatialLayerColorShader([255, 255, 255], 1.0);
67
+ const expected = `
68
+ void main() {
69
+ setColor(vec4(1, 1, 1, 1));
70
+ }
71
+ `;
72
+ expectShaderEqual(result, expected);
73
+ });
74
+ });
75
+
76
+ describe('getSpatialLayerColorWithSelectionShader', () => {
77
+ it('generates a shader that colors selected features with static color and unselected with default', () => {
78
+ const result = getSpatialLayerColorWithSelectionShader(
79
+ [255, 0, 0], 0.5, [2, 5], [128, 128, 128], 'gene_index',
80
+ );
81
+ const expected = `
82
+ void main() {
83
+ int geneIndex = prop_gene_index();
84
+ int selectedIndices[2] = int[2](2, 5);
85
+ bool isSelected = false;
86
+ for (int i = 0; i < 2; ++i) {
87
+ if (geneIndex == selectedIndices[i]) {
88
+ isSelected = true;
89
+ }
90
+ }
91
+ if (isSelected) {
92
+ setColor(vec4(1, 0, 0, 0.5));
93
+ } else {
94
+ setColor(vec4(0.5019607843137255, 0.5019607843137255, 0.5019607843137255, 0.5));
95
+ }
96
+ }
97
+ `;
98
+ expectShaderEqual(result, expected);
99
+ });
100
+
101
+ it('generates correct shader with a single selected feature', () => {
102
+ const result = getSpatialLayerColorWithSelectionShader(
103
+ [0, 255, 0], 1.0, [10], [0, 0, 0], 'feat_idx',
104
+ );
105
+ const expected = `
106
+ void main() {
107
+ int geneIndex = prop_feat_idx();
108
+ int selectedIndices[1] = int[1](10);
109
+ bool isSelected = false;
110
+ for (int i = 0; i < 1; ++i) {
111
+ if (geneIndex == selectedIndices[i]) {
112
+ isSelected = true;
113
+ }
114
+ }
115
+ if (isSelected) {
116
+ setColor(vec4(0, 1, 0, 1));
117
+ } else {
118
+ setColor(vec4(0, 0, 0, 1));
119
+ }
120
+ }
121
+ `;
122
+ expectShaderEqual(result, expected);
123
+ });
124
+ });
125
+
126
+ describe('getSpatialLayerColorFilteredShader', () => {
127
+ it('generates a shader that discards unselected points', () => {
128
+ const result = getSpatialLayerColorFilteredShader(
129
+ [0, 0, 255], 0.9, [3, 7, 11], 'gene_index',
130
+ );
131
+ const expected = `
132
+ void main() {
133
+ int geneIndex = prop_gene_index();
134
+ int selectedIndices[3] = int[3](3, 7, 11);
135
+ bool isSelected = false;
136
+ for (int i = 0; i < 3; ++i) {
137
+ if (geneIndex == selectedIndices[i]) {
138
+ isSelected = true;
139
+ }
140
+ }
141
+ if (!isSelected) {
142
+ discard;
143
+ }
144
+ setColor(vec4(0, 0, 1, 0.9));
145
+ }
146
+ `;
147
+ expectShaderEqual(result, expected);
148
+ });
149
+ });
150
+
151
+ // ============================================================
152
+ // Case 2: geneSelection
153
+ // ============================================================
154
+
155
+ describe('getGeneSelectionNoSelectionShader', () => {
156
+ it('generates a shader identical to spatialLayerColor (static color for all)', () => {
157
+ const result = getGeneSelectionNoSelectionShader([100, 200, 50], 0.7);
158
+ const expected = `
159
+ void main() {
160
+ setColor(vec4(0.39215686274509803, 0.7843137254901961, 0.19607843137254902, 0.7));
161
+ }
162
+ `;
163
+ expectShaderEqual(result, expected);
164
+ });
165
+ });
166
+
167
+ describe('getGeneSelectionWithSelectionShader', () => {
168
+ it('generates a shader with per-feature colors for selected and default for unselected', () => {
169
+ const result = getGeneSelectionWithSelectionShader(
170
+ [1, 4],
171
+ [[255, 0, 0], [0, 255, 0]],
172
+ [128, 128, 128],
173
+ [50, 50, 50],
174
+ 0.6,
175
+ 'gene_index',
176
+ );
177
+ const expected = `
178
+ void main() {
179
+ int geneIndex = prop_gene_index();
180
+ int selectedIndices[2] = int[2](1, 4);
181
+ vec3 featureColors[2] = vec3[2](vec3(1, 0, 0), vec3(0, 1, 0));
182
+ vec4 color = vec4(0.19607843137254902, 0.19607843137254902, 0.19607843137254902, 0.6);
183
+ for (int i = 0; i < 2; ++i) {
184
+ if (geneIndex == selectedIndices[i]) {
185
+ color = vec4(featureColors[i], 0.6);
186
+ }
187
+ }
188
+ setColor(color);
189
+ }
190
+ `;
191
+ expectShaderEqual(result, expected);
192
+ });
193
+
194
+ it('uses static color as fallback when feature color is falsy', () => {
195
+ // The code uses `c ? toVec3(c) : toVec3(normStatic)`.
196
+ // normalizeColor always returns an array (truthy), so the fallback
197
+ // only triggers if the original color was falsy before normalization.
198
+ // Since featureColors are always normalized, we just check the normal path.
199
+ const result = getGeneSelectionWithSelectionShader(
200
+ [0],
201
+ [[0, 0, 255]],
202
+ [255, 255, 255],
203
+ [0, 0, 0],
204
+ 1.0,
205
+ 'fi',
206
+ );
207
+ const expected = `
208
+ void main() {
209
+ int geneIndex = prop_fi();
210
+ int selectedIndices[1] = int[1](0);
211
+ vec3 featureColors[1] = vec3[1](vec3(0, 0, 1));
212
+ vec4 color = vec4(0, 0, 0, 1);
213
+ for (int i = 0; i < 1; ++i) {
214
+ if (geneIndex == selectedIndices[i]) {
215
+ color = vec4(featureColors[i], 1);
216
+ }
217
+ }
218
+ setColor(color);
219
+ }
220
+ `;
221
+ expectShaderEqual(result, expected);
222
+ });
223
+ });
224
+
225
+ describe('getGeneSelectionFilteredShader', () => {
226
+ it('generates a shader that discards unselected and colors selected per-feature', () => {
227
+ const result = getGeneSelectionFilteredShader(
228
+ [2, 8],
229
+ [[255, 0, 0], [0, 0, 255]],
230
+ [128, 128, 128],
231
+ 0.75,
232
+ 'gene_index',
233
+ );
234
+ const expected = `
235
+ void main() {
236
+ int geneIndex = prop_gene_index();
237
+ int selectedIndices[2] = int[2](2, 8);
238
+ vec3 featureColors[2] = vec3[2](vec3(1, 0, 0), vec3(0, 0, 1));
239
+ bool isSelected = false;
240
+ vec3 matchedColor = vec3(0.0);
241
+ for (int i = 0; i < 2; ++i) {
242
+ if (geneIndex == selectedIndices[i]) {
243
+ isSelected = true;
244
+ matchedColor = featureColors[i];
245
+ }
246
+ }
247
+ if (!isSelected) {
248
+ discard;
249
+ }
250
+ setColor(vec4(matchedColor, 0.75));
251
+ }
252
+ `;
253
+ expectShaderEqual(result, expected);
254
+ });
255
+ });
256
+
257
+ // ============================================================
258
+ // Case 3: randomByFeature
259
+ // ============================================================
260
+
261
+ describe('getRandomByFeatureShader', () => {
262
+ it('generates a shader using the palette to color by feature index', () => {
263
+ const result = getRandomByFeatureShader(0.5, 'gene_index');
264
+ const expected = `
265
+ void main() {
266
+ int geneIndex = prop_gene_index();
267
+ vec3 palette[3] = vec3[3](vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1));
268
+ int colorIdx = geneIndex - (geneIndex / 3) * 3;
269
+ if (colorIdx < 0) { colorIdx = -colorIdx; }
270
+ vec3 color = palette[colorIdx];
271
+ setColor(vec4(color, 0.5));
272
+ }
273
+ `;
274
+ expectShaderEqual(result, expected);
275
+ });
276
+ });
277
+
278
+ describe('getRandomByFeatureWithSelectionShader', () => {
279
+ it('generates a shader with palette colors for selected and default for unselected', () => {
280
+ const result = getRandomByFeatureWithSelectionShader(
281
+ [1, 2], [50, 50, 50], 0.8, 'gene_index',
282
+ );
283
+ const expected = `
284
+ void main() {
285
+ int geneIndex = prop_gene_index();
286
+ vec3 palette[3] = vec3[3](vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1));
287
+ int selectedIndices[2] = int[2](1, 2);
288
+ bool isSelected = false;
289
+ for (int i = 0; i < 2; ++i) {
290
+ if (geneIndex == selectedIndices[i]) {
291
+ isSelected = true;
292
+ }
293
+ }
294
+ if (isSelected) {
295
+ int colorIdx = geneIndex - (geneIndex / 3) * 3;
296
+ if (colorIdx < 0) { colorIdx = -colorIdx; }
297
+ setColor(vec4(palette[colorIdx], 0.8));
298
+ } else {
299
+ setColor(vec4(0.19607843137254902, 0.19607843137254902, 0.19607843137254902, 0.8));
300
+ }
301
+ }
302
+ `;
303
+ expectShaderEqual(result, expected);
304
+ });
305
+ });
306
+
307
+ describe('getRandomByFeatureFilteredShader', () => {
308
+ it('generates a shader that discards unselected and uses palette for selected', () => {
309
+ const result = getRandomByFeatureFilteredShader([0], 1.0, 'gene_index');
310
+ const expected = `
311
+ void main() {
312
+ int geneIndex = prop_gene_index();
313
+ vec3 palette[3] = vec3[3](vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1));
314
+ int selectedIndices[1] = int[1](0);
315
+ bool isSelected = false;
316
+ for (int i = 0; i < 1; ++i) {
317
+ if (geneIndex == selectedIndices[i]) {
318
+ isSelected = true;
319
+ }
320
+ }
321
+ if (!isSelected) {
322
+ discard;
323
+ }
324
+ int colorIdx = geneIndex - (geneIndex / 3) * 3;
325
+ if (colorIdx < 0) { colorIdx = -colorIdx; }
326
+ setColor(vec4(palette[colorIdx], 1));
327
+ }
328
+ `;
329
+ expectShaderEqual(result, expected);
330
+ });
331
+ });
332
+
333
+ // ============================================================
334
+ // Case 4: random (per point)
335
+ // ============================================================
336
+
337
+ describe('getRandomPerPointShader', () => {
338
+ it('generates a shader with pseudo-random color per point', () => {
339
+ const result = getRandomPerPointShader(0.9, 'gene_index', 'point_index');
340
+ const expected = `
341
+ float hashToFloat(int v, int seed) {
342
+ int h = v ^ (seed * 16777619);
343
+ h = h * 747796405 + 2891336453;
344
+ h = ((h >> 16) ^ h) * 2654435769;
345
+ h = ((h >> 16) ^ h);
346
+ return float(h & 0x7FFFFFFF) / float(0x7FFFFFFF);
347
+ }
348
+ void main() {
349
+ int geneIndex = prop_gene_index();
350
+ int pointIndex = prop_point_index();
351
+ float r = hashToFloat(pointIndex, 0);
352
+ float g = hashToFloat(pointIndex, 1);
353
+ float b = hashToFloat(pointIndex, 2);
354
+ setColor(vec4(r, g, b, 0.9));
355
+ }
356
+ `;
357
+ expectShaderEqual(result, expected);
358
+ });
359
+ });
360
+
361
+ describe('getRandomPerPointWithSelectionShader', () => {
362
+ it('generates a shader with random colors for selected and default for unselected', () => {
363
+ const result = getRandomPerPointWithSelectionShader(
364
+ [3, 6], [100, 100, 100], 0.5, 'gene_index', 'point_index',
365
+ );
366
+ const expected = `
367
+ float hashToFloat(int v, int seed) {
368
+ int h = v ^ (seed * 16777619);
369
+ h = h * 747796405 + 2891336453;
370
+ h = ((h >> 16) ^ h) * 2654435769;
371
+ h = ((h >> 16) ^ h);
372
+ return float(h & 0x7FFFFFFF) / float(0x7FFFFFFF);
373
+ }
374
+ void main() {
375
+ int geneIndex = prop_gene_index();
376
+ int pointIndex = prop_point_index();
377
+ int selectedIndices[2] = int[2](3, 6);
378
+ bool isSelected = false;
379
+ for (int i = 0; i < 2; ++i) {
380
+ if (geneIndex == selectedIndices[i]) {
381
+ isSelected = true;
382
+ }
383
+ }
384
+ if (isSelected) {
385
+ float r = hashToFloat(pointIndex, 0);
386
+ float g = hashToFloat(pointIndex, 1);
387
+ float b = hashToFloat(pointIndex, 2);
388
+ setColor(vec4(r, g, b, 0.5));
389
+ } else {
390
+ setColor(vec4(0.39215686274509803, 0.39215686274509803, 0.39215686274509803, 0.5));
391
+ }
392
+ }
393
+ `;
394
+ expectShaderEqual(result, expected);
395
+ });
396
+ });
397
+
398
+ describe('getRandomPerPointFilteredShader', () => {
399
+ it('generates a shader that discards unselected and uses random color for selected', () => {
400
+ const result = getRandomPerPointFilteredShader(
401
+ [5], 1.0, 'gene_index', 'point_index',
402
+ );
403
+ const expected = `
404
+ float hashToFloat(int v, int seed) {
405
+ int h = v ^ (seed * 16777619);
406
+ h = h * 747796405 + 2891336453;
407
+ h = ((h >> 16) ^ h) * 2654435769;
408
+ h = ((h >> 16) ^ h);
409
+ return float(h & 0x7FFFFFFF) / float(0x7FFFFFFF);
410
+ }
411
+ void main() {
412
+ int geneIndex = prop_gene_index();
413
+ int pointIndex = prop_point_index();
414
+ int selectedIndices[1] = int[1](5);
415
+ bool isSelected = false;
416
+ for (int i = 0; i < 1; ++i) {
417
+ if (geneIndex == selectedIndices[i]) {
418
+ isSelected = true;
419
+ }
420
+ }
421
+ if (!isSelected) {
422
+ discard;
423
+ }
424
+ float r = hashToFloat(pointIndex, 0);
425
+ float g = hashToFloat(pointIndex, 1);
426
+ float b = hashToFloat(pointIndex, 2);
427
+ setColor(vec4(r, g, b, 1));
428
+ }
429
+ `;
430
+ expectShaderEqual(result, expected);
431
+ });
432
+ });
@@ -0,0 +1,189 @@
1
+ /* eslint-disable no-unused-vars */
2
+ /* eslint-disable max-len */
3
+ import { useRef } from 'react';
4
+
5
+ /**
6
+ * A variant of useMemo that accepts a custom equality function
7
+ * to determine whether the dependencies have changed.
8
+ * @template T
9
+ * @param {() => T} factory - Function that computes the memoized value.
10
+ * @param {any} dependencies - The useMemo dependencies.
11
+ * @param {(prevDeps: any, nextDeps: any) => boolean} customIsEqual - Custom equality function,
12
+ * which receives the previous and next dependencies and returns true if they are considered equal.
13
+ * @returns {T} The memoized value.
14
+ */
15
+ export function useMemoCustomComparison(factory, dependencies, customIsEqual) {
16
+ const ref = useRef(/** @type {{ deps: any; value: T } | undefined} */ (undefined));
17
+
18
+ if (ref.current === undefined || !customIsEqual(ref.current.deps, dependencies)) {
19
+ ref.current = { deps: dependencies, value: factory() };
20
+ }
21
+
22
+ return ref.current.value;
23
+ }
24
+
25
+
26
+ // Comparison utilties inspired by componentDidUpdate in spatial-beta/Spatial.js:
27
+ const shallowDiff = (prevDeps, nextDeps, depName) => prevDeps[depName] !== nextDeps[depName];
28
+ const shallowDiffByLayer = (prevDeps, nextDeps, depName, scopeName) => (
29
+ prevDeps?.[depName]?.[scopeName] !== nextDeps?.[depName]?.[scopeName]
30
+ );
31
+ // Rather than checking equality of the entire object,
32
+ // here, we only shallowly compare the specific properties that are relevant.
33
+ const shallowDiffByLayerWithKeys = (prevDeps, nextDeps, depName, scopeName, keys) => keys.some(
34
+ k => (prevDeps?.[depName]?.[scopeName]?.[k] !== nextDeps?.[depName]?.[scopeName]?.[k]),
35
+ );
36
+ const shallowDiffByChannel = (prevDeps, nextDeps, depName, firstName, secondName) => (
37
+ prevDeps?.[depName]?.[firstName]?.[secondName]
38
+ !== nextDeps?.[depName]?.[firstName]?.[secondName]
39
+ );
40
+ const shallowDiffByChannelWithKeys = (prevDeps, nextDeps, depName, firstName, secondName, keys) => keys.some(
41
+ k => (
42
+ prevDeps?.[depName]?.[firstName]?.[secondName]?.[k]
43
+ !== nextDeps?.[depName]?.[firstName]?.[secondName]?.[k]
44
+ ),
45
+ );
46
+ const shallowDiffByLayerCoordination = (prevDeps, nextDeps, depName, layerScope) => (
47
+ prevDeps?.[depName]?.[0]?.[layerScope]
48
+ !== nextDeps?.[depName]?.[0]?.[layerScope]
49
+ );
50
+ const shallowDiffByLayerCoordinationWithKeys = (prevDeps, nextDeps, depName, layerScope, keys) => keys.some(
51
+ k => prevDeps?.[depName]?.[0]?.[layerScope]?.[k]
52
+ !== nextDeps?.[depName]?.[0]?.[layerScope]?.[k],
53
+ );
54
+ const shallowDiffByChannelCoordination = (prevDeps, nextDeps, depName, layerScope, channelScope) => (
55
+ prevDeps?.[depName]?.[0]?.[layerScope]?.[channelScope]
56
+ !== nextDeps?.[depName]?.[0]?.[layerScope]?.[channelScope]
57
+ );
58
+ const shallowDiffByChannelCoordinationWithKeys = (prevDeps, nextDeps, depName, layerScope, channelScope, keys) => keys.some(
59
+ k => prevDeps?.[depName]?.[0]?.[layerScope]?.[channelScope]?.[k]
60
+ !== nextDeps?.[depName]?.[0]?.[layerScope]?.[channelScope]?.[k],
61
+ );
62
+
63
+ // We need a custom equality function, to handle the nested nature of the dependencies.
64
+ // We only want to trigger a re-render if the list of layers/channels themselves changed,
65
+ // or if the nested properties (but only those that the colors rely on) for any of them changed.
66
+ // Note: if additional properties become relevant for determining cell colors,
67
+ // this function will need to be updated to stay in sync with that logic.
68
+ export function customIsEqualForCellColors(prevDeps, nextDeps) {
69
+ let forceUpdate = false;
70
+
71
+ // We create curried variants so we don't have to constantly pass prevDeps and nextDeps.
72
+ const curriedShallowDiff = depName => shallowDiff(prevDeps, nextDeps, depName);
73
+ const curriedShallowDiffByLayer = (depName, scopeName) => shallowDiffByLayer(prevDeps, nextDeps, depName, scopeName);
74
+ const curriedShallowDiffByChannel = (depName, firstName, secondName) => shallowDiffByChannel(prevDeps, nextDeps, depName, firstName, secondName);
75
+ const curriedShallowDiffByChannelWithKeys = (depName, firstName, secondName, keys) => shallowDiffByChannelWithKeys(prevDeps, nextDeps, depName, firstName, secondName, keys);
76
+ const curriedShallowDiffByLayerCoordination = (depName, layerScope) => shallowDiffByLayerCoordination(prevDeps, nextDeps, depName, layerScope);
77
+ const curriedShallowDiffByLayerCoordinationWithKeys = (depName, layerScope, keys) => shallowDiffByLayerCoordinationWithKeys(prevDeps, nextDeps, depName, layerScope, keys);
78
+ const curriedShallowDiffByChannelCoordination = (depName, layerScope, channelScope) => shallowDiffByChannelCoordination(prevDeps, nextDeps, depName, layerScope, channelScope);
79
+ const curriedShallowDiffByChannelCoordinationWithKeys = (depName, layerScope, channelScope, keys) => shallowDiffByChannelCoordinationWithKeys(prevDeps, nextDeps, depName, layerScope, channelScope, keys);
80
+
81
+ // Check if the theme changed, which could change the cell colors even if the underlying data didn't change.
82
+ if (curriedShallowDiff('theme')) {
83
+ forceUpdate = true;
84
+ }
85
+
86
+ // Segmentation sets data.
87
+ if (['segmentationLayerScopes', 'segmentationChannelScopesByLayer'].some(curriedShallowDiff)) {
88
+ // Force update for all layers since the layerScopes array changed.
89
+ forceUpdate = true;
90
+ } else {
91
+ // Iterate over layers and channels.
92
+ nextDeps.segmentationLayerScopes?.forEach((layerScope) => {
93
+ nextDeps.segmentationChannelScopesByLayer?.[layerScope]?.forEach((channelScope) => {
94
+ if (
95
+ curriedShallowDiffByChannelWithKeys('obsSegmentationsSetsData', layerScope, channelScope, [
96
+ 'obsSets', 'obsIndex',
97
+ ])
98
+ || curriedShallowDiffByChannelCoordinationWithKeys('segmentationChannelCoordination', layerScope, channelScope, [
99
+ 'obsSetColor',
100
+ 'obsColorEncoding',
101
+ 'obsSetSelection',
102
+ 'additionalObsSets',
103
+ ])
104
+ ) {
105
+ forceUpdate = true;
106
+ }
107
+ });
108
+ });
109
+ }
110
+
111
+ // Return "isEqual" value.
112
+ // (If forceUpdate is true, then isEqual should be false to trigger a re-render.)
113
+ return !forceUpdate;
114
+ }
115
+
116
+ export function customIsEqualForInitialViewerState(prevDeps, nextDeps) {
117
+ let forceUpdate = false;
118
+
119
+ // We create curried variants so we don't have to constantly pass prevDeps and nextDeps.
120
+ const curriedShallowDiff = depName => shallowDiff(prevDeps, nextDeps, depName);
121
+ const curriedShallowDiffByLayer = (depName, scopeName) => shallowDiffByLayer(prevDeps, nextDeps, depName, scopeName);
122
+ const curriedShallowDiffByLayerWithKeys = (depName, scopeName, keys) => shallowDiffByLayerWithKeys(prevDeps, nextDeps, depName, scopeName, keys);
123
+ const curriedShallowDiffByChannel = (depName, firstName, secondName) => shallowDiffByChannel(prevDeps, nextDeps, depName, firstName, secondName);
124
+ const curriedShallowDiffByChannelWithKeys = (depName, firstName, secondName, keys) => shallowDiffByChannelWithKeys(prevDeps, nextDeps, depName, firstName, secondName, keys);
125
+ const curriedShallowDiffByLayerCoordination = (depName, layerScope) => shallowDiffByLayerCoordination(prevDeps, nextDeps, depName, layerScope);
126
+ const curriedShallowDiffByLayerCoordinationWithKeys = (depName, layerScope, keys) => shallowDiffByLayerCoordinationWithKeys(prevDeps, nextDeps, depName, layerScope, keys);
127
+ const curriedShallowDiffByChannelCoordination = (depName, layerScope, channelScope) => shallowDiffByChannelCoordination(prevDeps, nextDeps, depName, layerScope, channelScope);
128
+ const curriedShallowDiffByChannelCoordinationWithKeys = (depName, layerScope, channelScope, keys) => shallowDiffByChannelCoordinationWithKeys(prevDeps, nextDeps, depName, layerScope, channelScope, keys);
129
+
130
+ // Segmentation layers/channels.
131
+ if (['segmentationLayerScopes', 'segmentationChannelScopesByLayer'].some(curriedShallowDiff)) {
132
+ // Force update for all layers since the layerScopes array changed.
133
+ forceUpdate = true;
134
+ } else {
135
+ // Iterate over layers and channels.
136
+ nextDeps.segmentationLayerScopes?.forEach((layerScope) => {
137
+ if (
138
+ curriedShallowDiffByLayer('obsSegmentationsData', layerScope)
139
+ || curriedShallowDiffByLayerCoordinationWithKeys('segmentationLayerCoordination', layerScope, [
140
+ 'spatialLayerVisible',
141
+ ])
142
+ ) {
143
+ forceUpdate = true;
144
+ }
145
+ nextDeps.segmentationChannelScopesByLayer?.[layerScope]?.forEach((channelScope) => {
146
+ if (
147
+ curriedShallowDiffByChannelCoordinationWithKeys('segmentationChannelCoordination', layerScope, channelScope, [
148
+ 'spatialChannelVisible',
149
+ ])
150
+ ) {
151
+ forceUpdate = true;
152
+ }
153
+ });
154
+ });
155
+ }
156
+
157
+ // Point layers.
158
+ if (curriedShallowDiff('pointLayerScopes')) {
159
+ // Force update for all layers since the layerScopes array changed.
160
+ forceUpdate = true;
161
+ } else {
162
+ // Iterate over layers and channels.
163
+ nextDeps.pointLayerScopes?.forEach((layerScope) => {
164
+ if (
165
+ curriedShallowDiffByLayer('obsPointsData', layerScope)
166
+ || curriedShallowDiffByLayer('pointMultiIndicesData', layerScope)
167
+ || curriedShallowDiffByLayerCoordinationWithKeys('pointLayerCoordination', layerScope, [
168
+ 'spatialLayerVisible',
169
+ 'obsColorEncoding',
170
+ 'spatialLayerColor',
171
+ 'featureSelection',
172
+ 'featureFilterMode',
173
+ 'featureColor',
174
+ ])
175
+ // For opacity, use an epsilon comparison to avoid too many re-renders, as it affects performance.
176
+ || (
177
+ Math.abs(prevDeps?.pointLayerCoordination?.[0]?.[layerScope]?.spatialLayerOpacity - nextDeps?.pointLayerCoordination?.[0]?.[layerScope]?.spatialLayerOpacity)
178
+ >= 0.05
179
+ )
180
+ ) {
181
+ forceUpdate = true;
182
+ }
183
+ });
184
+ }
185
+
186
+ // Return "isEqual" value.
187
+ // (If forceUpdate is true, then isEqual should be false to trigger a re-render.)
188
+ return !forceUpdate;
189
+ }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=data-hook-ng-utils.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"data-hook-ng-utils.test.d.ts","sourceRoot":"","sources":["../src/data-hook-ng-utils.test.js"],"names":[],"mappings":""}
@@ -1,35 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { extractDataTypeEntities, DEFAULT_NG_PROPS, } from './data-hook-ng-utils.js';
3
- describe('extractDataTypeEntities (minimal tests)', () => {
4
- it('returns empty array when internMap is missing or invalid', () => {
5
- expect(extractDataTypeEntities({}, 'A', 'obsSegmentations')).toEqual([]);
6
- expect(extractDataTypeEntities({ A: { loaders: {} } }, 'A', 'obsSegmentations')).toEqual([]);
7
- expect(extractDataTypeEntities({ A: { loaders: { obsSegmentations: {} } } }, 'A', 'obsSegmentations')).toEqual([]);
8
- });
9
- it('builds an entity for a precomputed loader and applies sane defaults', () => {
10
- const key = { fileUid: 'melanoma-meshes' };
11
- const loader = {
12
- fileType: 'obsSegmentations.ng-precomputed',
13
- url: 'https://www.example.com/example/example_meshes',
14
- options: { projectionScale: 2048 },
15
- };
16
- const internMap = new Map([[key, loader]]);
17
- const loaders = { A: { loaders: { obsSegmentations: internMap } } };
18
- const out = extractDataTypeEntities(loaders, 'A', 'obsSegmentations');
19
- expect(out).toHaveLength(1);
20
- const e = out[0];
21
- expect(e.key).toBe(key);
22
- expect(e.type).toBe('segmentation');
23
- expect(e.fileUid).toBe('melanoma-meshes');
24
- expect(e.layout).toBe(DEFAULT_NG_PROPS.layout);
25
- // URL + source prefixing
26
- expect(e.url).toBe(loader.url);
27
- expect(e.source).toBe('precomputed://https://www.example.com/example/example_meshes');
28
- expect(e.dimensions).toEqual({ x: [1, 'nm'], y: [1, 'nm'], z: [1, 'nm'] });
29
- // camera defaults + single override
30
- expect(e.position).toEqual(DEFAULT_NG_PROPS.position);
31
- expect(e.projectionOrientation).toEqual(DEFAULT_NG_PROPS.projectionOrientation);
32
- expect(e.projectionScale).toBe(2048);
33
- expect(e.crossSectionScale).toBe(DEFAULT_NG_PROPS.crossSectionScale);
34
- });
35
- });