arrayview 0.47.5__tar.gz → 0.48.0__tar.gz

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.
Files changed (88) hide show
  1. {arrayview-0.47.5 → arrayview-0.48.0}/PKG-INFO +1 -1
  2. {arrayview-0.47.5 → arrayview-0.48.0}/pyproject.toml +1 -1
  3. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_io.py +25 -7
  4. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_render.py +3 -1
  5. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_routes_websocket.py +24 -6
  6. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_viewer.html +98 -53
  7. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_vscode_extension.py +1 -1
  8. arrayview-0.48.0/src/arrayview/arrayview-opener.vsix +0 -0
  9. arrayview-0.47.5/src/arrayview/arrayview-opener.vsix +0 -0
  10. {arrayview-0.47.5 → arrayview-0.48.0}/.gitignore +0 -0
  11. {arrayview-0.47.5 → arrayview-0.48.0}/LICENSE +0 -0
  12. {arrayview-0.47.5 → arrayview-0.48.0}/README.md +0 -0
  13. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/ARCHITECTURE.md +0 -0
  14. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/__init__.py +0 -0
  15. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/__main__.py +0 -0
  16. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_analysis.py +0 -0
  17. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_app.py +0 -0
  18. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_bootstrap_app.py +0 -0
  19. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_config.py +0 -0
  20. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_diagnostics.py +0 -0
  21. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_dicom.py +0 -0
  22. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_diff.py +0 -0
  23. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_extra_ports.py +0 -0
  24. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_icon.png +0 -0
  25. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_imaging.py +0 -0
  26. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_instance_registry.py +0 -0
  27. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_launch_plan.py +0 -0
  28. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_launch_trace.py +0 -0
  29. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_launcher.py +0 -0
  30. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_lifecycle.py +0 -0
  31. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_overlays.py +0 -0
  32. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_platform.py +0 -0
  33. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_routes_analysis.py +0 -0
  34. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_routes_drop.py +0 -0
  35. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_routes_export.py +0 -0
  36. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_routes_loading.py +0 -0
  37. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_routes_persistence.py +0 -0
  38. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_routes_preferences.py +0 -0
  39. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_routes_preload.py +0 -0
  40. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_routes_query.py +0 -0
  41. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_routes_rendering.py +0 -0
  42. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_routes_segmentation.py +0 -0
  43. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_routes_state.py +0 -0
  44. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_routes_vectorfield.py +0 -0
  45. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_segmentation.py +0 -0
  46. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_server.py +0 -0
  47. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_session.py +0 -0
  48. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_session_spec.py +0 -0
  49. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_shell.html +0 -0
  50. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_source_safety.py +0 -0
  51. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_synthetic_mri.py +0 -0
  52. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_torch.py +0 -0
  53. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_tutorial.py +0 -0
  54. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_vectorfield.py +0 -0
  55. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_vscode.py +0 -0
  56. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_vscode_browser.py +0 -0
  57. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_vscode_signal.py +0 -0
  58. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/_watch.py +0 -0
  59. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/gsap.min.js +0 -0
  60. {arrayview-0.47.5 → arrayview-0.48.0}/src/arrayview/todo.md +0 -0
  61. {arrayview-0.47.5 → arrayview-0.48.0}/tests/capture_patient_loading.py +0 -0
  62. {arrayview-0.47.5 → arrayview-0.48.0}/tests/capture_v_animation.py +0 -0
  63. {arrayview-0.47.5 → arrayview-0.48.0}/tests/conftest.py +0 -0
  64. {arrayview-0.47.5 → arrayview-0.48.0}/tests/make_vectorfield_test_arrays.py +0 -0
  65. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_1d_arrays.py +0 -0
  66. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_api.py +0 -0
  67. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_backend_shared.py +0 -0
  68. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_browser.py +0 -0
  69. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_cli.py +0 -0
  70. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_config.py +0 -0
  71. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_dicom.py +0 -0
  72. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_drop_import.py +0 -0
  73. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_instance_registry.py +0 -0
  74. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_io.py +0 -0
  75. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_large_arrays.py +0 -0
  76. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_launch_blackbox.py +0 -0
  77. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_mode_roundtrip.py +0 -0
  78. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_nifti_meta.py +0 -0
  79. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_nifti_series.py +0 -0
  80. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_rgb_pixel_art.py +0 -0
  81. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_safe_server_stop.py +0 -0
  82. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_source_safety.py +0 -0
  83. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_torch.py +0 -0
  84. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_tutorial.py +0 -0
  85. {arrayview-0.47.5 → arrayview-0.48.0}/tests/test_vsix_bundle.py +0 -0
  86. {arrayview-0.47.5 → arrayview-0.48.0}/tests/ui_audit.py +0 -0
  87. {arrayview-0.47.5 → arrayview-0.48.0}/tests/v_anim_frames/.gitkeep +0 -0
  88. {arrayview-0.47.5 → arrayview-0.48.0}/tests/visual_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: arrayview
