Spaces:
Runtime error
Centre the viewer status text so it clears the pane labels
Browse files#status sat at top:8px/left:12px, directly under the "Coronal" pane label at
top:10px/left:10px, so "Downloading volume..." overlapped it. The four pane
labels own the corners, so the status moves to the centre of the canvas as a
pill with a backdrop, which is also where a loading indicator reads best.
Two follow-ons: #status:empty is transparent so no empty pill floats over the
image when idle, and the download/decode timing shows for 3.5s then fades
rather than sitting permanently on top of the volume. setStatus() clears the
fade class so a re-render is visible again.
Also moved the viewer_copy import inside the DOWNCAST_VIEWER branch. It ran
unconditionally, so NV_GENERATE_DOWNCAST_VIEWER=0 still required nibabel to be
importable -- which defeats the point of an escape hatch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- viewer/niivue_embed.py +34 -5
|
@@ -75,7 +75,22 @@ def _build_iframe_doc(cfg_json: str, container_id: str) -> str:
|
|
| 75 |
<style>
|
| 76 |
html, body {{ margin: 0; padding: 0; height: 100%; background: #0b1020; overflow: hidden; }}
|
| 77 |
#{container_id} {{ display: block; width: 100%; height: 100%; background: #0b1020; }}
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
#err {{ position: absolute; bottom: 8px; left: 12px; right: 12px; color: #ff8d8d; font: 12px monospace; white-space: pre-wrap; pointer-events: none; }}
|
| 80 |
/* Pane labels overlaid at the outer corner of each MPR quadrant */
|
| 81 |
.pane-labels {{ position: absolute; inset: 0; pointer-events: none; }}
|
|
@@ -110,7 +125,11 @@ def _build_iframe_doc(cfg_json: str, container_id: str) -> str:
|
|
| 110 |
const cfg = {cfg_json};
|
| 111 |
const statusEl = document.getElementById('status');
|
| 112 |
const errEl = document.getElementById('err');
|
| 113 |
-
function setStatus(t) {{
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
function showErr(e) {{ if (errEl) errEl.textContent = String(e && e.stack || e); console.error(e); }}
|
| 115 |
|
| 116 |
function b64ToBlobUrl(b64, mime) {{
|
|
@@ -235,7 +254,13 @@ function b64ToBlobUrl(b64, mime) {{
|
|
| 235 |
|
| 236 |
nv.setSliceType(nv.sliceTypeMultiplanar);
|
| 237 |
nv.updateGLVolume();
|
| 238 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
}} catch (e) {{ showErr(e); }}
|
| 240 |
}})();
|
| 241 |
</script>
|
|
@@ -276,9 +301,13 @@ def render_viewer(
|
|
| 276 |
# still crosses the wire compressed, but the browser inflates half as
|
| 277 |
# many bytes and uploads half the texture, which is where the wait
|
| 278 |
# actually was: download measured 1.5s, decode was the rest.
|
| 279 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
|
| 281 |
-
|
| 282 |
mask_src = mask_path # masks are label maps, already integral
|
| 283 |
vol_url = _file_url(vol_src)
|
| 284 |
mask_url = _file_url(mask_src) if mask_src else None
|
|
|
|
| 75 |
<style>
|
| 76 |
html, body {{ margin: 0; padding: 0; height: 100%; background: #0b1020; overflow: hidden; }}
|
| 77 |
#{container_id} {{ display: block; width: 100%; height: 100%; background: #0b1020; }}
|
| 78 |
+
/* Centred rather than top-left: the four pane labels own the corners, and a
|
| 79 |
+
status pill at top-left collided with "Coronal". */
|
| 80 |
+
#status {{
|
| 81 |
+
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
| 82 |
+
color: #cfd6ea; font: 12px/1.4 -apple-system, BlinkMacSystemFont, sans-serif;
|
| 83 |
+
text-align: center; white-space: nowrap;
|
| 84 |
+
padding: 8px 14px;
|
| 85 |
+
background: rgba(8, 16, 36, 0.78);
|
| 86 |
+
border: 1px solid rgba(170, 200, 255, 0.18);
|
| 87 |
+
backdrop-filter: blur(4px);
|
| 88 |
+
pointer-events: none;
|
| 89 |
+
opacity: 1; transition: opacity 400ms ease;
|
| 90 |
+
}}
|
| 91 |
+
/* No empty pill floating over the image once loading is done. */
|
| 92 |
+
#status:empty {{ opacity: 0; }}
|
| 93 |
+
#status.fade {{ opacity: 0; }}
|
| 94 |
#err {{ position: absolute; bottom: 8px; left: 12px; right: 12px; color: #ff8d8d; font: 12px monospace; white-space: pre-wrap; pointer-events: none; }}
|
| 95 |
/* Pane labels overlaid at the outer corner of each MPR quadrant */
|
| 96 |
.pane-labels {{ position: absolute; inset: 0; pointer-events: none; }}
|
|
|
|
| 125 |
const cfg = {cfg_json};
|
| 126 |
const statusEl = document.getElementById('status');
|
| 127 |
const errEl = document.getElementById('err');
|
| 128 |
+
function setStatus(t) {{
|
| 129 |
+
if (!statusEl) return;
|
| 130 |
+
statusEl.textContent = t;
|
| 131 |
+
statusEl.classList.remove('fade');
|
| 132 |
+
}}
|
| 133 |
function showErr(e) {{ if (errEl) errEl.textContent = String(e && e.stack || e); console.error(e); }}
|
| 134 |
|
| 135 |
function b64ToBlobUrl(b64, mime) {{
|
|
|
|
| 254 |
|
| 255 |
nv.setSliceType(nv.sliceTypeMultiplanar);
|
| 256 |
nv.updateGLVolume();
|
| 257 |
+
// Show the timing split briefly, then fade -- useful to see, not to keep.
|
| 258 |
+
if (statusEl && statusEl.dataset.timing) {{
|
| 259 |
+
setStatus(statusEl.dataset.timing);
|
| 260 |
+
setTimeout(() => {{ statusEl.classList.add('fade'); }}, 3500);
|
| 261 |
+
}} else {{
|
| 262 |
+
setStatus('');
|
| 263 |
+
}}
|
| 264 |
}} catch (e) {{ showErr(e); }}
|
| 265 |
}})();
|
| 266 |
</script>
|
|
|
|
| 301 |
# still crosses the wire compressed, but the browser inflates half as
|
| 302 |
# many bytes and uploads half the texture, which is where the wait
|
| 303 |
# actually was: download measured 1.5s, decode was the rest.
|
| 304 |
+
vol_src = volume_path
|
| 305 |
+
if DOWNCAST_VIEWER:
|
| 306 |
+
# Imported here, not at module scope: with downcasting off the
|
| 307 |
+
# viewer must not depend on nibabel being importable at all.
|
| 308 |
+
from utils.nifti_io import viewer_copy
|
| 309 |
|
| 310 |
+
vol_src = viewer_copy(volume_path)
|
| 311 |
mask_src = mask_path # masks are label maps, already integral
|
| 312 |
vol_url = _file_url(vol_src)
|
| 313 |
mask_url = _file_url(mask_src) if mask_src else None
|