1// ââ NiiVue Neuro-Oncology Viewer ââââââââââââââââââââââââââââââââââââââââââââ 2(() => { 3 const viewer = document.getElementById("niivue-viewer"); 4 const canvas = document.getElementById("nv-canvas"); 5 const modSel = document.getElementById("nv-modality"); 6 const viewSel = document.getElementById("nv-view"); 7 if (!(viewer && canvas)) { 8 return; 9 } 10 11 const base = "assests/BraTS-MET-00001-000/"; 12 const segUrl = `${base}BraTS-MET-00001-000-seg.nii.gz`; 13 const mods = { 14 t1c: `${base}BraTS-MET-00001-000-t1c.nii.gz`, 15 t1n: `${base}BraTS-MET-00001-000-t1n.nii.gz`, 16 t2f: `${base}BraTS-MET-00001-000-t2f.nii.gz`, 17 t2w: `${base}BraTS-MET-00001-000-t2w.nii.gz`, 18 }; 19 20 let nv = null; 21 const blobUrls = {}; 22 let segBlobUrl = null; 23 let prefetchPromise = null; 24 let initPromise = null; 25 26 async function waitForSize() { 27 for (let i = 0; i < 40; i += 1) { 28 if (canvas.clientWidth > 0 && canvas.clientHeight > 0) { 29 return; 30 } 31 // Polling frame by frame is the point here â the canvas has no size yet. 32 // biome-ignore lint/performance/noAwaitInLoops: sequential by design 33 await new Promise((r) => requestAnimationFrame(r)); 34 } 35 } 36 37 function reportError(msg) { 38 let box = document.getElementById("nv-error"); 39 if (!box) { 40 box = document.createElement("div"); 41 box.id = "nv-error"; 42 box.className = "niivue-error"; 43 viewer.appendChild(box); 44 } 45 box.textContent = msg; 46 console.error("[niivue]", msg); 47 } 48 49 async function fetchBlob(url) { 50 const r = await fetch(url); 51 if (!r.ok) { 52 throw new Error(`HTTP ${r.status} for ${url}`); 53 } 54 return URL.createObjectURL(await r.blob()); 55 } 56 57 function prefetchAll() { 58 if (prefetchPromise) { 59 return prefetchPromise; 60 } 61 const tasks = [ 62 fetchBlob(segUrl).then((u) => { 63 segBlobUrl = u; 64 }), 65 ...Object.keys(mods).map((k) => 66 fetchBlob(mods[k]).then((u) => { 67 blobUrls[k] = u; 68 }) 69 ), 70 ]; 71 prefetchPromise = Promise.all(tasks).catch((e) => { 72 reportError( 73 "Volume prefetch failed:\n" + 74 e.message + 75 (location.protocol === "file:" 76 ? "\nServe over http (e.g. `python -m http.server`)." 77 : "") 78 ); 79 throw e; 80 }); 81 return prefetchPromise; 82 } 83 84 function init() { 85 if (initPromise) { 86 return initPromise; 87 } 88 initPromise = (async () => { 89 if (typeof niivue === "undefined" || !niivue.Niivue) { 90 reportError("NiiVue library not loaded (check network / CDN)."); 91 return; 92 } 93 94 // Off-screen sizing so attachToCanvas / texture uploads run before the user clicks. 95 const wasActive = viewer.classList.contains("active"); 96 if (!wasActive) { 97 viewer.classList.add("preheating"); 98 } 99 await waitForSize(); 100 101 try { 102 await prefetchAll(); 103 nv = new niivue.Niivue({ 104 backColor: [0, 0, 0, 1], 105 crosshairColor: [1, 1, 1, 0.6], 106 isColorbar: false, 107 loadingText: "loadingâ¦", 108 show3Dcrosshair: true, 109 }); 110 await nv.attachToCanvas(canvas); 111 await loadMod(modSel.value); 112 applyView(viewSel.value); 113 if (nv.resizeListener) { 114 nv.resizeListener(); 115 } 116 if (nv.drawScene) { 117 nv.drawScene(); 118 } 119 window.addEventListener("resize", () => { 120 if (nv?.resizeListener) { 121 nv.resizeListener(); 122 } 123 }); 124 } catch (e) { 125 if (e.message) { 126 reportError(`NiiVue init failed: ${e.message}`); 127 } 128 } finally { 129 if (!wasActive) { 130 viewer.classList.remove("preheating"); 131 } 132 } 133 })(); 134 return initPromise; 135 } 136 137 async function loadMod(mod) { 138 if (!nv) { 139 return; 140 } 141 const baseVol = { 142 colormap: "gray", 143 name: `${mod}.nii.gz`, 144 url: blobUrls[mod] || mods[mod], 145 }; 146 const seg = { 147 colormap: "redyell", 148 name: "seg.nii.gz", 149 opacity: 0.6, 150 url: segBlobUrl || segUrl, 151 }; 152 try { 153 await nv.loadVolumes([baseVol, seg]); 154 } catch (err) { 155 console.error("NiiVue load failed (with seg)", err);
156 try { 157 await nv.loadVolumes([baseVol]); 158 } catch (e2) { 159 reportError(`Volume load failed: ${e2.message ? e2.message : e2}`); 160 } 161 } 162 } 163 164 function applyView(v) { 165 if (!nv) { 166 return; 167 } 168 const map = { 169 axial: nv.sliceTypeAxial, 170 coronal: nv.sliceTypeCoronal, 171 multi: nv.sliceTypeMultiplanar, 172 sagittal: nv.sliceTypeSagittal, 173 }; 174 nv.setSliceType(map[v]); 175 } 176 177 modSel.addEventListener("change", () => loadMod(modSel.value)); 178 viewSel.addEventListener("change", () => applyView(viewSel.value)); 179 180 // ââ Public API: the research modal drives visibility ââ 181 function activate() { 182 viewer.classList.remove("preheating"); 183 viewer.classList.add("active"); 184 init() 185 .then(() => { 186 if (nv?.resizeListener) { 187 nv.resizeListener(); 188 } 189 }) 190 .catch((e) => console.debug("[niivue] init rejected", e)); 191 } 192 193 function deactivate() { 194 viewer.classList.remove("active"); 195 } 196 197 const warm = () => { 198 init().catch(() => { 199 // Warm-up is best effort; a real failure surfaces on activate(). 200 }); 201 }; 202 203 window.NiiViewer = { activate, deactivate, prefetch: warm }; 204 205 // Warm the volumes as soon as the visitor heads for the research section. 206 for (const el of document.querySelectorAll( 207 'a[href="#research"], .research-link' 208 )) { 209 el.addEventListener("click", warm, { once: true }); 210 } 211 212 const researchSection = document.getElementById("research"); 213 if (researchSection?.classList.contains("active")) { 214 warm(); 215 } 216})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.