@niivue/nvreact 0.1.3 → 1.0.0-rc.10

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 CHANGED
@@ -5,7 +5,7 @@ Lightweight React bindings for [Niivue](https://github.com/niivue/niivue) with m
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install @niivue/nvreact @niivue/niivue react react-dom
8
+ bun add @niivue/nvreact @niivue/niivue react react-dom
9
9
  ```
10
10
 
11
11
  ## Quick start
package/dist/lib.js CHANGED
@@ -1,52 +1,28 @@
1
- // src/nvscene.tsx
2
- import { useEffect, useRef } from "react";
1
+ // src/context.tsx
2
+ import { createContext, useContext } from "react";
3
3
  import { jsx } from "react/jsx-runtime";
4
- var NvScene = ({
4
+ var NvSceneContext = createContext(null);
5
+ function NvSceneProvider({
5
6
  scene,
6
- className,
7
- style,
8
- initialLayout = "1x1"
9
- }) => {
10
- const containerRef = useRef(null);
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
- // src/nvviewer.tsx
37
- import { useEffect as useEffect2, useRef as useRef2 } from "react";
38
- import {
39
- Niivue as Niivue2,
40
- SLICE_TYPE as SLICE_TYPE2
41
- } from "@niivue/niivue";
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
- var defaultMouseConfig = {
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).add(cb);
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) => 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.viewers.length === 0 && this.containerElement) {
293
- this.addViewer();
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) => callback(viewer.niivue, 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.setCustomLayout(layout);
361
+ if (layout && layout.length > 0) {
362
+ nv.customLayout = layout;
386
363
  } else {
387
- nv.clearCustomLayout();
388
- nv.setSliceType(SLICE_TYPE.AXIAL);
389
- nv.opts.multiplanarShowRender = SHOW_RENDER.NEVER;
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
- const image = await viewer.niivue.addVolumeFromUrl(opts);
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 vol = nv.volumes.find((v) => v.url === url || v.name === url);
431
- if (vol) {
432
- nv.removeVolume(vol);
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.vol.colormap = colormap;
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, cal_min, cal_max) {
440
+ async setCalMinMax(viewerIndex, volumeIndex, calMin, calMax) {
460
441
  const found = this.findVolume(viewerIndex, volumeIndex);
461
442
  if (!found)
462
443
  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);
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.setOpacity(volumeIndex, opacity);
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 Niivue(mergedOptions);
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.onLocationChange = (data) => {
528
- this.emit("locationChange", index, data);
529
- };
530
- niivue.onImageLoaded = (vol) => {
531
- this.emit("imageLoaded", index, vol);
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.onMeshLoaded = (_mesh) => {
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
- return viewer;
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
- const nv = viewer.niivue;
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/nvviewer.tsx
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 ?? opts.colorMap,
609
- cal_min: opts.cal_min,
610
- cal_max: opts.cal_max,
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 = useRef2(null);
625
- const nvRef = useRef2(null);
626
- const loadedVolumesRef = useRef2(new Map);
627
- const onLocationChangeRef = useRef2(onLocationChange);
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 = useRef2(onImageLoaded);
666
+ const onImageLoadedRef = useRef3(onImageLoaded);
630
667
  onImageLoadedRef.current = onImageLoaded;
631
- const onErrorRef = useRef2(onError);
668
+ const onErrorRef = useRef3(onError);
632
669
  onErrorRef.current = onError;
633
- useEffect2(() => {
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 Niivue2(mergedOptions);
650
- nv.setMouseEventConfig(defaultMouseConfig);
651
- nv.onLocationChange = (data) => {
652
- onLocationChangeRef.current?.(data);
653
- };
654
- nv.onImageLoaded = (vol) => {
655
- onImageLoadedRef.current?.(vol);
656
- };
657
- nv.attachToCanvas(canvas);
658
- nv.setSliceType(sliceType);
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.resizeListener();
698
+ nv.resize();
662
699
  });
663
700
  ro.observe(container);
664
701
  return () => {
665
702
  ro.disconnect();
666
- const gl = nv._gl;
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
- useEffect2(() => {
680
- nvRef.current?.setSliceType(sliceType);
710
+ }, [sliceType, options]);
711
+ useEffect3(() => {
712
+ const nv = nvRef.current;
713
+ if (nv) {
714
+ nv.sliceType = sliceType;
715
+ }
681
716
  }, [sliceType]);
682
- useEffect2(() => {
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 vol = nv.volumes.find((v) => v.url === url || v.name === url);
691
- if (vol)
692
- nv.removeVolume(vol);
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
- if (!currentVolumes.has(opts.url)) {
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(opts.url, props);
701
- nv.addVolumeFromUrl(opts).catch((err) => {
702
- currentVolumes.delete(opts.url);
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(opts.url);
743
+ const prev = currentVolumes.get(urlKey);
707
744
  const next = extractVisualProps(opts);
708
- const vol = nv.volumes.find((v) => v.url === opts.url || v.name === opts.url);
709
- if (!vol)
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
- vol.colormap = next.colormap;
713
- needsGLUpdate = true;
750
+ updates.colormap = next.colormap;
714
751
  }
715
- if (next.cal_min !== undefined && next.cal_min !== prev.cal_min) {
716
- vol.cal_min = next.cal_min;
717
- needsGLUpdate = true;
752
+ if (next.calMin !== undefined && next.calMin !== prev.calMin) {
753
+ updates.calMin = next.calMin;
718
754
  }
719
- if (next.cal_max !== undefined && next.cal_max !== prev.cal_max) {
720
- vol.cal_max = next.cal_max;
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
- const volIdx = nv.volumes.indexOf(vol);
725
- nv.setOpacity(volIdx, next.opacity);
759
+ updates.opacity = next.opacity;
726
760
  }
727
- currentVolumes.set(opts.url, next);
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__ */ jsx2("div", {
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,
@@ -1,5 +1,5 @@
1
- import { type ReactNode } from "react";
2
- import { NvSceneController } from "./nvscene-controller";
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;
@@ -1,10 +1,10 @@
1
- import type { NVConfigOptions } 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<NVConfigOptions>): {
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").Niivue | undefined;
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;
@@ -1,9 +1,9 @@
1
- export { NvScene } from "./nvscene";
2
- export { NvViewer } from "./nvviewer";
3
- export type { NvViewerProps } from "./nvviewer";
4
- export { NvSceneController, SLICE_TYPE, defaultSliceLayout, splitSliceLayout, triSliceLayout, stackedSliceLayout, quadSliceLayout, heroRenderSliceLayout, defaultSliceLayouts, defaultViewerOptions, defaultMouseConfig, } from "./nvscene-controller";
5
- export type { NvSceneControllerSnapshot, ViewerSlot, SliceLayoutTile, SliceLayoutConfig, BroadcastOptions, NiivueCallback, } from "./nvscene-controller";
6
- export { defaultLayouts } from "./layouts";
7
- export { useScene, useNiivue, useSceneEvent } from "./hooks";
8
- export { NvSceneProvider, useSceneContext } from "./context";
9
- export type { NvSceneEventMap, ViewerState, NVImage, ImageFromUrlOptions, NVConfigOptions, } from "./types";
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 { DRAG_MODE, Niivue, SLICE_TYPE, type NVConfigOptions, type NVImage } from "@niivue/niivue";
2
- import type { LayoutConfig } from "./layouts";
3
- import type { NvSceneEventMap, ViewerState, ImageFromUrlOptions } from "./types";
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: Niivue, index: number) => void;
6
+ export type NiivueCallback = (nv: NiiVueGPU, index: number) => void;
6
7
  export interface ViewerSlot {
7
8
  id: string;
8
- niivue: 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
- "2d": boolean;
22
- "3d": boolean;
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<NVConfigOptions>;
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<NVConfigOptions>);
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): Niivue | undefined;
109
- getAllNiivue(): Niivue[];
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): Niivue | undefined;
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 (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;
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<NVConfigOptions>): ViewerSlot;
133
+ addViewer(options?: Partial<NiiVueOptions>): Promise<ViewerSlot>;
139
134
  removeViewer(index: number, shouldNotify?: boolean): void;
140
135
  private disposeViewer;
141
136
  clearViewers(): void;
@@ -1,5 +1,5 @@
1
- import type { CSSProperties } from "react";
2
- import { NvSceneController } from "./nvscene-controller";
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;
@@ -1,9 +1,9 @@
1
- import type { CSSProperties } from "react";
2
- import { type NVConfigOptions, type NVImage } from "@niivue/niivue";
3
- import type { ImageFromUrlOptions } from "./types";
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<NVConfigOptions>;
6
+ options?: Partial<NiiVueOptions>;
7
7
  sliceType?: number;
8
8
  className?: string;
9
9
  style?: CSSProperties;
@@ -1,9 +1,8 @@
1
- import type { Niivue, NVImage, NVConfigOptions } from "@niivue/niivue";
2
- export type { NVImage, NVConfigOptions };
3
- /** Extract the ImageFromUrlOptions type from Niivue's addVolumeFromUrl method */
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: Niivue, index: number) => void;
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.1.3",
3
+ "version": "1.0.0-rc.10",
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": "bun --hot src/index.ts",
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": "^0.68.1",
33
+ "@niivue/niivue": "1.0.0-rc.9",
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": "^0.68.1",
39
+ "@niivue/niivue": "1.0.0-rc.9",
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 {};