3
- Version: 0.47.5
3
+ Version: 0.48.0
4
4
  Summary: Fast multi-dimensional array viewer
5
5
  Project-URL: Home, https://github.com/oscarvanderheide/arrayview
6
6
  Project-URL: Source, https://github.com/oscarvanderheide/arrayview
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arrayview"
7
- version = "0.47.5"
7
+ version = "0.48.0"
8
8
  description = "Fast multi-dimensional array viewer"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.12"
@@ -1748,7 +1748,7 @@ def _viewer_start_index(shape, dims):
1748
1748
  )
1749
1749
 
1750
1750
 
1751
- def prefetch_first_frame_pages(mapped, filepath) -> bool:
1751
+ def prefetch_first_frame_pages(mapped, filepath, progress=None) -> bool:
1752
1752
  """Pull just the pages of the viewer's first frame into memory, in parallel.
1753
1753
 
1754
1754
  When the first frame is one element per page across the whole file (a
@@ -1768,7 +1768,9 @@ def prefetch_first_frame_pages(mapped, filepath) -> bool:
1768
1768
  dims = _viewer_start_dims_for_shape(shape)
1769
1769
  if dims is None:
1770
1770
  return False
1771
- return prefetch_slice_pages(mapped, filepath, dims, _viewer_start_index(shape, dims))
1771
+ return prefetch_slice_pages(
1772
+ mapped, filepath, dims, _viewer_start_index(shape, dims), progress=progress
1773
+ )
1772
1774
 
1773
1775
 
1774
1776
  @functools.lru_cache(maxsize=64)
@@ -1776,7 +1778,7 @@ def _on_known_local_disk_cached(filepath) -> bool:
1776
1778
  return _on_known_local_disk(filepath)
1777
1779
 
1778
1780
 
1779
- def prefetch_slice_pages(mapped, filepath, dims, index) -> bool:
1781
+ def prefetch_slice_pages(mapped, filepath, dims, index, progress=None) -> bool:
1780
1782
  """Fetch the pages of one 2-D slice of a memmap with parallel reads.
1781
1783
 
1782
1784
  Faulting such a slice in through the map is the slow way: readahead drags
@@ -1784,7 +1786,9 @@ def prefetch_slice_pages(mapped, filepath, dims, index) -> bool:
1784
1786
  C-order file), and without readahead each page is its own wait (7.7 s).
1785
1787
  Many small reads in flight take ~0.7 s on a local disk. Only worth it when
1786
1788
  the slice is scattered one element per page; returns False otherwise, or
