nv-mzephyr Claude Opus 5 (1M context) commited on
Commit
3855d49
·
1 Parent(s): 3452775

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>

Files changed (1) hide show
  1. viewer/niivue_embed.py +34 -5
viewer/niivue_embed.py CHANGED
@@ -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
- #status {{ position: absolute; top: 8px; left: 12px; color: #8c93ad; font: 12px -apple-system, BlinkMacSystemFont, sans-serif; pointer-events: none; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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) {{ if (statusEl) statusEl.textContent = 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
- setStatus(statusEl && statusEl.dataset.timing ? statusEl.dataset.timing : '');
 
 
 
 
 
 
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
- from utils.nifti_io import viewer_copy
 
 
 
 
280
 
281
- vol_src = viewer_copy(volume_path) if DOWNCAST_VIEWER else volume_path
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