File size: 3,703 Bytes
227ab66
 
 
 
 
 
491d824
227ab66
 
 
 
 
 
 
 
 
 
 
 
 
491d824
 
227ab66
 
 
 
 
 
 
 
 
 
 
 
 
491d824
 
227ab66
 
 
 
 
491d824
227ab66
 
491d824
 
 
 
 
 
 
 
 
 
 
 
 
 
227ab66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
491d824
227ab66
491d824
227ab66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
491d824
 
227ab66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { Niivue } from '@niivue/niivue';
  import { Block } from "@gradio/atoms";
  import { StatusTracker } from "@gradio/statustracker";
  import type { LoadingStatus } from "@gradio/statustracker";
  import type { Gradio } from "@gradio/utils";

  interface Props {
      value?: { background_url: string | null; overlay_url: string | null } | null;
      label?: string;
      show_label?: boolean;
      loading_status?: LoadingStatus;
      elem_id?: string;
      elem_classes?: string[];
      visible?: boolean;
      height?: number;
      container?: boolean;
      scale?: number;
      min_width?: number;
      // CRITICAL: gradio prop provides i18n, autoscroll, dispatch
      gradio: Gradio<{ change: never }>;
  }

  let {
      value = null,
      label,
      show_label = true,
      loading_status,
      elem_id = "",
      elem_classes = [],
      visible = true,
      height = 500,
      container = true,
      scale = null,
      min_width = undefined,
      gradio
  }: Props = $props();

  let div_container: HTMLDivElement;
  let nv: Niivue | null = null;
  let canvas: HTMLCanvasElement;
  let initialized = false;

  onMount(async () => {
    try {
      nv = new Niivue({
        backColor: [0, 0, 0, 1],
        show3Dcrosshair: true,
        logging: false
      });

      await nv.attachToCanvas(canvas);
      // Don't call loadVolumes() here - let $effect handle it reactively
      // Setting initialized = true triggers $effect which calls loadVolumes()
      initialized = true;
    } catch (error) {
      console.error('[NiiVue] Initialization failed:', error);
    }
  });

  onDestroy(() => {
    // Release WebGL resources and event listeners
    if (nv) {
      nv.cleanup();
      nv = null;
    }
  });

  async function loadVolumes() {
    if (!nv) return;

    // Clear existing volumes
    // nv.volumes is the internal array.
    // The safest way to clear is to remove volumes one by one or re-init.
    // However, loading new volumes usually requires removing old ones if we want a fresh state.
    // NiiVue doesn't have a clearVolumes() method exposed easily in all versions,
    // but iterating and removing works.
    while (nv.volumes.length > 0) {
         nv.removeVolume(nv.volumes[0]);
    }

    if (!value) {
        nv.drawScene();
        return;
    }

    const volumes = [];
    if (value.background_url) {
      volumes.push({ url: value.background_url });
    }
    if (value.overlay_url) {
      volumes.push({
        url: value.overlay_url,
        colormap: 'red',
        opacity: 0.5,
      });
    }

    if (volumes.length > 0) {
        await nv.loadVolumes(volumes);
    } else {
        nv.drawScene();
    }
  }

  // Reactive effect: Re-load volumes when `value` changes (only after init)
  $effect(() => {
      if (initialized && value !== undefined) {
          loadVolumes();
      }
  });

</script>

<Block
	{visible}
	variant={"solid"}
	padding={false}
	{elem_id}
	{elem_classes}
    {height}
	allow_overflow={false}
	{container}
	{scale}
	{min_width}
>
    {#if loading_status}
        <StatusTracker
            autoscroll={gradio.autoscroll}
            i18n={gradio.i18n}
            {...loading_status}
        />
    {/if}

    <div bind:this={div_container} class="niivue-container" style="height: {height}px;">
        <canvas bind:this={canvas}></canvas>
    </div>
</Block>

<style>
  .niivue-container {
    width: 100%;
    background: #000;
    position: relative;
    border-radius: var(--radius-lg);
    overflow: hidden;
  }
  canvas {
    width: 100%;
    height: 100%;
    outline: none;
    display: block;
  }
</style>