1787
- off a known local disk, having read nothing.
1789
+ off a known local disk, having read nothing. *progress*, if given, is
1790
+ called with ``(pages_done, pages_total)`` as batches of pages land — this
1791
+ fetch is the whole wait, so it is what a progress line should follow.
1788
1792
  """
1789
1793
  shape = tuple(int(s) for s in mapped.shape)
1790
1794
  strides = tuple(int(s) for s in mapped.strides)
@@ -1815,14 +1819,28 @@ def prefetch_slice_pages(mapped, filepath, dims, index) -> bool:
1815
1819
  except (AttributeError, OSError):
1816
1820
  pass
1817
1821
  offsets = (pages * _MMAP_PAGE_BYTES).tolist()
1822
+ total = len(offsets)
1818
1823
  n = _FIRST_FRAME_PREFETCH_THREADS
1824
+ # Interleaved batches: each one spans the file, so batches finish at
1825
+ # a steady rate and the progress they report moves evenly.
1826
+ batches = max(n, 64)
1827
+ lock = threading.Lock()
1828
+ done = [0]
1819
1829
 
1820
1830
  def _warm(part):
1821
1831
  for offset in part:
1822
1832
  os.pread(fd, _MMAP_PAGE_BYTES, offset)
1833
+ if progress is not None:
1834
+ with lock:
1835
+ done[0] += len(part)
1836
+ finished = done[0]
1837
+ try:
1838
+ progress(finished, total)
1839
+ except Exception:
1840
+ pass
1823
1841
 
1824
1842
  with ThreadPoolExecutor(max_workers=n, thread_name_prefix="arrayview-slice-prefetch") as pool:
1825
- for _ in pool.map(_warm, [offsets[k::n] for k in range(n)]):
1843
+ for _ in pool.map(_warm, [offsets[k::batches] for k in range(batches)]):
1826
1844
  pass
1827
1845
  except OSError:
1828
1846
  return False
@@ -1988,12 +2006,12 @@ def load_data(filepath, key=None, *, progress=None, fast_first_frame=False):
1988
2006
  mapped = np.load(filepath, mmap_mode="r")
1989
2007
  if size >= npy_eager_limit():
1990
2008
  if fast_first_frame:
1991
- prefetch_first_frame_pages(mapped, filepath)
2009
+ prefetch_first_frame_pages(mapped, filepath, progress)
1992
2010
  return mapped
1993
2011
  if mmap_first_frame_is_cheap(mapped, size):
1994
2012
  # Session creation upgrades this to an in-RAM array in the background.
1995
2013
  return mapped
1996
- if fast_first_frame and prefetch_first_frame_pages(mapped, filepath):
2014
+ if fast_first_frame and prefetch_first_frame_pages(mapped, filepath, progress):
1997
2015
  # Same as above: the first frame's pages are already in memory, and
1998
2016
  # the full read runs behind the viewer.
1999
2017
  return mapped
@@ -214,7 +214,9 @@ def extract_slice(session, dim_x, dim_y, idx_list, progress=None):
214
214
  from ._io import prefetch_slice_pages
215
215
 
216
216
  try:
217
- prefetch_slice_pages(data, data.filename, (dim_x, dim_y), idx_list)
217
+ prefetch_slice_pages(
218
+ data, data.filename, (dim_x, dim_y), idx_list, progress=progress
219
+ )
218
220
  except Exception:
219
221
  pass
220
222
  extracted = _extract_with_progress(data, slicer, progress)
@@ -81,9 +81,11 @@ def _render_progress_reporter(
81
81
  Anything finishing inside `quiet_for_s` therefore shows nothing at all,
82
82
  which is what keeps a fast load from flashing a bar on its way past.
83
83
  """
84
- state = {"started": time.perf_counter(), "last": 0.0}
84
+ state = {"started": time.perf_counter(), "last": 0.0, "closed": False}
85
85
 
86
86
  def report(done, total):
87
+ if state["closed"]:
88
+ return
87
89
  now = time.perf_counter()
88
90
  if now - state["started"] < quiet_for_s:
89
91
  return
@@ -98,15 +100,24 @@ def _render_progress_reporter(
98
100
  "total": int(total),
99
101
  "label": label,
100
102
  }
103
+ def _send():
104
+ # Checked again on the loop: an update queued just before the
105
+ # frame was sent would otherwise arrive after it, and the viewer
106
+ # (which clears its bar when a frame lands) kept the bar up.
107
+ if not state["closed"]:
108
+ asyncio.ensure_future(_send_json_quietly(ws, payload))
109
+
101
110
  try:
102
- loop.call_soon_threadsafe(
103
- lambda: asyncio.ensure_future(_send_json_quietly(ws, payload))
104
- )
111
+ loop.call_soon_threadsafe(_send)
105
112
  except RuntimeError:
106
113
  # Loop already closed (viewer went away mid-render); progress is
107
114
  # never worth raising over.
108
115
  pass
109
116
 
117
+ def close():
118
+ state["closed"] = True
119
+
120
+ report.close = close
110
121
  return report
111
122
 
112
123
 
@@ -118,9 +129,11 @@ async def _await_session_reporting_read_progress(ws, sid):
118
129
  holding a line to the viewer while the wait happens.
119
130
 
120
131
  Silent for the first stretch, so an ordinary fast open never flashes a
