@niivue/nvreact 0.1.3 → 1.0.0-rc.4
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/README.md +1 -1
- package/dist/lib.js +195 -203
- package/dist/types/context.d.ts +2 -2
- package/dist/types/hooks.d.ts +6 -6
- package/dist/types/lib.d.ts +9 -9
- package/dist/types/nvscene-controller.d.ts +20 -25
- package/dist/types/nvscene.d.ts +2 -2
- package/dist/types/nvviewer.d.ts +4 -4
- package/dist/types/types.d.ts +4 -5
- package/package.json +8 -7
- package/dist/types/nvcontainer.d.ts +0 -10
package/README.md
CHANGED
package/dist/lib.js
CHANGED
|
@@ -1,52 +1,28 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
1
|
+
// src/context.tsx
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
|
-
var
|
|
4
|
+
var NvSceneContext = createContext(null);
|
|
5
|
+
function NvSceneProvider({
|
|
5
6
|
scene,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
if (containerRef.current) {
|
|
13
|
-
scene.setContainerElement(containerRef.current);
|
|
14
|
-
if (scene.viewers.length === 0) {
|
|
15
|
-
scene.setLayout(initialLayout);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return () => {
|
|
19
|
-
scene.setContainerElement(null);
|
|
20
|
-
};
|
|
21
|
-
}, [scene, initialLayout]);
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
const el = containerRef.current;
|
|
24
|
-
if (!el)
|
|
25
|
-
return;
|
|
26
|
-
const ro = new ResizeObserver(() => scene.updateLayout());
|
|
27
|
-
ro.observe(el);
|
|
28
|
-
return () => ro.disconnect();
|
|
29
|
-
}, [scene]);
|
|
30
|
-
return /* @__PURE__ */ jsx("div", {
|
|
31
|
-
ref: containerRef,
|
|
32
|
-
className,
|
|
33
|
-
style
|
|
7
|
+
children
|
|
8
|
+
}) {
|
|
9
|
+
return /* @__PURE__ */ jsx(NvSceneContext.Provider, {
|
|
10
|
+
value: scene,
|
|
11
|
+
children
|
|
34
12
|
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
13
|
+
}
|
|
14
|
+
function useSceneContext() {
|
|
15
|
+
const scene = useContext(NvSceneContext);
|
|
16
|
+
if (!scene) {
|
|
17
|
+
throw new Error("useSceneContext must be used within an NvSceneProvider");
|
|
18
|
+
}
|
|
19
|
+
return scene;
|
|
20
|
+
}
|
|
21
|
+
// src/hooks.ts
|
|
22
|
+
import { useEffect, useMemo, useRef, useSyncExternalStore } from "react";
|
|
42
23
|
|
|
43
24
|
// src/nvscene-controller.ts
|
|
44
|
-
import {
|
|
45
|
-
DRAG_MODE,
|
|
46
|
-
Niivue,
|
|
47
|
-
SLICE_TYPE,
|
|
48
|
-
SHOW_RENDER
|
|
49
|
-
} from "@niivue/niivue";
|
|
25
|
+
import NiiVueGPU, { DRAG_MODE, SHOW_RENDER, SLICE_TYPE } from "@niivue/niivue";
|
|
50
26
|
|
|
51
27
|
// src/layouts.ts
|
|
52
28
|
var layout1x1 = () => ({
|
|
@@ -197,14 +173,9 @@ var defaultSliceLayouts = {
|
|
|
197
173
|
}
|
|
198
174
|
};
|
|
199
175
|
var defaultViewerOptions = {
|
|
200
|
-
crosshairGap: 5
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
leftButton: {
|
|
204
|
-
primary: DRAG_MODE.crosshair
|
|
205
|
-
},
|
|
206
|
-
rightButton: DRAG_MODE.pan,
|
|
207
|
-
centerButton: DRAG_MODE.slicer3D
|
|
176
|
+
crosshairGap: 5,
|
|
177
|
+
primaryDragMode: DRAG_MODE.crosshair,
|
|
178
|
+
secondaryDragMode: DRAG_MODE.pan
|
|
208
179
|
};
|
|
209
180
|
|
|
210
181
|
class NvSceneController {
|
|
@@ -234,7 +205,7 @@ class NvSceneController {
|
|
|
234
205
|
if (!this.eventListeners.has(event)) {
|
|
235
206
|
this.eventListeners.set(event, new Set);
|
|
236
207
|
}
|
|
237
|
-
this.eventListeners.get(event)
|
|
208
|
+
this.eventListeners.get(event)?.add(cb);
|
|
238
209
|
return () => this.off(event, cb);
|
|
239
210
|
}
|
|
240
211
|
off(event, cb) {
|
|
@@ -272,7 +243,9 @@ class NvSceneController {
|
|
|
272
243
|
};
|
|
273
244
|
notify() {
|
|
274
245
|
this.snapshotCache = null;
|
|
275
|
-
this.listeners.forEach((listener) =>
|
|
246
|
+
this.listeners.forEach((listener) => {
|
|
247
|
+
listener();
|
|
248
|
+
});
|
|
276
249
|
}
|
|
277
250
|
setContainerElement(element) {
|
|
278
251
|
this.containerElement = element;
|
|
@@ -289,8 +262,10 @@ class NvSceneController {
|
|
|
289
262
|
while (this.viewers.length > this.slots) {
|
|
290
263
|
this.removeViewer(this.viewers.length - 1, false);
|
|
291
264
|
}
|
|
292
|
-
if (this.
|
|
293
|
-
this.
|
|
265
|
+
if (this.containerElement) {
|
|
266
|
+
while (this.viewers.length < this.slots) {
|
|
267
|
+
this.addViewer();
|
|
268
|
+
}
|
|
294
269
|
}
|
|
295
270
|
this.updateLayout();
|
|
296
271
|
this.notify();
|
|
@@ -322,7 +297,9 @@ class NvSceneController {
|
|
|
322
297
|
return this.viewers.map((viewer) => viewer.niivue);
|
|
323
298
|
}
|
|
324
299
|
forEachNiivue(callback) {
|
|
325
|
-
this.viewers.forEach((viewer, i) =>
|
|
300
|
+
this.viewers.forEach((viewer, i) => {
|
|
301
|
+
callback(viewer.niivue, i);
|
|
302
|
+
});
|
|
326
303
|
}
|
|
327
304
|
hasSyncableContent(nv) {
|
|
328
305
|
return nv.volumes.length > 0 || nv.meshes.length > 0;
|
|
@@ -381,12 +358,12 @@ class NvSceneController {
|
|
|
381
358
|
return this.viewerSliceLayouts.get(viewer.id) ?? null;
|
|
382
359
|
}
|
|
383
360
|
applySliceLayout(nv, layout) {
|
|
384
|
-
if (layout) {
|
|
385
|
-
nv.
|
|
361
|
+
if (layout && layout.length > 0) {
|
|
362
|
+
nv.customLayout = layout;
|
|
386
363
|
} else {
|
|
387
|
-
nv.
|
|
388
|
-
nv.
|
|
389
|
-
nv.
|
|
364
|
+
nv.customLayout = null;
|
|
365
|
+
nv.sliceType = SLICE_TYPE.AXIAL;
|
|
366
|
+
nv.showRender = SHOW_RENDER.NEVER;
|
|
390
367
|
}
|
|
391
368
|
}
|
|
392
369
|
getNiivueById(id) {
|
|
@@ -401,7 +378,11 @@ class NvSceneController {
|
|
|
401
378
|
throw new Error(`No viewer at index ${index}`);
|
|
402
379
|
this.incrementLoading(viewer.id);
|
|
403
380
|
try {
|
|
404
|
-
|
|
381
|
+
await viewer.niivue.addVolume(opts);
|
|
382
|
+
const vols = viewer.niivue.volumes;
|
|
383
|
+
const image = vols[vols.length - 1];
|
|
384
|
+
if (!image)
|
|
385
|
+
throw new Error("Volume was not added");
|
|
405
386
|
this.emit("volumeAdded", index, opts, image);
|
|
406
387
|
if (this.broadcasting) {
|
|
407
388
|
this.rewireBroadcasting();
|
|
@@ -422,14 +403,15 @@ class NvSceneController {
|
|
|
422
403
|
}
|
|
423
404
|
return results;
|
|
424
405
|
}
|
|
425
|
-
removeVolume(index, url) {
|
|
406
|
+
async removeVolume(index, url) {
|
|
426
407
|
const viewer = this.viewers[index];
|
|
427
408
|
if (!viewer)
|
|
428
409
|
return;
|
|
429
410
|
const nv = viewer.niivue;
|
|
430
|
-
const
|
|
431
|
-
if (
|
|
432
|
-
nv.removeVolume(
|
|
411
|
+
const volIdx = nv.volumes.findIndex((v) => v.url === url || v.name === url);
|
|
412
|
+
if (volIdx >= 0) {
|
|
413
|
+
nv.model.removeVolume(volIdx);
|
|
414
|
+
await nv.updateGLVolume();
|
|
433
415
|
this.emit("volumeRemoved", index, url);
|
|
434
416
|
if (this.broadcasting) {
|
|
435
417
|
this.rewireBroadcasting();
|
|
@@ -447,30 +429,27 @@ class NvSceneController {
|
|
|
447
429
|
return;
|
|
448
430
|
return { nv, vol, volumeIndex };
|
|
449
431
|
}
|
|
450
|
-
setColormap(viewerIndex, volumeIndex, colormap) {
|
|
432
|
+
async setColormap(viewerIndex, volumeIndex, colormap) {
|
|
451
433
|
const found = this.findVolume(viewerIndex, volumeIndex);
|
|
452
434
|
if (!found)
|
|
453
435
|
return;
|
|
454
|
-
found.
|
|
455
|
-
found.nv.updateGLVolume();
|
|
436
|
+
await found.nv.setVolume(found.volumeIndex, { colormap });
|
|
456
437
|
this.emit("colormapChanged", viewerIndex, volumeIndex, colormap);
|
|
457
438
|
this.notify();
|
|
458
439
|
}
|
|
459
|
-
setCalMinMax(viewerIndex, volumeIndex,
|
|
440
|
+
async setCalMinMax(viewerIndex, volumeIndex, calMin, calMax) {
|
|
460
441
|
const found = this.findVolume(viewerIndex, volumeIndex);
|
|
461
442
|
if (!found)
|
|
462
443
|
return;
|
|
463
|
-
found.
|
|
464
|
-
|
|
465
|
-
found.nv.updateGLVolume();
|
|
466
|
-
this.emit("intensityChanged", viewerIndex, volumeIndex, cal_min, cal_max);
|
|
444
|
+
await found.nv.setVolume(found.volumeIndex, { calMin, calMax });
|
|
445
|
+
this.emit("intensityChanged", viewerIndex, volumeIndex, calMin, calMax);
|
|
467
446
|
this.notify();
|
|
468
447
|
}
|
|
469
|
-
setOpacity(viewerIndex, volumeIndex, opacity) {
|
|
448
|
+
async setOpacity(viewerIndex, volumeIndex, opacity) {
|
|
470
449
|
const found = this.findVolume(viewerIndex, volumeIndex);
|
|
471
450
|
if (!found)
|
|
472
451
|
return;
|
|
473
|
-
found.nv.
|
|
452
|
+
await found.nv.setVolume(found.volumeIndex, { opacity });
|
|
474
453
|
this.emit("opacityChanged", viewerIndex, volumeIndex, opacity);
|
|
475
454
|
this.notify();
|
|
476
455
|
}
|
|
@@ -497,8 +476,15 @@ class NvSceneController {
|
|
|
497
476
|
}
|
|
498
477
|
const containerDiv = document.createElement("div");
|
|
499
478
|
containerDiv.className = "niivue-canvas-container";
|
|
479
|
+
containerDiv.style.position = "relative";
|
|
480
|
+
containerDiv.style.overflow = "hidden";
|
|
500
481
|
const canvas = document.createElement("canvas");
|
|
501
482
|
canvas.className = "niivue-canvas";
|
|
483
|
+
canvas.style.position = "absolute";
|
|
484
|
+
canvas.style.top = "0";
|
|
485
|
+
canvas.style.left = "0";
|
|
486
|
+
canvas.style.width = "100%";
|
|
487
|
+
canvas.style.height = "100%";
|
|
502
488
|
containerDiv.appendChild(canvas);
|
|
503
489
|
this.containerElement.appendChild(containerDiv);
|
|
504
490
|
const mergedOptions = {
|
|
@@ -506,9 +492,7 @@ class NvSceneController {
|
|
|
506
492
|
...this.viewerDefaults,
|
|
507
493
|
...options
|
|
508
494
|
};
|
|
509
|
-
const niivue = new
|
|
510
|
-
niivue.setMouseEventConfig(defaultMouseConfig);
|
|
511
|
-
niivue.attachToCanvas(canvas);
|
|
495
|
+
const niivue = new NiiVueGPU(mergedOptions);
|
|
512
496
|
const id = `nv-${this.nextId++}`;
|
|
513
497
|
this.viewerSliceLayouts.set(id, null);
|
|
514
498
|
this.loadingCounts.set(id, 0);
|
|
@@ -522,29 +506,33 @@ class NvSceneController {
|
|
|
522
506
|
this.viewers.push(viewer);
|
|
523
507
|
this.viewersById.set(id, viewer);
|
|
524
508
|
this.updateLayout();
|
|
525
|
-
this.applySliceLayout(niivue, null);
|
|
526
509
|
const index = this.viewers.length - 1;
|
|
527
|
-
niivue.
|
|
528
|
-
this.emit("locationChange", index,
|
|
529
|
-
};
|
|
530
|
-
niivue.
|
|
531
|
-
this.emit("imageLoaded", index,
|
|
510
|
+
niivue.addEventListener("locationChange", (evt) => {
|
|
511
|
+
this.emit("locationChange", index, evt.detail);
|
|
512
|
+
});
|
|
513
|
+
niivue.addEventListener("volumeLoaded", (evt) => {
|
|
514
|
+
this.emit("imageLoaded", index, evt.detail.volume);
|
|
532
515
|
if (this.broadcasting) {
|
|
533
516
|
this.rewireBroadcasting();
|
|
534
517
|
}
|
|
535
|
-
};
|
|
536
|
-
niivue.
|
|
518
|
+
});
|
|
519
|
+
niivue.addEventListener("meshLoaded", () => {
|
|
537
520
|
if (this.broadcasting) {
|
|
538
521
|
this.rewireBroadcasting();
|
|
539
522
|
}
|
|
540
|
-
};
|
|
541
|
-
if (this.broadcasting) {
|
|
542
|
-
this.setBroadcasting(true);
|
|
543
|
-
}
|
|
523
|
+
});
|
|
544
524
|
this.onViewerCreated?.(niivue, index);
|
|
545
525
|
this.emit("viewerCreated", niivue, index);
|
|
546
526
|
this.notify();
|
|
547
|
-
|
|
527
|
+
const ready = niivue.attachToCanvas(canvas).then(() => {
|
|
528
|
+
this.applySliceLayout(niivue, null);
|
|
529
|
+
if (this.broadcasting) {
|
|
530
|
+
this.setBroadcasting(true);
|
|
531
|
+
}
|
|
532
|
+
this.notify();
|
|
533
|
+
return viewer;
|
|
534
|
+
});
|
|
535
|
+
return ready;
|
|
548
536
|
}
|
|
549
537
|
removeViewer(index, shouldNotify = true) {
|
|
550
538
|
if (index < 0 || index >= this.viewers.length)
|
|
@@ -569,15 +557,7 @@ class NvSceneController {
|
|
|
569
557
|
}
|
|
570
558
|
}
|
|
571
559
|
disposeViewer(viewer) {
|
|
572
|
-
|
|
573
|
-
let gl = nv._gl;
|
|
574
|
-
if (gl) {
|
|
575
|
-
const ext = gl.getExtension("WEBGL_lose_context");
|
|
576
|
-
if (ext) {
|
|
577
|
-
ext.loseContext();
|
|
578
|
-
}
|
|
579
|
-
gl = null;
|
|
580
|
-
}
|
|
560
|
+
viewer.niivue.destroy();
|
|
581
561
|
viewer.canvasElement.width = 0;
|
|
582
562
|
viewer.canvasElement.height = 0;
|
|
583
563
|
}
|
|
@@ -601,13 +581,70 @@ class NvSceneController {
|
|
|
601
581
|
}
|
|
602
582
|
}
|
|
603
583
|
|
|
604
|
-
// src/
|
|
584
|
+
// src/hooks.ts
|
|
585
|
+
function useScene(controller, layouts, viewerDefaults) {
|
|
586
|
+
const scene = useMemo(() => controller ?? new NvSceneController(layouts, viewerDefaults), [controller, viewerDefaults, layouts]);
|
|
587
|
+
const snapshot = useSyncExternalStore(scene.subscribe, scene.getSnapshot, scene.getSnapshot);
|
|
588
|
+
return { scene, snapshot };
|
|
589
|
+
}
|
|
590
|
+
function useNiivue(scene, index) {
|
|
591
|
+
const snapshot = useSyncExternalStore(scene.subscribe, scene.getSnapshot, scene.getSnapshot);
|
|
592
|
+
return index < snapshot.viewerCount ? scene.getNiivue(index) : undefined;
|
|
593
|
+
}
|
|
594
|
+
function useSceneEvent(scene, event, callback) {
|
|
595
|
+
const callbackRef = useRef(callback);
|
|
596
|
+
callbackRef.current = callback;
|
|
597
|
+
useEffect(() => {
|
|
598
|
+
const handler = (...args) => {
|
|
599
|
+
callbackRef.current(...args);
|
|
600
|
+
};
|
|
601
|
+
return scene.on(event, handler);
|
|
602
|
+
}, [scene, event]);
|
|
603
|
+
}
|
|
604
|
+
// src/nvscene.tsx
|
|
605
|
+
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
605
606
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
607
|
+
var NvScene = ({
|
|
608
|
+
scene,
|
|
609
|
+
className,
|
|
610
|
+
style,
|
|
611
|
+
initialLayout = "1x1"
|
|
612
|
+
}) => {
|
|
613
|
+
const containerRef = useRef2(null);
|
|
614
|
+
useEffect2(() => {
|
|
615
|
+
if (containerRef.current) {
|
|
616
|
+
scene.setContainerElement(containerRef.current);
|
|
617
|
+
if (scene.viewers.length === 0) {
|
|
618
|
+
scene.setLayout(initialLayout);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return () => {
|
|
622
|
+
scene.setContainerElement(null);
|
|
623
|
+
};
|
|
624
|
+
}, [scene, initialLayout]);
|
|
625
|
+
useEffect2(() => {
|
|
626
|
+
const el = containerRef.current;
|
|
627
|
+
if (!el)
|
|
628
|
+
return;
|
|
629
|
+
const ro = new ResizeObserver(() => scene.updateLayout());
|
|
630
|
+
ro.observe(el);
|
|
631
|
+
return () => ro.disconnect();
|
|
632
|
+
}, [scene]);
|
|
633
|
+
return /* @__PURE__ */ jsx2("div", {
|
|
634
|
+
ref: containerRef,
|
|
635
|
+
className,
|
|
636
|
+
style
|
|
637
|
+
});
|
|
638
|
+
};
|
|
639
|
+
// src/nvviewer.tsx
|
|
640
|
+
import NiiVueGPU2, { SLICE_TYPE as SLICE_TYPE2 } from "@niivue/niivue";
|
|
641
|
+
import { useEffect as useEffect3, useRef as useRef3 } from "react";
|
|
642
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
606
643
|
function extractVisualProps(opts) {
|
|
607
644
|
return {
|
|
608
|
-
colormap: opts.colormap
|
|
609
|
-
|
|
610
|
-
|
|
645
|
+
colormap: opts.colormap,
|
|
646
|
+
calMin: opts.calMin,
|
|
647
|
+
calMax: opts.calMax,
|
|
611
648
|
opacity: opts.opacity
|
|
612
649
|
};
|
|
613
650
|
}
|
|
@@ -621,16 +658,16 @@ var NvViewer = ({
|
|
|
621
658
|
onImageLoaded,
|
|
622
659
|
onError
|
|
623
660
|
}) => {
|
|
624
|
-
const containerRef =
|
|
625
|
-
const nvRef =
|
|
626
|
-
const loadedVolumesRef =
|
|
627
|
-
const onLocationChangeRef =
|
|
661
|
+
const containerRef = useRef3(null);
|
|
662
|
+
const nvRef = useRef3(null);
|
|
663
|
+
const loadedVolumesRef = useRef3(new Map);
|
|
664
|
+
const onLocationChangeRef = useRef3(onLocationChange);
|
|
628
665
|
onLocationChangeRef.current = onLocationChange;
|
|
629
|
-
const onImageLoadedRef =
|
|
666
|
+
const onImageLoadedRef = useRef3(onImageLoaded);
|
|
630
667
|
onImageLoadedRef.current = onImageLoaded;
|
|
631
|
-
const onErrorRef =
|
|
668
|
+
const onErrorRef = useRef3(onError);
|
|
632
669
|
onErrorRef.current = onError;
|
|
633
|
-
|
|
670
|
+
useEffect3(() => {
|
|
634
671
|
const container = containerRef.current;
|
|
635
672
|
if (!container)
|
|
636
673
|
return;
|
|
@@ -646,138 +683,94 @@ var NvViewer = ({
|
|
|
646
683
|
...defaultViewerOptions,
|
|
647
684
|
...options
|
|
648
685
|
};
|
|
649
|
-
const nv = new
|
|
650
|
-
nv.
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
686
|
+
const nv = new NiiVueGPU2(mergedOptions);
|
|
687
|
+
nv.addEventListener("locationChange", (evt) => {
|
|
688
|
+
onLocationChangeRef.current?.(evt.detail);
|
|
689
|
+
});
|
|
690
|
+
nv.addEventListener("volumeLoaded", (evt) => {
|
|
691
|
+
onImageLoadedRef.current?.(evt.detail.volume);
|
|
692
|
+
});
|
|
693
|
+
nv.attachToCanvas(canvas).then(() => {
|
|
694
|
+
nv.sliceType = sliceType;
|
|
695
|
+
});
|
|
659
696
|
nvRef.current = nv;
|
|
660
697
|
const ro = new ResizeObserver(() => {
|
|
661
|
-
nv.
|
|
698
|
+
nv.resize();
|
|
662
699
|
});
|
|
663
700
|
ro.observe(container);
|
|
664
701
|
return () => {
|
|
665
702
|
ro.disconnect();
|
|
666
|
-
|
|
667
|
-
if (gl) {
|
|
668
|
-
const ext = gl.getExtension("WEBGL_lose_context");
|
|
669
|
-
if (ext)
|
|
670
|
-
ext.loseContext();
|
|
671
|
-
}
|
|
703
|
+
nv.destroy();
|
|
672
704
|
canvas.width = 0;
|
|
673
705
|
canvas.height = 0;
|
|
674
706
|
canvas.remove();
|
|
675
707
|
nvRef.current = null;
|
|
676
708
|
loadedVolumesRef.current.clear();
|
|
677
709
|
};
|
|
678
|
-
}, []);
|
|
679
|
-
|
|
680
|
-
nvRef.current
|
|
710
|
+
}, [sliceType, options]);
|
|
711
|
+
useEffect3(() => {
|
|
712
|
+
const nv = nvRef.current;
|
|
713
|
+
if (nv) {
|
|
714
|
+
nv.sliceType = sliceType;
|
|
715
|
+
}
|
|
681
716
|
}, [sliceType]);
|
|
682
|
-
|
|
717
|
+
useEffect3(() => {
|
|
683
718
|
const nv = nvRef.current;
|
|
684
719
|
if (!nv)
|
|
685
720
|
return;
|
|
686
|
-
const desiredUrls = new Set((volumes ?? []).map((v) => v.url));
|
|
721
|
+
const desiredUrls = new Set((volumes ?? []).map((v) => typeof v.url === "string" ? v.url : v.url.name));
|
|
687
722
|
const currentVolumes = loadedVolumesRef.current;
|
|
688
723
|
for (const url of currentVolumes.keys()) {
|
|
689
724
|
if (!desiredUrls.has(url)) {
|
|
690
|
-
const
|
|
691
|
-
if (
|
|
692
|
-
nv.removeVolume(
|
|
725
|
+
const volIdx = nv.volumes.findIndex((v) => v.url === url || v.name === url);
|
|
726
|
+
if (volIdx >= 0) {
|
|
727
|
+
nv.model.removeVolume(volIdx);
|
|
728
|
+
nv.updateGLVolume();
|
|
729
|
+
}
|
|
693
730
|
currentVolumes.delete(url);
|
|
694
731
|
}
|
|
695
732
|
}
|
|
696
|
-
let needsGLUpdate = false;
|
|
697
733
|
for (const opts of volumes ?? []) {
|
|
698
|
-
|
|
734
|
+
const urlKey = typeof opts.url === "string" ? opts.url : opts.url.name;
|
|
735
|
+
if (!currentVolumes.has(urlKey)) {
|
|
699
736
|
const props = extractVisualProps(opts);
|
|
700
|
-
currentVolumes.set(
|
|
701
|
-
nv.
|
|
702
|
-
currentVolumes.delete(
|
|
737
|
+
currentVolumes.set(urlKey, props);
|
|
738
|
+
nv.addVolume(opts).catch((err) => {
|
|
739
|
+
currentVolumes.delete(urlKey);
|
|
703
740
|
onErrorRef.current?.(err);
|
|
704
741
|
});
|
|
705
742
|
} else {
|
|
706
|
-
const prev = currentVolumes.get(
|
|
743
|
+
const prev = currentVolumes.get(urlKey);
|
|
707
744
|
const next = extractVisualProps(opts);
|
|
708
|
-
const
|
|
709
|
-
if (
|
|
745
|
+
const volIdx = nv.volumes.findIndex((v) => v.url === urlKey || v.name === urlKey);
|
|
746
|
+
if (volIdx < 0)
|
|
710
747
|
continue;
|
|
748
|
+
const updates = {};
|
|
711
749
|
if (next.colormap !== undefined && next.colormap !== prev.colormap) {
|
|
712
|
-
|
|
713
|
-
needsGLUpdate = true;
|
|
750
|
+
updates.colormap = next.colormap;
|
|
714
751
|
}
|
|
715
|
-
if (next.
|
|
716
|
-
|
|
717
|
-
needsGLUpdate = true;
|
|
752
|
+
if (next.calMin !== undefined && next.calMin !== prev.calMin) {
|
|
753
|
+
updates.calMin = next.calMin;
|
|
718
754
|
}
|
|
719
|
-
if (next.
|
|
720
|
-
|
|
721
|
-
needsGLUpdate = true;
|
|
755
|
+
if (next.calMax !== undefined && next.calMax !== prev.calMax) {
|
|
756
|
+
updates.calMax = next.calMax;
|
|
722
757
|
}
|
|
723
758
|
if (next.opacity !== undefined && next.opacity !== prev.opacity) {
|
|
724
|
-
|
|
725
|
-
nv.setOpacity(volIdx, next.opacity);
|
|
759
|
+
updates.opacity = next.opacity;
|
|
726
760
|
}
|
|
727
|
-
|
|
761
|
+
if (Object.keys(updates).length > 0) {
|
|
762
|
+
nv.setVolume(volIdx, updates);
|
|
763
|
+
}
|
|
764
|
+
currentVolumes.set(urlKey, next);
|
|
728
765
|
}
|
|
729
766
|
}
|
|
730
|
-
if (needsGLUpdate) {
|
|
731
|
-
nv.updateGLVolume();
|
|
732
|
-
}
|
|
733
767
|
}, [volumes]);
|
|
734
|
-
return /* @__PURE__ */
|
|
768
|
+
return /* @__PURE__ */ jsx3("div", {
|
|
735
769
|
ref: containerRef,
|
|
736
770
|
className,
|
|
737
771
|
style: { position: "relative", ...style }
|
|
738
772
|
});
|
|
739
773
|
};
|
|
740
|
-
// src/hooks.ts
|
|
741
|
-
import { useEffect as useEffect3, useMemo, useSyncExternalStore, useRef as useRef3 } from "react";
|
|
742
|
-
function useScene(controller, layouts, viewerDefaults) {
|
|
743
|
-
const scene = useMemo(() => controller ?? new NvSceneController(layouts, viewerDefaults), [controller]);
|
|
744
|
-
const snapshot = useSyncExternalStore(scene.subscribe, scene.getSnapshot, scene.getSnapshot);
|
|
745
|
-
return { scene, snapshot };
|
|
746
|
-
}
|
|
747
|
-
function useNiivue(scene, index) {
|
|
748
|
-
const snapshot = useSyncExternalStore(scene.subscribe, scene.getSnapshot, scene.getSnapshot);
|
|
749
|
-
return index < snapshot.viewerCount ? scene.getNiivue(index) : undefined;
|
|
750
|
-
}
|
|
751
|
-
function useSceneEvent(scene, event, callback) {
|
|
752
|
-
const callbackRef = useRef3(callback);
|
|
753
|
-
callbackRef.current = callback;
|
|
754
|
-
useEffect3(() => {
|
|
755
|
-
const handler = (...args) => {
|
|
756
|
-
callbackRef.current(...args);
|
|
757
|
-
};
|
|
758
|
-
return scene.on(event, handler);
|
|
759
|
-
}, [scene, event]);
|
|
760
|
-
}
|
|
761
|
-
// src/context.tsx
|
|
762
|
-
import { createContext, useContext } from "react";
|
|
763
|
-
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
764
|
-
var NvSceneContext = createContext(null);
|
|
765
|
-
function NvSceneProvider({
|
|
766
|
-
scene,
|
|
767
|
-
children
|
|
768
|
-
}) {
|
|
769
|
-
return /* @__PURE__ */ jsx3(NvSceneContext.Provider, {
|
|
770
|
-
value: scene,
|
|
771
|
-
children
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
function useSceneContext() {
|
|
775
|
-
const scene = useContext(NvSceneContext);
|
|
776
|
-
if (!scene) {
|
|
777
|
-
throw new Error("useSceneContext must be used within an NvSceneProvider");
|
|
778
|
-
}
|
|
779
|
-
return scene;
|
|
780
|
-
}
|
|
781
774
|
export {
|
|
782
775
|
useSceneEvent,
|
|
783
776
|
useSceneContext,
|
|
@@ -791,7 +784,6 @@ export {
|
|
|
791
784
|
defaultViewerOptions,
|
|
792
785
|
defaultSliceLayouts,
|
|
793
786
|
defaultSliceLayout,
|
|
794
|
-
defaultMouseConfig,
|
|
795
787
|
defaultLayouts,
|
|
796
788
|
SLICE_TYPE,
|
|
797
789
|
NvViewer,
|
package/dist/types/context.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type ReactNode } from
|
|
2
|
-
import { NvSceneController } from
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { NvSceneController } from './nvscene-controller';
|
|
3
3
|
export declare function NvSceneProvider({ scene, children, }: {
|
|
4
4
|
scene: NvSceneController;
|
|
5
5
|
children: ReactNode;
|
package/dist/types/hooks.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { LayoutConfig } from
|
|
3
|
-
import { NvSceneController } from
|
|
4
|
-
import type { NvSceneEventMap } from
|
|
5
|
-
export declare function useScene(controller?: NvSceneController, layouts?: Record<string, LayoutConfig>, viewerDefaults?: Partial<
|
|
1
|
+
import type { NiiVueOptions } from '@niivue/niivue';
|
|
2
|
+
import type { LayoutConfig } from './layouts';
|
|
3
|
+
import { NvSceneController } from './nvscene-controller';
|
|
4
|
+
import type { NvSceneEventMap } from './types';
|
|
5
|
+
export declare function useScene(controller?: NvSceneController, layouts?: Record<string, LayoutConfig>, viewerDefaults?: Partial<NiiVueOptions>): {
|
|
6
6
|
scene: NvSceneController;
|
|
7
7
|
snapshot: import("./nvscene-controller").NvSceneControllerSnapshot;
|
|
8
8
|
};
|
|
9
|
-
export declare function useNiivue(scene: NvSceneController, index: number): import("@niivue/niivue").
|
|
9
|
+
export declare function useNiivue(scene: NvSceneController, index: number): import("@niivue/niivue").default | undefined;
|
|
10
10
|
export declare function useSceneEvent<E extends keyof NvSceneEventMap>(scene: NvSceneController, event: E, callback: NvSceneEventMap[E]): void;
|
package/dist/types/lib.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export
|
|
4
|
-
export {
|
|
5
|
-
export type {
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
9
|
-
export type {
|
|
1
|
+
export { NvSceneProvider, useSceneContext } from './context';
|
|
2
|
+
export { useNiivue, useScene, useSceneEvent } from './hooks';
|
|
3
|
+
export { defaultLayouts } from './layouts';
|
|
4
|
+
export { NvScene } from './nvscene';
|
|
5
|
+
export type { BroadcastOptions, NiivueCallback, NvSceneControllerSnapshot, SliceLayoutConfig, SliceLayoutTile, ViewerSlot, } from './nvscene-controller';
|
|
6
|
+
export { defaultSliceLayout, defaultSliceLayouts, defaultViewerOptions, heroRenderSliceLayout, NvSceneController, quadSliceLayout, SLICE_TYPE, splitSliceLayout, stackedSliceLayout, triSliceLayout, } from './nvscene-controller';
|
|
7
|
+
export type { NvViewerProps } from './nvviewer';
|
|
8
|
+
export { NvViewer } from './nvviewer';
|
|
9
|
+
export type { ImageFromUrlOptions, NiiVueOptions, NVImage, NvSceneEventMap, ViewerState, } from './types';
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import type {
|
|
1
|
+
import type { NiiVueOptions, NVImage } from '@niivue/niivue';
|
|
2
|
+
import NiiVueGPU, { SLICE_TYPE } from '@niivue/niivue';
|
|
3
|
+
import type { LayoutConfig } from './layouts';
|
|
4
|
+
import type { ImageFromUrlOptions, NvSceneEventMap, ViewerState } from './types';
|
|
4
5
|
export { SLICE_TYPE };
|
|
5
|
-
export type NiivueCallback = (nv:
|
|
6
|
+
export type NiivueCallback = (nv: NiiVueGPU, index: number) => void;
|
|
6
7
|
export interface ViewerSlot {
|
|
7
8
|
id: string;
|
|
8
|
-
niivue:
|
|
9
|
+
niivue: NiiVueGPU;
|
|
9
10
|
canvasElement: HTMLCanvasElement;
|
|
10
11
|
containerDiv: HTMLDivElement;
|
|
11
12
|
}
|
|
@@ -18,12 +19,13 @@ export interface NvSceneControllerSnapshot {
|
|
|
18
19
|
viewerStates: ViewerState[];
|
|
19
20
|
}
|
|
20
21
|
export interface BroadcastOptions {
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
'2d': boolean;
|
|
23
|
+
'3d': boolean;
|
|
23
24
|
}
|
|
24
25
|
export interface SliceLayoutTile {
|
|
25
26
|
sliceType: number;
|
|
26
27
|
position: [number, number, number, number];
|
|
28
|
+
sliceMM?: number;
|
|
27
29
|
}
|
|
28
30
|
export interface SliceLayoutConfig {
|
|
29
31
|
label: string;
|
|
@@ -55,14 +57,7 @@ export declare const quadSliceLayout: SliceLayoutTile[];
|
|
|
55
57
|
export declare const heroRenderSliceLayout: SliceLayoutTile[];
|
|
56
58
|
export declare const defaultSliceLayouts: Record<string, SliceLayoutConfig>;
|
|
57
59
|
type Listener = () => void;
|
|
58
|
-
export declare const defaultViewerOptions: Partial<
|
|
59
|
-
export declare const defaultMouseConfig: {
|
|
60
|
-
leftButton: {
|
|
61
|
-
primary: DRAG_MODE;
|
|
62
|
-
};
|
|
63
|
-
rightButton: DRAG_MODE;
|
|
64
|
-
centerButton: DRAG_MODE;
|
|
65
|
-
};
|
|
60
|
+
export declare const defaultViewerOptions: Partial<NiiVueOptions>;
|
|
66
61
|
/**
|
|
67
62
|
* An NvSceneController is a declarative representation of what we want to render with Niivue.
|
|
68
63
|
*
|
|
@@ -94,7 +89,7 @@ export declare class NvSceneController {
|
|
|
94
89
|
private eventListeners;
|
|
95
90
|
private loadingCounts;
|
|
96
91
|
private viewerErrors;
|
|
97
|
-
constructor(layouts?: Record<string, LayoutConfig>, viewerDefaults?: Partial<
|
|
92
|
+
constructor(layouts?: Record<string, LayoutConfig>, viewerDefaults?: Partial<NiiVueOptions>);
|
|
98
93
|
on<E extends keyof NvSceneEventMap>(event: E, cb: NvSceneEventMap[E]): () => void;
|
|
99
94
|
off<E extends keyof NvSceneEventMap>(event: E, cb: NvSceneEventMap[E]): void;
|
|
100
95
|
private emit;
|
|
@@ -105,8 +100,8 @@ export declare class NvSceneController {
|
|
|
105
100
|
setLayout(layoutName: string): void;
|
|
106
101
|
updateLayout(): void;
|
|
107
102
|
canAddViewer(): boolean;
|
|
108
|
-
getNiivue(index: number):
|
|
109
|
-
getAllNiivue():
|
|
103
|
+
getNiivue(index: number): NiiVueGPU | undefined;
|
|
104
|
+
getAllNiivue(): NiiVueGPU[];
|
|
110
105
|
forEachNiivue(callback: NiivueCallback): void;
|
|
111
106
|
private hasSyncableContent;
|
|
112
107
|
private safeBroadcastTo;
|
|
@@ -116,26 +111,26 @@ export declare class NvSceneController {
|
|
|
116
111
|
setViewerSliceLayout(index: number, layout: SliceLayoutTile[] | null): void;
|
|
117
112
|
getViewerSliceLayout(index: number): SliceLayoutTile[] | null;
|
|
118
113
|
private applySliceLayout;
|
|
119
|
-
getNiivueById(id: string):
|
|
114
|
+
getNiivueById(id: string): NiiVueGPU | undefined;
|
|
120
115
|
getViewerById(id: string): ViewerSlot | undefined;
|
|
121
116
|
loadVolume(index: number, opts: ImageFromUrlOptions): Promise<NVImage>;
|
|
122
117
|
loadVolumes(index: number, opts: ImageFromUrlOptions[]): Promise<NVImage[]>;
|
|
123
|
-
removeVolume(index: number, url: string): void
|
|
118
|
+
removeVolume(index: number, url: string): Promise<void>;
|
|
124
119
|
/**
|
|
125
120
|
* Look up the Niivue instance and NVImage at the given viewer and volume indices.
|
|
126
121
|
* Returns undefined if either index is out of bounds.
|
|
127
122
|
*/
|
|
128
123
|
private findVolume;
|
|
129
124
|
/** 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 (
|
|
132
|
-
setCalMinMax(viewerIndex: number, volumeIndex: number,
|
|
125
|
+
setColormap(viewerIndex: number, volumeIndex: number, colormap: string): Promise<void>;
|
|
126
|
+
/** Set the intensity range (calMin / calMax) for a volume at the given viewer and volume index. */
|
|
127
|
+
setCalMinMax(viewerIndex: number, volumeIndex: number, calMin: number, calMax: number): Promise<void>;
|
|
133
128
|
/** Set the opacity for a volume at the given viewer and volume index. */
|
|
134
|
-
setOpacity(viewerIndex: number, volumeIndex: number, opacity: number): void
|
|
129
|
+
setOpacity(viewerIndex: number, volumeIndex: number, opacity: number): Promise<void>;
|
|
135
130
|
private incrementLoading;
|
|
136
131
|
private decrementLoading;
|
|
137
132
|
private addError;
|
|
138
|
-
addViewer(options?: Partial<
|
|
133
|
+
addViewer(options?: Partial<NiiVueOptions>): Promise<ViewerSlot>;
|
|
139
134
|
removeViewer(index: number, shouldNotify?: boolean): void;
|
|
140
135
|
private disposeViewer;
|
|
141
136
|
clearViewers(): void;
|
package/dist/types/nvscene.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { CSSProperties } from
|
|
2
|
-
import { NvSceneController } from
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
import type { NvSceneController } from './nvscene-controller';
|
|
3
3
|
interface NiivueControllerProps {
|
|
4
4
|
scene: NvSceneController;
|
|
5
5
|
className?: string;
|
package/dist/types/nvviewer.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import
|
|
3
|
-
import type { ImageFromUrlOptions } from
|
|
1
|
+
import type { NiiVueOptions, NVImage } from '@niivue/niivue';
|
|
2
|
+
import type { CSSProperties } from 'react';
|
|
3
|
+
import type { ImageFromUrlOptions } from './types';
|
|
4
4
|
export interface NvViewerProps {
|
|
5
5
|
volumes?: ImageFromUrlOptions[];
|
|
6
|
-
options?: Partial<
|
|
6
|
+
options?: Partial<NiiVueOptions>;
|
|
7
7
|
sliceType?: number;
|
|
8
8
|
className?: string;
|
|
9
9
|
style?: CSSProperties;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export type ImageFromUrlOptions = Parameters<Niivue["addVolumeFromUrl"]>[0];
|
|
1
|
+
import type NiiVueGPU from '@niivue/niivue';
|
|
2
|
+
import type { ImageFromUrlOptions, NiiVueOptions, NVImage } from '@niivue/niivue';
|
|
3
|
+
export type { ImageFromUrlOptions, NiiVueOptions, NVImage };
|
|
5
4
|
export interface NvSceneEventMap {
|
|
6
|
-
viewerCreated: (nv:
|
|
5
|
+
viewerCreated: (nv: NiiVueGPU, index: number) => void;
|
|
7
6
|
viewerRemoved: (index: number) => void;
|
|
8
7
|
locationChange: (viewerIndex: number, data: unknown) => void;
|
|
9
8
|
imageLoaded: (viewerIndex: number, volume: NVImage) => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@niivue/nvreact",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-rc.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/lib.js",
|
|
@@ -19,9 +19,8 @@
|
|
|
19
19
|
"dist"
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
|
-
"dev": "
|
|
22
|
+
"dev": "vite",
|
|
23
23
|
"build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'",
|
|
24
|
-
"start": "NODE_ENV=production bun src/index.ts",
|
|
25
24
|
"build:lib": "NODE_ENV=production bun build ./src/lib.ts --outdir=dist --target=browser --format=esm --external react --external react-dom --external @niivue/niivue",
|
|
26
25
|
"build:types": "tsc -p tsconfig.build.json",
|
|
27
26
|
"build:package": "bun run build:lib && bun run build:types",
|
|
@@ -31,21 +30,23 @@
|
|
|
31
30
|
},
|
|
32
31
|
"dependencies": {},
|
|
33
32
|
"peerDependencies": {
|
|
34
|
-
"@niivue/niivue": "
|
|
33
|
+
"@niivue/niivue": "workspace:*",
|
|
35
34
|
"react": "^19",
|
|
36
35
|
"react-dom": "^19"
|
|
37
36
|
},
|
|
38
37
|
"devDependencies": {
|
|
39
38
|
"@happy-dom/global-registrator": "^20.6.1",
|
|
40
|
-
"@niivue/niivue": "
|
|
39
|
+
"@niivue/niivue": "workspace:*",
|
|
41
40
|
"@testing-library/dom": "^10.4.1",
|
|
42
41
|
"@testing-library/jest-dom": "^6.9.1",
|
|
43
42
|
"@testing-library/react": "^16.3.2",
|
|
44
43
|
"@types/bun": "latest",
|
|
45
44
|
"@types/react": "^19",
|
|
46
45
|
"@types/react-dom": "^19",
|
|
46
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
47
47
|
"react": "^19",
|
|
48
48
|
"react-dom": "^19",
|
|
49
|
-
"typescript": "^5.9.3"
|
|
49
|
+
"typescript": "^5.9.3",
|
|
50
|
+
"vite": "^8.0.9"
|
|
50
51
|
}
|
|
51
|
-
}
|
|
52
|
+
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { CSSProperties } from "react";
|
|
2
|
-
import { NvSceneController } from "./nvscene-controller";
|
|
3
|
-
interface NiivueControllerProps {
|
|
4
|
-
scene: NvSceneController;
|
|
5
|
-
className?: string;
|
|
6
|
-
style?: CSSProperties;
|
|
7
|
-
initialLayout?: string;
|
|
8
|
-
}
|
|
9
|
-
export declare const NvScene: ({ scene, className, style, initialLayout, }: NiivueControllerProps) => import("react/jsx-runtime").JSX.Element;
|
|
10
|
-
export {};
|