@niivue/nvreact 0.1.0 → 0.1.2
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/LICENSE +24 -0
- package/README.md +4 -0
- package/dist/lib.js +127 -18
- package/dist/types/nvscene-controller.d.ts +14 -0
- package/dist/types/types.d.ts +3 -0
- package/package.json +7 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, hanayik
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
19
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
23
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
24
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
CHANGED
|
@@ -95,6 +95,10 @@ useSceneEvent(scene, "viewerCreated", (nv, index) => {
|
|
|
95
95
|
useSceneEvent(scene, "imageLoaded", (viewerIndex, volume) => {
|
|
96
96
|
console.log(`loaded ${volume.name} in viewer ${viewerIndex}`);
|
|
97
97
|
});
|
|
98
|
+
|
|
99
|
+
useSceneEvent(scene, "locationChange", (viewerIndex, data) => {
|
|
100
|
+
console.log(`viewer ${viewerIndex}: ${data.string}`);
|
|
101
|
+
});
|
|
98
102
|
```
|
|
99
103
|
|
|
100
104
|
#### Events
|
package/dist/lib.js
CHANGED
|
@@ -324,21 +324,43 @@ class NvSceneController {
|
|
|
324
324
|
forEachNiivue(callback) {
|
|
325
325
|
this.viewers.forEach((viewer, i) => callback(viewer.niivue, i));
|
|
326
326
|
}
|
|
327
|
+
hasSyncableContent(nv) {
|
|
328
|
+
return nv.volumes.length > 0 || nv.meshes.length > 0;
|
|
329
|
+
}
|
|
330
|
+
safeBroadcastTo(index, targets) {
|
|
331
|
+
const viewer = this.viewers[index];
|
|
332
|
+
if (!viewer)
|
|
333
|
+
return;
|
|
334
|
+
try {
|
|
335
|
+
viewer.niivue.broadcastTo(targets, this.broadcastOptions);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
this.addError(viewer.id, err);
|
|
338
|
+
this.emit("error", index, err);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
rewireBroadcasting() {
|
|
342
|
+
if (!this.broadcasting) {
|
|
343
|
+
this.viewers.forEach((_viewer, index) => {
|
|
344
|
+
this.safeBroadcastTo(index, []);
|
|
345
|
+
});
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const syncableViewers = this.viewers.filter((viewer) => this.hasSyncableContent(viewer.niivue));
|
|
349
|
+
this.viewers.forEach((viewer, index) => {
|
|
350
|
+
if (!this.hasSyncableContent(viewer.niivue)) {
|
|
351
|
+
this.safeBroadcastTo(index, []);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const others = syncableViewers.filter((v) => v.id !== viewer.id).map((v) => v.niivue);
|
|
355
|
+
this.safeBroadcastTo(index, others);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
327
358
|
setBroadcasting(enabled, options) {
|
|
328
359
|
this.broadcasting = enabled;
|
|
329
360
|
if (options) {
|
|
330
361
|
this.broadcastOptions = { ...this.broadcastOptions, ...options };
|
|
331
362
|
}
|
|
332
|
-
|
|
333
|
-
this.viewers.forEach((viewer) => {
|
|
334
|
-
const others = this.viewers.filter((v) => v.id !== viewer.id).map((v) => v.niivue);
|
|
335
|
-
viewer.niivue.broadcastTo(others, this.broadcastOptions);
|
|
336
|
-
});
|
|
337
|
-
} else {
|
|
338
|
-
this.viewers.forEach((viewer) => {
|
|
339
|
-
viewer.niivue.broadcastTo([], this.broadcastOptions);
|
|
340
|
-
});
|
|
341
|
-
}
|
|
363
|
+
this.rewireBroadcasting();
|
|
342
364
|
this.notify();
|
|
343
365
|
}
|
|
344
366
|
isBroadcasting() {
|
|
@@ -381,6 +403,9 @@ class NvSceneController {
|
|
|
381
403
|
try {
|
|
382
404
|
const image = await viewer.niivue.addVolumeFromUrl(opts);
|
|
383
405
|
this.emit("volumeAdded", index, opts, image);
|
|
406
|
+
if (this.broadcasting) {
|
|
407
|
+
this.rewireBroadcasting();
|
|
408
|
+
}
|
|
384
409
|
return image;
|
|
385
410
|
} catch (err) {
|
|
386
411
|
this.addError(viewer.id, err);
|
|
@@ -406,9 +431,49 @@ class NvSceneController {
|
|
|
406
431
|
if (vol) {
|
|
407
432
|
nv.removeVolume(vol);
|
|
408
433
|
this.emit("volumeRemoved", index, url);
|
|
434
|
+
if (this.broadcasting) {
|
|
435
|
+
this.rewireBroadcasting();
|
|
436
|
+
}
|
|
409
437
|
this.notify();
|
|
410
438
|
}
|
|
411
439
|
}
|
|
440
|
+
findVolume(viewerIndex, volumeIndex) {
|
|
441
|
+
const viewer = this.viewers[viewerIndex];
|
|
442
|
+
if (!viewer)
|
|
443
|
+
return;
|
|
444
|
+
const nv = viewer.niivue;
|
|
445
|
+
const vol = nv.volumes[volumeIndex];
|
|
446
|
+
if (!vol)
|
|
447
|
+
return;
|
|
448
|
+
return { nv, vol, volumeIndex };
|
|
449
|
+
}
|
|
450
|
+
setColormap(viewerIndex, volumeIndex, colormap) {
|
|
451
|
+
const found = this.findVolume(viewerIndex, volumeIndex);
|
|
452
|
+
if (!found)
|
|
453
|
+
return;
|
|
454
|
+
found.vol.colormap = colormap;
|
|
455
|
+
found.nv.updateGLVolume();
|
|
456
|
+
this.emit("colormapChanged", viewerIndex, volumeIndex, colormap);
|
|
457
|
+
this.notify();
|
|
458
|
+
}
|
|
459
|
+
setCalMinMax(viewerIndex, volumeIndex, cal_min, cal_max) {
|
|
460
|
+
const found = this.findVolume(viewerIndex, volumeIndex);
|
|
461
|
+
if (!found)
|
|
462
|
+
return;
|
|
463
|
+
found.vol.cal_min = cal_min;
|
|
464
|
+
found.vol.cal_max = cal_max;
|
|
465
|
+
found.nv.updateGLVolume();
|
|
466
|
+
this.emit("intensityChanged", viewerIndex, volumeIndex, cal_min, cal_max);
|
|
467
|
+
this.notify();
|
|
468
|
+
}
|
|
469
|
+
setOpacity(viewerIndex, volumeIndex, opacity) {
|
|
470
|
+
const found = this.findVolume(viewerIndex, volumeIndex);
|
|
471
|
+
if (!found)
|
|
472
|
+
return;
|
|
473
|
+
found.nv.setOpacity(volumeIndex, opacity);
|
|
474
|
+
this.emit("opacityChanged", viewerIndex, volumeIndex, opacity);
|
|
475
|
+
this.notify();
|
|
476
|
+
}
|
|
412
477
|
incrementLoading(id) {
|
|
413
478
|
this.loadingCounts.set(id, (this.loadingCounts.get(id) ?? 0) + 1);
|
|
414
479
|
this.notify();
|
|
@@ -464,6 +529,14 @@ class NvSceneController {
|
|
|
464
529
|
};
|
|
465
530
|
niivue.onImageLoaded = (vol) => {
|
|
466
531
|
this.emit("imageLoaded", index, vol);
|
|
532
|
+
if (this.broadcasting) {
|
|
533
|
+
this.rewireBroadcasting();
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
niivue.onMeshLoaded = (_mesh) => {
|
|
537
|
+
if (this.broadcasting) {
|
|
538
|
+
this.rewireBroadcasting();
|
|
539
|
+
}
|
|
467
540
|
};
|
|
468
541
|
if (this.broadcasting) {
|
|
469
542
|
this.setBroadcasting(true);
|
|
@@ -530,6 +603,14 @@ class NvSceneController {
|
|
|
530
603
|
|
|
531
604
|
// src/nvviewer.tsx
|
|
532
605
|
import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
|
|
606
|
+
function extractVisualProps(opts) {
|
|
607
|
+
return {
|
|
608
|
+
colormap: opts.colormap ?? opts.colorMap,
|
|
609
|
+
cal_min: opts.cal_min,
|
|
610
|
+
cal_max: opts.cal_max,
|
|
611
|
+
opacity: opts.opacity
|
|
612
|
+
};
|
|
613
|
+
}
|
|
533
614
|
var NvViewer = ({
|
|
534
615
|
volumes,
|
|
535
616
|
options,
|
|
@@ -542,7 +623,7 @@ var NvViewer = ({
|
|
|
542
623
|
}) => {
|
|
543
624
|
const containerRef = useRef2(null);
|
|
544
625
|
const nvRef = useRef2(null);
|
|
545
|
-
const
|
|
626
|
+
const loadedVolumesRef = useRef2(new Map);
|
|
546
627
|
const onLocationChangeRef = useRef2(onLocationChange);
|
|
547
628
|
onLocationChangeRef.current = onLocationChange;
|
|
548
629
|
const onImageLoadedRef = useRef2(onImageLoaded);
|
|
@@ -592,7 +673,7 @@ var NvViewer = ({
|
|
|
592
673
|
canvas.height = 0;
|
|
593
674
|
canvas.remove();
|
|
594
675
|
nvRef.current = null;
|
|
595
|
-
|
|
676
|
+
loadedVolumesRef.current.clear();
|
|
596
677
|
};
|
|
597
678
|
}, []);
|
|
598
679
|
useEffect2(() => {
|
|
@@ -603,24 +684,52 @@ var NvViewer = ({
|
|
|
603
684
|
if (!nv)
|
|
604
685
|
return;
|
|
605
686
|
const desiredUrls = new Set((volumes ?? []).map((v) => v.url));
|
|
606
|
-
const
|
|
607
|
-
for (const url of
|
|
687
|
+
const currentVolumes = loadedVolumesRef.current;
|
|
688
|
+
for (const url of currentVolumes.keys()) {
|
|
608
689
|
if (!desiredUrls.has(url)) {
|
|
609
690
|
const vol = nv.volumes.find((v) => v.url === url || v.name === url);
|
|
610
691
|
if (vol)
|
|
611
692
|
nv.removeVolume(vol);
|
|
612
|
-
|
|
693
|
+
currentVolumes.delete(url);
|
|
613
694
|
}
|
|
614
695
|
}
|
|
696
|
+
let needsGLUpdate = false;
|
|
615
697
|
for (const opts of volumes ?? []) {
|
|
616
|
-
if (!
|
|
617
|
-
|
|
698
|
+
if (!currentVolumes.has(opts.url)) {
|
|
699
|
+
const props = extractVisualProps(opts);
|
|
700
|
+
currentVolumes.set(opts.url, props);
|
|
618
701
|
nv.addVolumeFromUrl(opts).catch((err) => {
|
|
619
|
-
|
|
702
|
+
currentVolumes.delete(opts.url);
|
|
620
703
|
onErrorRef.current?.(err);
|
|
621
704
|
});
|
|
705
|
+
} else {
|
|
706
|
+
const prev = currentVolumes.get(opts.url);
|
|
707
|
+
const next = extractVisualProps(opts);
|
|
708
|
+
const vol = nv.volumes.find((v) => v.url === opts.url || v.name === opts.url);
|
|
709
|
+
if (!vol)
|
|
710
|
+
continue;
|
|
711
|
+
if (next.colormap !== undefined && next.colormap !== prev.colormap) {
|
|
712
|
+
vol.colormap = next.colormap;
|
|
713
|
+
needsGLUpdate = true;
|
|
714
|
+
}
|
|
715
|
+
if (next.cal_min !== undefined && next.cal_min !== prev.cal_min) {
|
|
716
|
+
vol.cal_min = next.cal_min;
|
|
717
|
+
needsGLUpdate = true;
|
|
718
|
+
}
|
|
719
|
+
if (next.cal_max !== undefined && next.cal_max !== prev.cal_max) {
|
|
720
|
+
vol.cal_max = next.cal_max;
|
|
721
|
+
needsGLUpdate = true;
|
|
722
|
+
}
|
|
723
|
+
if (next.opacity !== undefined && next.opacity !== prev.opacity) {
|
|
724
|
+
const volIdx = nv.volumes.indexOf(vol);
|
|
725
|
+
nv.setOpacity(volIdx, next.opacity);
|
|
726
|
+
}
|
|
727
|
+
currentVolumes.set(opts.url, next);
|
|
622
728
|
}
|
|
623
729
|
}
|
|
730
|
+
if (needsGLUpdate) {
|
|
731
|
+
nv.updateGLVolume();
|
|
732
|
+
}
|
|
624
733
|
}, [volumes]);
|
|
625
734
|
return /* @__PURE__ */ jsxDEV2("div", {
|
|
626
735
|
ref: containerRef,
|
|
@@ -108,6 +108,9 @@ export declare class NvSceneController {
|
|
|
108
108
|
getNiivue(index: number): Niivue | undefined;
|
|
109
109
|
getAllNiivue(): Niivue[];
|
|
110
110
|
forEachNiivue(callback: NiivueCallback): void;
|
|
111
|
+
private hasSyncableContent;
|
|
112
|
+
private safeBroadcastTo;
|
|
113
|
+
private rewireBroadcasting;
|
|
111
114
|
setBroadcasting(enabled: boolean, options?: Partial<BroadcastOptions>): void;
|
|
112
115
|
isBroadcasting(): boolean;
|
|
113
116
|
setViewerSliceLayout(index: number, layout: SliceLayoutTile[] | null): void;
|
|
@@ -118,6 +121,17 @@ export declare class NvSceneController {
|
|
|
118
121
|
loadVolume(index: number, opts: ImageFromUrlOptions): Promise<NVImage>;
|
|
119
122
|
loadVolumes(index: number, opts: ImageFromUrlOptions[]): Promise<NVImage[]>;
|
|
120
123
|
removeVolume(index: number, url: string): void;
|
|
124
|
+
/**
|
|
125
|
+
* Look up the Niivue instance and NVImage at the given viewer and volume indices.
|
|
126
|
+
* Returns undefined if either index is out of bounds.
|
|
127
|
+
*/
|
|
128
|
+
private findVolume;
|
|
129
|
+
/** Set the colormap for a volume at the given viewer and volume index. */
|
|
130
|
+
setColormap(viewerIndex: number, volumeIndex: number, colormap: string): void;
|
|
131
|
+
/** Set the intensity range (cal_min / cal_max) for a volume at the given viewer and volume index. */
|
|
132
|
+
setCalMinMax(viewerIndex: number, volumeIndex: number, cal_min: number, cal_max: number): void;
|
|
133
|
+
/** Set the opacity for a volume at the given viewer and volume index. */
|
|
134
|
+
setOpacity(viewerIndex: number, volumeIndex: number, opacity: number): void;
|
|
121
135
|
private incrementLoading;
|
|
122
136
|
private decrementLoading;
|
|
123
137
|
private addError;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export interface NvSceneEventMap {
|
|
|
10
10
|
error: (viewerIndex: number, error: unknown) => void;
|
|
11
11
|
volumeAdded: (viewerIndex: number, imageOptions: ImageFromUrlOptions, image: NVImage) => void;
|
|
12
12
|
volumeRemoved: (viewerIndex: number, url: string) => void;
|
|
13
|
+
colormapChanged: (viewerIndex: number, volumeIndex: number, colormap: string) => void;
|
|
14
|
+
intensityChanged: (viewerIndex: number, volumeIndex: number, cal_min: number, cal_max: number) => void;
|
|
15
|
+
opacityChanged: (viewerIndex: number, volumeIndex: number, opacity: number) => void;
|
|
13
16
|
}
|
|
14
17
|
export interface ViewerState {
|
|
15
18
|
id: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@niivue/nvreact",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/lib.js",
|
|
@@ -31,12 +31,16 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@niivue/niivue": "0.
|
|
34
|
+
"@niivue/niivue": "^0.68.1",
|
|
35
35
|
"react": "^19",
|
|
36
36
|
"react-dom": "^19"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@
|
|
39
|
+
"@happy-dom/global-registrator": "^20.6.1",
|
|
40
|
+
"@niivue/niivue": "^0.68.1",
|
|
41
|
+
"@testing-library/dom": "^10.4.1",
|
|
42
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
43
|
+
"@testing-library/react": "^16.3.2",
|
|
40
44
|
"@types/bun": "latest",
|
|
41
45
|
"@types/react": "^19",
|
|
42
46
|
"@types/react-dom": "^19",
|