121
- bar on its way past — the same rule the render progress reporter uses.
132
+ bar on its way past. Shorter than the viewer's own 0.6 s before it shows
133
+ its loading screen (counted from page load, which starts later), so the
134
+ line is ready when the screen appears rather than coming after it.
122
135
  """
123
- quiet_for_s = 0.6
136
+ quiet_for_s = 0.3
124
137
  started = time.perf_counter()
125
138
  waiter = asyncio.ensure_future(wait_for_session_ready(sid))
126
139
  last = None
@@ -694,7 +707,12 @@ def register_websocket_routes(app) -> None:
694
707
 
695
708
  # The pixels exist; nothing after this point reads the array,
696
709
  # and the viewer clears its indicator when the frame lands.
710
+ # Closing the reporter also drops updates still queued for
711
+ # the socket, which would otherwise land after the frame.
712
+ sink = session.progress_sink
697
713
  session.progress_sink = None
714
+ if hasattr(sink, "close"):
715
+ sink.close()
698
716
  render_ms = (time.perf_counter() - render_t0) * 1000.0
699
717
  post_t0 = time.perf_counter()
700
718
  header = np.array([seq, w, h], dtype=np.uint32).tobytes()
@@ -4482,9 +4482,17 @@
4482
4482
  box-shadow: none;
4483
4483
  }
4484
4484
  .qmri-synth-param-value { color: var(--text); text-align: right; font-variant-numeric: tabular-nums; }
4485
+ /* Hidden for the first 600 ms of the page: most opens draw their
4486
+ first frame within that, and a logo that flashes past reads as
4487
+ noise. Only a slower open gets the loading screen (see the script
4488
+ after the overlay markup). */
4485
4489
  #loading-overlay {
4486
4490
  display: flex; flex-direction: column; align-items: center; justify-content: center;
4487
4491
  position: fixed; inset: 0; z-index: 5; pointer-events: none;
4492
+ visibility: hidden; opacity: 0;
4493
+ }
4494
+ #loading-overlay.av-load-shown {
4495
+ visibility: visible; opacity: 1; transition: opacity 0.15s ease;
4488
4496
  }
4489
4497
  #loading-overlay .av-load-logo {
4490
4498
  width: 64px; height: 64px;
@@ -4492,11 +4500,28 @@
4492
4500
  #loading-overlay .av-load-name {
4493
4501
  margin-top: 14px; font-size: 11px; color: var(--muted);
4494
4502
  letter-spacing: 0.1em; text-transform: uppercase;
4495
- opacity: 0; transition: opacity 0.4s ease;
4503
+ min-height: 1.2em; opacity: 0;
4496
4504
  }
4497
4505
  #loading-overlay .av-load-name.visible { opacity: 1; }
4506
+ /* The startup screen is the only loading signal until the first frame:
4507
+ logo, name, and — whenever there is progress to report — this line
4508
+ under the name. The top-edge hairline and the busy panel stay hidden
4509
+ meanwhile (see _reconcileUI), so a slow open never stacks three
4510
+ different indicators. */
4511
+ #loading-overlay .av-load-progress {
4512
+ margin-top: 12px; width: 120px; height: 2px; border-radius: 1px;
4513
+ background: color-mix(in srgb, var(--muted) 35%, transparent);
4514
+ overflow: hidden; opacity: 0; transition: opacity 0.25s ease;
4515
+ }
4516
+ #loading-overlay .av-load-progress.visible { opacity: 1; }
4517
+ #loading-overlay .av-load-progress .bar {
4518
+ height: 100%; width: 0%;
4519
+ background: var(--accent, #4cc9f0);
4520
+ transition: width 0.25s linear;
4521
+ }
4498
4522
  #loading-overlay.av-array-switch .av-load-logo,
4499
- #loading-overlay.av-array-switch .av-load-name { display: none; }
4523
+ #loading-overlay.av-array-switch .av-load-name,
4524
+ #loading-overlay.av-array-switch .av-load-progress { display: none; }
4500
4525
  /* Deliberately outside #loading-overlay. The overlay is centred over the
4501
4526
  canvas, so anything drawn in it sits on top of the slice — wrong for
4502
4527
  progress that continues after the viewer is live and showing data.
@@ -5717,12 +5742,29 @@
5717
5742
  <div id="multi-view-wrap"></div>
5718
5743
  <div id="qmri-view-wrap"></div>
5719
5744
  <div id="stack-mosaic-wrap"></div>
5720
- <div id="loading-overlay"><svg class="av-load-logo av-logo-loading" width="64" height="64" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="16" rx="2.5" fill="#0c0c0c"/><rect id="load-b0" x="2" y="2" width="3" height="3" fill="#3a0ca3"/><rect id="load-b1" x="6.5" y="2" width="3" height="3" fill="#560bad"/><rect id="load-b2" x="11" y="2" width="3" height="3" fill="#c77dff"/><rect id="load-b3" x="2" y="6.5" width="3" height="3" fill="#4361ee"/><rect id="load-b4" x="6.5" y="6.5" width="3" height="3" fill="#4cc9f0"/><rect id="load-b5" x="11" y="6.5" width="3" height="3" fill="#f5c842"/><rect id="load-b6" x="2" y="11" width="3" height="3" fill="#4895ef"/><rect id="load-b7" x="6.5" y="11" width="3" height="3" fill="#80ed99"/><rect id="load-b8" x="11" y="11" width="3" height="3" fill="#f8961e"/></svg><div class="av-load-name"></div></div>
5745
+ <div id="loading-overlay"><svg class="av-load-logo av-logo-loading" width="64" height="64" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="16" rx="2.5" fill="#0c0c0c"/><rect id="load-b0" x="2" y="2" width="3" height="3" fill="#3a0ca3"/><rect id="load-b1" x="6.5" y="2" width="3" height="3" fill="#560bad"/><rect id="load-b2" x="11" y="2" width="3" height="3" fill="#c77dff"/><rect id="load-b3" x="2" y="6.5" width="3" height="3" fill="#4361ee"/><rect id="load-b4" x="6.5" y="6.5" width="3" height="3" fill="#4cc9f0"/><rect id="load-b5" x="11" y="6.5" width="3" height="3" fill="#f5c842"/><rect id="load-b6" x="2" y="11" width="3" height="3" fill="#4895ef"/><rect id="load-b7" x="6.5" y="11" width="3" height="3" fill="#80ed99"/><rect id="load-b8" x="11" y="11" width="3" height="3" fill="#f8961e"/></svg><div class="av-load-name"></div><div class="av-load-progress"><div class="bar"></div></div></div>
5721
5746
  <div id="render-progress"><div class="bar"></div></div>
5722
5747
  <div id="render-progress-text"></div>
5723
5748
  <div id="fft-spinner-overlay"><div class="av-loading-panel"><svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="16" rx="2.5" fill="#0c0c0c"/><rect x="2" y="2" width="3" height="3" fill="#3a0ca3"/><rect x="6.5" y="2" width="3" height="3" fill="#560bad"/><rect x="11" y="2" width="3" height="3" fill="#c77dff"/><rect x="2" y="6.5" width="3" height="3" fill="#4361ee"/><rect x="6.5" y="6.5" width="3" height="3" fill="#4cc9f0"/><rect x="11" y="6.5" width="3" height="3" fill="#f5c842"/><rect x="2" y="11" width="3" height="3" fill="#4895ef"/><rect x="6.5" y="11" width="3" height="3" fill="#80ed99"/><rect x="11" y="11" width="3" height="3" fill="#f8961e"/></svg><span>Computing FFT…</span></div></div>
5724
5749
  <div id="busy-overlay" role="status" aria-live="polite" aria-hidden="true"><div class="av-loading-panel"><svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="16" rx="2.5" fill="#0c0c0c"/><rect x="2" y="2" width="3" height="3" fill="#3a0ca3"/><rect x="6.5" y="2" width="3" height="3" fill="#560bad"/><rect x="11" y="2" width="3" height="3" fill="#c77dff"/><rect x="2" y="6.5" width="3" height="3" fill="#4361ee"/><rect x="6.5" y="6.5" width="3" height="3" fill="#4cc9f0"/><rect x="11" y="6.5" width="3" height="3" fill="#f5c842"/><rect x="2" y="11" width="3" height="3" fill="#4895ef"/><rect x="6.5" y="11" width="3" height="3" fill="#80ed99"/><rect x="11" y="11" width="3" height="3" fill="#f8961e"/></svg><span>Loading…</span></div></div>
5725
5750
  <script>
5751
+ // Show the loading screen only if the first frame has not arrived
5752
+ // 600 ms into the page (a switch between arrays shows its panel
5753
+ // at once). Timed from navigation start, here rather than in the
5754
+ // main script, which may itself take longer than that to arrive.
5755
+ try {
5756
+ const ol = document.getElementById('loading-overlay');
5757
+ setTimeout(() => {
5758
+ if (document.body.classList.contains('av-loading')) ol.classList.add('av-load-shown');
5759
+ }, Math.max(0, 600 - performance.now()));
5760
+ } catch (_) {}
5761
+ // The array's name under the startup logo, so the loading screen
5762
+ // does not change while it is up.
5763
+ try {
5764
+ const name = new URLSearchParams(__ARRAYVIEW_QUERY__ || location.search).get('av_name');
5765
+ const el = document.querySelector('#loading-overlay .av-load-name');
5766
+ if (el && name) { el.textContent = name; el.classList.add('visible'); }
5767
+ } catch (_) {}
5726
5768
  // After a switch between arrays of a multi-array file the page
5727
5769
  // reloads. Show the same small loading panel the switch used, not
5728
5770
  // the startup splash, so no second, larger logo flashes by.
@@ -5734,7 +5776,7 @@
5734
5776
  const panel = document.querySelector('#busy-overlay .av-loading-panel').cloneNode(true);
5735
5777
  panel.querySelector('span').textContent = `Loading ${key}…`;
5736
5778
  panel.classList.add('av-array-switch-panel');
5737
- ol.classList.add('av-array-switch');
5779
+ ol.classList.add('av-array-switch', 'av-load-shown');
5738
5780
  ol.appendChild(panel);
5739
5781
  }
5740
5782
  } catch (_) {}
@@ -5920,6 +5962,17 @@
5920
5962
  // server could not answer immediately (still loading), in which case
5921
5963
  // the viewer asks over the socket exactly as it always has.
5922
5964
  const BOOT_METADATA = __ARRAYVIEW_BOOT_METADATA__;
5965
+ // Launches without ?av_name= still have the name in the page itself.
5966
+ (function () {
5967
+ try {
5968
+ const el = document.querySelector('#loading-overlay .av-load-name');
5969
+ const name = BOOT_METADATA && BOOT_METADATA.name;
5970
+ if (el && !el.textContent && name && name !== 'welcome') {
5971
+ el.textContent = name;
5972
+ el.classList.add('visible');
5973
+ }
5974
+ } catch (_) {}
5975
+ })();
5923
5976
  /* __AV_STATIC_SCRIPT_BELOW__ — nothing after this line is substituted at serve time. */
5924
5977
  const urlParams = new URLSearchParams(_arrayViewLaunchSearch);
5925
5978
  const _isIntegratedBrowserViewer = (
@@ -7028,6 +7081,7 @@
7028
7081
  [arrayName, info, cbWrap, helpHint].forEach(el => {
7029
7082
  if (!el) return;
7030
7083
  el.style.removeProperty('opacity');
7084
+ el.style.removeProperty('transition');
7031
7085
  el.style.removeProperty('--fly-y');
7032
7086
  el.style.removeProperty('scale');
7033
7087
  el.style.removeProperty('transformOrigin');
@@ -7086,15 +7140,11 @@
7086
7140
  const helpHint = document.getElementById('help-hint');
7087
7141
  const cbVisible = cbWrap && getComputedStyle(cbWrap).display !== 'none';
7088
7142
 
7089
- // Seize opacity immediately so CSS transition doesn't race GSAP
7090
- gsap.set([canvasWrap, arrayName, info, helpHint], { opacity: 0 });
7091
- if (cbVisible) gsap.set(cbWrap, { opacity: 0 });
7092
-
7093
- // Set starting transforms for Act 3 (chrome assembly)
7094
- if (arrayName) gsap.set(arrayName, { '--fly-y': '-8px' });
7095
- if (info) gsap.set(info, { '--fly-y': '-8px' });
7096
- if (cbVisible) gsap.set(cbWrap, { '--fly-y': '12px' });
7097
- if (helpHint) gsap.set(helpHint, { scale: 0.55, transformOrigin: 'center center' });
7143
+ // Seize opacity immediately, and switch off their CSS opacity
7144
+ // transition: left on, it re-eases every step of the fade below
7145
+ // and stretches it to twice its length.
7146
+ gsap.set([canvasWrap, arrayName, info, helpHint], { opacity: 0, transition: 'none' });
7147
+ if (cbVisible) gsap.set(cbWrap, { opacity: 0, transition: 'none' });
7098
7148
 
7099
7149
  _startupAnimTl = gsap.timeline({ onComplete: () => {
7100
7150
  clearTimeout(_safety);
@@ -7103,47 +7153,28 @@
7103
7153
  if (ol) ol.style.display = 'none';
7104
7154
  }});
7105
7155
 
7106
- // ── Act 1: Logo settles (stops pulsing, all blocks snap to full) ──
7156
+ // The logo stops pulsing, and the loading screen hands over to the
7157
+ // viewer in one short cross-fade: picture and its labels arrive
7158
+ // together. (They used to arrive in two waves, the labels flying in
7159
+ // after the picture, which read as yet another loading step.)
7107
7160
  _startupAnimTl.call(() => {
7108
7161
  if (loadLogo) loadLogo.classList.remove('av-logo-loading');
7109
7162
  }, null, 0);
7110
-
7111
- // ── Act 2: Overlay exits, canvas revealed ──────────────────────
7112
- if (loadLogo) {
7113
- _startupAnimTl.to(loadLogo, {
7114
- opacity: 0, scale: 0.6, duration: 0.18, ease: 'power2.in'
7115
- }, 0.05);
7116
- }
7117
- if (loadName) {
7118
- _startupAnimTl.to(loadName, {
7119
- opacity: 0, duration: 0.14, ease: 'power2.in'
7120
- }, 0.05);
7121
- }
7122
- _startupAnimTl.to(canvasWrap, {
7123
- opacity: 1, duration: 0.22, ease: 'power2.out'
7124
- }, 0.08);
7163
+ const loadProgress = ol ? ol.querySelector('.av-load-progress') : null;
7164
+ const leaving = [loadLogo, loadName, loadProgress].filter(Boolean);
7165
+ if (leaving.length) {
7166
+ _startupAnimTl.to(leaving, { opacity: 0, duration: 0.16, ease: 'power2.in' }, 0);
7167
+ }
7168
+ const arriving = [canvasWrap, arrayName, info, helpHint].filter(Boolean);
7169
+ if (cbVisible) arriving.push(cbWrap);
7170
+ _startupAnimTl.to(arriving, { opacity: 1, duration: 0.2, ease: 'power2.out' }, 0.06);
7125
7171
  _startupAnimTl.call(() => {
7126
7172
  document.body.classList.remove('av-loading');
7127
7173
  _reconcileUI();
7128
- }, null, 0.08);
7174
+ }, null, 0.06);
7129
7175
  _startupAnimTl.call(() => {
7130
7176
  if (ol) ol.style.display = 'none';
7131
- }, null, 0.30);
7132
-
7133
- // ── Act 3: Chrome assembles around canvas ──────────────────────
7134
- _startupAnimTl
7135
- .to(arrayName, { opacity: 1, '--fly-y': '0px', duration: 0.26, ease: 'power3.out' }, 0.32)
7136
- .to(info, { opacity: 1, '--fly-y': '0px', duration: 0.26, ease: 'power3.out' }, 0.38);
7137
- if (cbVisible) {
7138
- _startupAnimTl.to(cbWrap, {
7139
- opacity: 1, '--fly-y': '0px', duration: 0.24, ease: 'power3.out'
7140
- }, 0.48);
7141
- }
7142
- if (helpHint) {
7143
- _startupAnimTl.to(helpHint, {
7144
- opacity: 1, scale: 1, duration: 0.22, ease: 'back.out(1.7)'
7145
- }, 0.58);
7146
- }
7177
+ }, null, 0.26);
7147
7178
  }
7148
7179
  let _immersiveDriveTween = null; // tween that scrubs immersiveTl.progress()
7149
7180
  let _immTargetZoom = 1.0; // cached zoom target at start of transition
@@ -15925,7 +15956,12 @@
15925
15956
  } else if (msg.type === 'render_timing' && _PERF_ENABLED) {
15926
15957
  _perfHandleTimingMessage(event.data, 'main');
15927
15958
  } else if (msg.type === 'render_progress') {
15928
- _showRenderProgress(msg.done, msg.total, msg.label);
15959
+ // Only while a frame is actually awaited: a late
15960
+ // report for a frame already drawn would show a
15961
+ // bar that nothing then clears.
15962
+ if (isRendering || !_reportedFirstFrame) {
15963
+ _showRenderProgress(msg.done, msg.total, msg.label);
15964
+ }
15929
15965
  } else if (msg.type === 'warm_path') {
15930
15966
  _warmClientPath(msg.connections);
15931
15967
  } else if (msg.type === 'render_error') {
@@ -17037,6 +17073,8 @@
17037
17073
  }
17038
17074
  function _showRenderProgress(done, total, label) {
17039
17075
  if (!total || done > total) return;
17076
+ const startupBar = document.querySelector('#loading-overlay .av-load-progress .bar');
17077
+ if (startupBar) startupBar.style.width = ((done / total) * 100).toFixed(1) + '%';
17040
17078
  const bar = document.querySelector('#render-progress .bar');
17041
17079
  const text = document.getElementById('render-progress-text');
17042
17080
  if (!bar || !text) return;
@@ -41906,22 +41944,29 @@
41906
41944
  _reconcileMainLoupeState();
41907
41945
  _reconcileInfoCursor();
41908
41946
  _reconcileModePicker();
41947
+ // Until the first frame the startup screen owns every loading
41948
+ // signal; its own line stands in for the hairline.
41949
+ const startupScreen = document.body.classList.contains('av-loading');
41950
+ const startupProgress = document.querySelector('#loading-overlay .av-load-progress');
41951
+ if (startupProgress) {
41952
+ startupProgress.classList.toggle('visible', startupScreen && _renderProgressVisible);
41953
+ }
41909
41954
  const busyOverlay = document.getElementById('busy-overlay');
41910
41955
  if (busyOverlay) {
41911
- const busy = _patientLoadingVisible || _slowRenderBusyVisible || _smPageBusyVisible;
41956
+ const busy = !startupScreen
41957
+ && (_patientLoadingVisible || _slowRenderBusyVisible || _smPageBusyVisible);
41912
41958
  busyOverlay.classList.toggle('visible', busy);
41913
41959
  busyOverlay.setAttribute('aria-hidden', busy ? 'false' : 'true');
41914
41960
  }
41961
+ const hairline = _renderProgressVisible && !startupScreen;
41915
41962
  const renderProgress = document.getElementById('render-progress');
41916
41963
  if (renderProgress) {
41917
- renderProgress.classList.toggle('visible', _renderProgressVisible);
41918
- renderProgress.setAttribute(
41919
- 'aria-hidden', _renderProgressVisible ? 'false' : 'true'
41920
- );
41964
+ renderProgress.classList.toggle('visible', hairline);
41965
+ renderProgress.setAttribute('aria-hidden', hairline ? 'false' : 'true');
41921
41966
  }
41922
41967
  const renderProgressText = document.getElementById('render-progress-text');
41923
41968
  if (renderProgressText) {
41924
- renderProgressText.classList.toggle('visible', _renderProgressVisible);
41969
+ renderProgressText.classList.toggle('visible', hairline);
41925
41970
  }
41926
41971
  const dropMenu = document.getElementById('drop-action-menu');
41927
41972
  if (dropMenu) {
@@ -58,7 +58,7 @@ _VSCODE_EXT_FRESH_INSTALL = False # True if we just installed it this session
58
58
  _VSCODE_EXT_RELOAD_REQUIRED = False # installed files are newer than the live host
59
59
  _VSCODE_EXT_INSTALL_FAILED = False # automatic install could not complete safely
60
60
  _VSCODE_EXT_NO_LIVE_WINDOW = False # no live host claims this terminal's window id
61
- _VSCODE_EXT_VERSION = "0.15.64" # current bundled extension version
61
+ _VSCODE_EXT_VERSION = "0.15.70" # current bundled extension version
62
62
  _VSCODE_CONFIGURED_PORTS: set[int] = set()
63
63
  # Version-skew notices already printed, so a long-lived process (Jupyter, a
64
64
  # script opening several arrays) states the mismatch once instead of per call.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes