arrayview 0.47.4__py3-none-any.whl → 0.48.0__py3-none-any.whl
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.
- arrayview/_io.py +368 -44
- arrayview/_launcher.py +1 -0
- arrayview/_render.py +13 -1
- arrayview/_routes_loading.py +1 -0
- arrayview/_routes_query.py +1 -0
- arrayview/_routes_websocket.py +24 -6
- arrayview/_session.py +10 -7
- arrayview/_viewer.html +181 -66
- arrayview/_vscode_extension.py +1 -1
- arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.47.4.dist-info → arrayview-0.48.0.dist-info}/METADATA +1 -1
- {arrayview-0.47.4.dist-info → arrayview-0.48.0.dist-info}/RECORD +15 -15
- {arrayview-0.47.4.dist-info → arrayview-0.48.0.dist-info}/WHEEL +0 -0
- {arrayview-0.47.4.dist-info → arrayview-0.48.0.dist-info}/entry_points.txt +0 -0
- {arrayview-0.47.4.dist-info → arrayview-0.48.0.dist-info}/licenses/LICENSE +0 -0
arrayview/_io.py
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import fnmatch
|
|
6
|
+
import functools
|
|
6
7
|
import gzip
|
|
7
8
|
import glob
|
|
8
9
|
import os
|
|
9
10
|
import re
|
|
11
|
+
import sys
|
|
10
12
|
import threading
|
|
13
|
+
import zlib
|
|
11
14
|
from concurrent.futures import ThreadPoolExecutor
|
|
12
15
|
from collections import OrderedDict
|
|
13
16
|
|
|
@@ -114,6 +117,23 @@ def list_npz_keys(filepath):
|
|
|
114
117
|
return keys
|
|
115
118
|
|
|
116
119
|
|
|
120
|
+
# MATLAB class -> dtype scipy.io.loadmat returns for it (complex is not
|
|
121
|
+
# visible in the header; the picker only shows this when several arrays exist).
|
|
122
|
+
_MAT_CLASS_DTYPES = {
|
|
123
|
+
"double": "float64",
|
|
124
|
+
"single": "float32",
|
|
125
|
+
"int8": "int8",
|
|
126
|
+
"uint8": "uint8",
|
|
127
|
+
"int16": "int16",
|
|
128
|
+
"uint16": "uint16",
|
|
129
|
+
"int32": "int32",
|
|
130
|
+
"uint32": "uint32",
|
|
131
|
+
"int64": "int64",
|
|
132
|
+
"uint64": "uint64",
|
|
133
|
+
"logical": "uint8",
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
117
137
|
def list_mat_keys(filepath):
|
|
118
138
|
"""Return [{key, shape, dtype}] for each array in a .mat file.
|
|
119
139
|
|
|
@@ -123,6 +143,17 @@ def list_mat_keys(filepath):
|
|
|
123
143
|
try:
|
|
124
144
|
import scipy.io
|
|
125
145
|
|
|
146
|
+
# The headers alone settle the common single-array file, whose array
|
|
147
|
+
# is loaded right after this anyway; reading it here too doubled the
|
|
148
|
+
# wait on network shares.
|
|
149
|
+
numeric = [
|
|
150
|
+
(name, shape, cls)
|
|
151
|
+
for name, shape, cls in scipy.io.whosmat(filepath)
|
|
152
|
+
if cls in _MAT_CLASS_DTYPES
|
|
153
|
+
]
|
|
154
|
+
if len(numeric) == 1:
|
|
155
|
+
name, shape, cls = numeric[0]
|
|
156
|
+
return [{"key": name, "shape": list(shape), "dtype": _MAT_CLASS_DTYPES[cls]}]
|
|
126
157
|
mat = scipy.io.loadmat(filepath)
|
|
127
158
|
keys = []
|
|
128
159
|
for k, v in mat.items():
|
|
@@ -435,12 +466,100 @@ def _load_nifti_with_meta(filepath):
|
|
|
435
466
|
random-access, but it is no longer silently reordered.
|
|
436
467
|
"""
|
|
437
468
|
img, meta = _nifti_header_with_meta(filepath, native=True)
|
|
438
|
-
|
|
439
|
-
img.dataobj
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
469
|
+
if filepath.endswith(".nii"):
|
|
470
|
+
return img.dataobj, meta
|
|
471
|
+
source = img
|
|
472
|
+
if _nifti_voxel_bytes(img) <= npy_eager_limit():
|
|
473
|
+
raw = read_gzip_file_parallel(filepath)
|
|
474
|
+
if raw is not None:
|
|
475
|
+
try:
|
|
476
|
+
source = type(img).from_bytes(raw)
|
|
477
|
+
except Exception:
|
|
478
|
+
source = img
|
|
479
|
+
return _nifti_display_array(source.dataobj), meta
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _nifti_voxel_bytes(img) -> int:
|
|
483
|
+
try:
|
|
484
|
+
return int(np.prod(img.shape)) * np.dtype(img.get_data_dtype()).itemsize
|
|
485
|
+
except Exception:
|
|
486
|
+
return 1 << 62
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
_GZIP_READ_CHUNK_BYTES = 8 << 20
|
|
490
|
+
_GZIP_READ_THREADS = 8
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def read_gzip_file_parallel(filepath):
|
|
494
|
+
"""Decompress a whole .gz file, reading it with several reads in flight.
|
|
495
|
+
|
|
496
|
+
A gzip stream can only be decoded front to back, so the decode is one
|
|
497
|
+
thread; but the bytes can arrive out of order. Reading 8 MB chunks in
|
|
498
|
+
parallel and decoding each as soon as its turn comes overlaps the two, so
|
|
499
|
+
on an SMB share the wait is close to the parallel read alone (~1.3 s for a
|
|
500
|
+
110 MB file) instead of a single slow reader followed by the decode.
|
|
501
|
+
Handles multi-member files. Returns None on any read or decode error so
|
|
502
|
+
the caller can fall back to its ordinary reader.
|
|
503
|
+
"""
|
|
504
|
+
try:
|
|
505
|
+
size = os.path.getsize(filepath)
|
|
506
|
+
fd = os.open(filepath, os.O_RDONLY)
|
|
507
|
+
except OSError:
|
|
508
|
+
return None
|
|
509
|
+
buf = bytearray(size)
|
|
510
|
+
view = memoryview(buf)
|
|
511
|
+
starts = list(range(0, size, _GZIP_READ_CHUNK_BYTES))
|
|
512
|
+
ready = [threading.Event() for _ in starts]
|
|
513
|
+
failed = []
|
|
514
|
+
|
|
515
|
+
def _read(i):
|
|
516
|
+
start = starts[i]
|
|
517
|
+
stop = min(start + _GZIP_READ_CHUNK_BYTES, size)
|
|
518
|
+
pos = start
|
|
519
|
+
try:
|
|
520
|
+
while pos < stop:
|
|
521
|
+
n = os.preadv(fd, [view[pos:stop]], pos)
|
|
522
|
+
if n <= 0:
|
|
523
|
+
raise OSError("unexpected end of file")
|
|
524
|
+
pos += n
|
|
525
|
+
except Exception as exc: # noqa: BLE001 - reported to the decoder
|
|
526
|
+
failed.append(exc)
|
|
527
|
+
finally:
|
|
528
|
+
ready[i].set()
|
|
529
|
+
|
|
530
|
+
out = []
|
|
531
|
+
try:
|
|
532
|
+
with ThreadPoolExecutor(
|
|
533
|
+
max_workers=max(1, min(_GZIP_READ_THREADS, len(starts))),
|
|
534
|
+
thread_name_prefix="arrayview-gzip-read",
|
|
535
|
+
) as pool:
|
|
536
|
+
for i in range(len(starts)):
|
|
537
|
+
pool.submit(_read, i)
|
|
538
|
+
decoder = zlib.decompressobj(wbits=31)
|
|
539
|
+
fed = False
|
|
540
|
+
for i, start in enumerate(starts):
|
|
541
|
+
ready[i].wait()
|
|
542
|
+
if failed:
|
|
543
|
+
return None
|
|
544
|
+
chunk = view[start:min(start + _GZIP_READ_CHUNK_BYTES, size)]
|
|
545
|
+
while chunk:
|
|
546
|
+
fed = True
|
|
547
|
+
out.append(decoder.decompress(chunk))
|
|
548
|
+
if not decoder.eof:
|
|
549
|
+
break
|
|
550
|
+
# End of one gzip member: any rest starts the next one.
|
|
551
|
+
chunk = decoder.unused_data
|
|
552
|
+
decoder = zlib.decompressobj(wbits=31)
|
|
553
|
+
fed = False
|
|
554
|
+
if fed and not decoder.eof:
|
|
555
|
+
return None # truncated: let the ordinary reader report it
|
|
556
|
+
if not out:
|
|
557
|
+
return None
|
|
558
|
+
except (zlib.error, OSError):
|
|
559
|
+
return None
|
|
560
|
+
finally:
|
|
561
|
+
os.close(fd)
|
|
562
|
+
return b"".join(out)
|
|
444
563
|
|
|
445
564
|
|
|
446
565
|
# ---------------------------------------------------------------------------
|
|
@@ -1601,41 +1720,238 @@ def mmap_first_frame_is_cheap(arr, total_bytes) -> bool:
|
|
|
1601
1720
|
return runs <= max_runs and faulted <= fraction * total_bytes
|
|
1602
1721
|
|
|
1603
1722
|
|
|
1604
|
-
|
|
1605
|
-
|
|
1723
|
+
# Fetching the first frame's pages one by one beats reading the whole file
|
|
1724
|
+
# only on a local disk: on an SMB share the same 65k small reads of a 1.6 GB
|
|
1725
|
+
# file took 11 s against 0.7 s locally.
|
|
1726
|
+
_FIRST_FRAME_PREFETCH_MAX_PAGES = 1 << 17
|
|
1727
|
+
_FIRST_FRAME_PREFETCH_THREADS = 16
|
|
1606
1728
|
|
|
1607
|
-
For a large array this read is the whole wait between a click and the
|
|
1608
|
-
first frame — 9 s for a 1.6 GB file on a cold cache — and ``np.load``
|
|
1609
|
-
reports nothing while it runs. Copying in pieces costs the same reads
|
|
1610
|
-
and makes the wait explainable.
|
|
1611
1729
|
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1730
|
+
def _on_known_local_disk(filepath) -> bool:
|
|
1731
|
+
if not sys.platform.startswith("linux"):
|
|
1732
|
+
return False
|
|
1733
|
+
try:
|
|
1734
|
+
from ._source_safety import _KNOWN_LOCAL_STAGE_FILESYSTEMS, mount_for_path
|
|
1735
|
+
|
|
1736
|
+
mount = mount_for_path(os.path.realpath(filepath))
|
|
1737
|
+
except Exception:
|
|
1738
|
+
return False
|
|
1739
|
+
return mount is not None and mount.filesystem in _KNOWN_LOCAL_STAGE_FILESYSTEMS
|
|
1740
|
+
|
|
1741
|
+
|
|
1742
|
+
def _viewer_start_index(shape, dims):
|
|
1743
|
+
"""The slice the viewer opens on; mirrors its ``_initialIndices``."""
|
|
1744
|
+
ref = max(shape[dims[0]], shape[dims[1]])
|
|
1745
|
+
return tuple(
|
|
1746
|
+
size // 2 if (i in dims or size >= 0.25 * ref) else 0
|
|
1747
|
+
for i, size in enumerate(shape)
|
|
1748
|
+
)
|
|
1749
|
+
|
|
1750
|
+
|
|
1751
|
+
def prefetch_first_frame_pages(mapped, filepath, progress=None) -> bool:
|
|
1752
|
+
"""Pull just the pages of the viewer's first frame into memory, in parallel.
|
|
1753
|
+
|
|
1754
|
+
When the first frame is one element per page across the whole file (a
|
|
1755
|
+
C-order array viewed along its leading axes), reading the file first made
|
|
1756
|
+
a 1.6 GB array wait 3-4 s cold for its picture. Fetching only the pages the
|
|
1757
|
+
frame touches, many at a time, takes ~0.7 s; the memmap then renders the
|
|
1758
|
+
frame from memory while the full read continues in the background.
|
|
1759
|
+
|
|
1760
|
+
Returns False, having read nothing, when this would not pay off: not a
|
|
1761
|
+
known local disk, the viewer would start elsewhere, or too many pages.
|
|
1615
1762
|
"""
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1763
|
+
from ._session import _default_start_dims_for_data, _viewer_start_dims_for_shape
|
|
1764
|
+
|
|
1765
|
+
shape = tuple(int(s) for s in mapped.shape)
|
|
1766
|
+
if len(shape) < 3 or _default_start_dims_for_data(mapped) is not None:
|
|
1767
|
+
return False
|
|
1768
|
+
dims = _viewer_start_dims_for_shape(shape)
|
|
1769
|
+
if dims is None:
|
|
1770
|
+
return False
|
|
1771
|
+
return prefetch_slice_pages(
|
|
1772
|
+
mapped, filepath, dims, _viewer_start_index(shape, dims), progress=progress
|
|
1773
|
+
)
|
|
1774
|
+
|
|
1775
|
+
|
|
1776
|
+
@functools.lru_cache(maxsize=64)
|
|
1777
|
+
def _on_known_local_disk_cached(filepath) -> bool:
|
|
1778
|
+
return _on_known_local_disk(filepath)
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
def prefetch_slice_pages(mapped, filepath, dims, index, progress=None) -> bool:
|
|
1782
|
+
"""Fetch the pages of one 2-D slice of a memmap with parallel reads.
|
|
1783
|
+
|
|
1784
|
+
Faulting such a slice in through the map is the slow way: readahead drags
|
|
1785
|
+
in the pages between the wanted elements (6 s for one slice of a 1.6 GB
|
|
1786
|
+
C-order file), and without readahead each page is its own wait (7.7 s).
|
|
1787
|
+
Many small reads in flight take ~0.7 s on a local disk. Only worth it when
|
|
1788
|
+
the slice is scattered one element per page; returns False otherwise, or
|
|
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.
|
|
1792
|
+
"""
|
|
1793
|
+
shape = tuple(int(s) for s in mapped.shape)
|
|
1794
|
+
strides = tuple(int(s) for s in mapped.strides)
|
|
1795
|
+
runs, _faulted = first_frame_fault_cost(
|
|
1796
|
+
shape, mapped.dtype.itemsize, strides, dims, mapped.nbytes
|
|
1797
|
+
)
|
|
1798
|
+
if runs <= 1024 or not _on_known_local_disk_cached(str(filepath)):
|
|
1799
|
+
return False
|
|
1800
|
+
base = int(mapped.offset) + sum(
|
|
1801
|
+
int(index[i]) * strides[i] for i in range(len(shape)) if i not in dims
|
|
1802
|
+
)
|
|
1803
|
+
rows = np.arange(shape[dims[0]], dtype=np.int64) * strides[dims[0]]
|
|
1804
|
+
cols = np.arange(shape[dims[1]], dtype=np.int64) * strides[dims[1]]
|
|
1805
|
+
starts = (base + rows[:, None] + cols[None, :]).ravel()
|
|
1806
|
+
ends = starts + (mapped.dtype.itemsize - 1)
|
|
1807
|
+
pages = np.unique(np.concatenate((starts, ends)) // _MMAP_PAGE_BYTES)
|
|
1808
|
+
if pages.size > _FIRST_FRAME_PREFETCH_MAX_PAGES:
|
|
1809
|
+
return False
|
|
1810
|
+
|
|
1811
|
+
try:
|
|
1812
|
+
fd = os.open(filepath, os.O_RDONLY)
|
|
1813
|
+
except OSError:
|
|
1814
|
+
return False
|
|
1815
|
+
try:
|
|
1816
|
+
# Only the pages asked for: readahead around each would multiply the I/O.
|
|
1630
1817
|
try:
|
|
1631
|
-
|
|
1632
|
-
except
|
|
1818
|
+
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_RANDOM)
|
|
1819
|
+
except (AttributeError, OSError):
|
|
1633
1820
|
pass
|
|
1821
|
+
offsets = (pages * _MMAP_PAGE_BYTES).tolist()
|
|
1822
|
+
total = len(offsets)
|
|
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]
|
|
1829
|
+
|
|
1830
|
+
def _warm(part):
|
|
1831
|
+
for offset in part:
|
|
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
|
|
1841
|
+
|
|
1842
|
+
with ThreadPoolExecutor(max_workers=n, thread_name_prefix="arrayview-slice-prefetch") as pool:
|
|
1843
|
+
for _ in pool.map(_warm, [offsets[k::batches] for k in range(batches)]):
|
|
1844
|
+
pass
|
|
1845
|
+
except OSError:
|
|
1846
|
+
return False
|
|
1847
|
+
finally:
|
|
1848
|
+
os.close(fd)
|
|
1849
|
+
return True
|
|
1850
|
+
|
|
1851
|
+
|
|
1852
|
+
def _h5_contiguous_memmap(filepath, ds):
|
|
1853
|
+
"""Map an HDF5 dataset straight from the file, or None if it can't be.
|
|
1854
|
+
|
|
1855
|
+
MATLAB v7.3 writes large arrays as one uncompressed contiguous block, so
|
|
1856
|
+
the same first-frame shortcut as .npy applies: the viewer can draw a slice
|
|
1857
|
+
without reading the whole file first. Chunked, compressed or filtered
|
|
1858
|
+
datasets are not one block and return None.
|
|
1859
|
+
"""
|
|
1860
|
+
try:
|
|
1861
|
+
if ds.chunks is not None or ds.compression is not None:
|
|
1862
|
+
return None
|
|
1863
|
+
offset = ds.id.get_offset()
|
|
1864
|
+
if offset is None:
|
|
1865
|
+
return None
|
|
1866
|
+
dtype = ds.dtype
|
|
1867
|
+
if dtype.names:
|
|
1868
|
+
# MATLAB complex: {real, imag} of one float type, packed, which is
|
|
1869
|
+
# byte-for-byte numpy's complex layout.
|
|
1870
|
+
if set(dtype.names) != {"real", "imag"}:
|
|
1871
|
+
return None
|
|
1872
|
+
real_t, real_off = dtype.fields["real"][:2]
|
|
1873
|
+
imag_t, imag_off = dtype.fields["imag"][:2]
|
|
1874
|
+
# Only single precision: the eager path downcasts complex to
|
|
1875
|
+
# complex64, and the mapped array must come out the same.
|
|
1876
|
+
if (
|
|
1877
|
+
real_t != imag_t
|
|
1878
|
+
or real_t.kind != "f"
|
|
1879
|
+
or real_t.itemsize != 4
|
|
1880
|
+
or real_off != 0
|
|
1881
|
+
or imag_off != real_t.itemsize
|
|
1882
|
+
or dtype.itemsize != 2 * real_t.itemsize
|
|
1883
|
+
):
|
|
1884
|
+
return None
|
|
1885
|
+
dtype = np.dtype(f"{real_t.byteorder}c{2 * real_t.itemsize}")
|
|
1886
|
+
elif dtype.kind not in "biuf":
|
|
1887
|
+
return None
|
|
1888
|
+
if offset + int(np.prod(ds.shape)) * dtype.itemsize > os.path.getsize(filepath):
|
|
1889
|
+
return None
|
|
1890
|
+
return np.memmap(filepath, dtype=dtype, mode="r", offset=offset, shape=ds.shape)
|
|
1891
|
+
except Exception:
|
|
1892
|
+
return None
|
|
1893
|
+
|
|
1894
|
+
|
|
1895
|
+
_NPY_READ_CHUNK_BYTES = 8 << 20
|
|
1896
|
+
_NPY_READ_THREADS = 8
|
|
1897
|
+
|
|
1898
|
+
|
|
1899
|
+
def read_npy_into_ram(mapped, progress=None):
|
|
1900
|
+
"""Read a memmapped .npy into RAM with several reads in flight.
|
|
1901
|
+
|
|
1902
|
+
For a large array this read is the whole wait between a click and the
|
|
1903
|
+
first frame. One sequential reader leaves a network share mostly idle:
|
|
1904
|
+
on SMB a 220 MB file took 5.5 s that way and ~3 s with eight readers.
|
|
1905
|
+
Local disks are not slowed by it.
|
|
1906
|
+
|
|
1907
|
+
The reads land directly in the result's memory, in the file's own byte
|
|
1908
|
+
order: a .npy body is contiguous in either C or Fortran order, so the
|
|
1909
|
+
result is allocated in that order and filled byte for byte. *progress*,
|
|
1910
|
+
if given, is called with ``(done_bytes, total_bytes)`` as chunks land.
|
|
1911
|
+
"""
|
|
1912
|
+
order = "F" if mapped.flags.f_contiguous and not mapped.flags.c_contiguous else "C"
|
|
1913
|
+
out = np.empty(mapped.shape, dtype=mapped.dtype, order=order)
|
|
1914
|
+
total = int(out.nbytes)
|
|
1915
|
+
if total == 0:
|
|
1916
|
+
return out
|
|
1917
|
+
dst = memoryview(out.reshape(-1, order="A").view(np.uint8))
|
|
1918
|
+
offset = int(mapped.offset)
|
|
1919
|
+
lock = threading.Lock()
|
|
1920
|
+
done = [0]
|
|
1921
|
+
|
|
1922
|
+
fd = os.open(mapped.filename, os.O_RDONLY)
|
|
1923
|
+
try:
|
|
1924
|
+
def _read(start):
|
|
1925
|
+
stop = min(start + _NPY_READ_CHUNK_BYTES, total)
|
|
1926
|
+
pos = start
|
|
1927
|
+
while pos < stop:
|
|
1928
|
+
n = os.preadv(fd, [dst[pos:stop]], offset + pos)
|
|
1929
|
+
if n <= 0:
|
|
1930
|
+
raise OSError(f"unexpected end of file reading {mapped.filename!r}")
|
|
1931
|
+
pos += n
|
|
1932
|
+
if progress is not None:
|
|
1933
|
+
with lock:
|
|
1934
|
+
done[0] += stop - start
|
|
1935
|
+
finished = done[0]
|
|
1936
|
+
try:
|
|
1937
|
+
progress(finished, total)
|
|
1938
|
+
except Exception:
|
|
1939
|
+
pass
|
|
1940
|
+
|
|
1941
|
+
starts = range(0, total, _NPY_READ_CHUNK_BYTES)
|
|
1942
|
+
with ThreadPoolExecutor(
|
|
1943
|
+
max_workers=min(_NPY_READ_THREADS, len(starts)),
|
|
1944
|
+
thread_name_prefix="arrayview-npy-read",
|
|
1945
|
+
) as pool:
|
|
1946
|
+
for _ in pool.map(_read, starts):
|
|
1947
|
+
pass
|
|
1948
|
+
finally:
|
|
1949
|
+
os.close(fd)
|
|
1634
1950
|
return out
|
|
1635
1951
|
|
|
1636
1952
|
|
|
1637
1953
|
def load_data_with_meta(filepath, key=None, *, load="lazy", stack="auto", select=None,
|
|
1638
|
-
progress=None):
|
|
1954
|
+
progress=None, fast_first_frame=False):
|
|
1639
1955
|
"""Like load_data but also returns spatial metadata for NIfTI files.
|
|
1640
1956
|
|
|
1641
1957
|
Returns (array, meta_or_None). meta is None for non-NIfTI formats.
|
|
@@ -1661,10 +1977,12 @@ def load_data_with_meta(filepath, key=None, *, load="lazy", stack="auto", select
|
|
|
1661
1977
|
return _load_file_series(filepath, load=load, stack=stack)
|
|
1662
1978
|
if filepath.endswith(".nii") or filepath.endswith(".nii.gz"):
|
|
1663
1979
|
return _load_nifti_with_meta(filepath)
|
|
1664
|
-
return load_data(
|
|
1980
|
+
return load_data(
|
|
1981
|
+
filepath, key=key, progress=progress, fast_first_frame=fast_first_frame
|
|
1982
|
+
), None
|
|
1665
1983
|
|
|
1666
1984
|
|
|
1667
|
-
def load_data(filepath, key=None, *, progress=None):
|
|
1985
|
+
def load_data(filepath, key=None, *, progress=None, fast_first_frame=False):
|
|
1668
1986
|
if filepath.lower().endswith(".dcm"):
|
|
1669
1987
|
from ._dicom import load_dicom_series
|
|
1670
1988
|
|
|
@@ -1685,16 +2003,19 @@ def load_data(filepath, key=None, *, progress=None):
|
|
|
1685
2003
|
# read runs behind it (see mmap_first_frame_is_cheap) or the viewer shows
|
|
1686
2004
|
# a spinner until the read completes.
|
|
1687
2005
|
size = os.path.getsize(filepath)
|
|
1688
|
-
if size >= npy_eager_limit():
|
|
1689
|
-
return np.load(filepath, mmap_mode="r")
|
|
1690
2006
|
mapped = np.load(filepath, mmap_mode="r")
|
|
2007
|
+
if size >= npy_eager_limit():
|
|
2008
|
+
if fast_first_frame:
|
|
2009
|
+
prefetch_first_frame_pages(mapped, filepath, progress)
|
|
2010
|
+
return mapped
|
|
1691
2011
|
if mmap_first_frame_is_cheap(mapped, size):
|
|
1692
2012
|
# Session creation upgrades this to an in-RAM array in the background.
|
|
1693
2013
|
return mapped
|
|
1694
|
-
if
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
2014
|
+
if fast_first_frame and prefetch_first_frame_pages(mapped, filepath, progress):
|
|
2015
|
+
# Same as above: the first frame's pages are already in memory, and
|
|
2016
|
+
# the full read runs behind the viewer.
|
|
2017
|
+
return mapped
|
|
2018
|
+
return read_npy_into_ram(mapped, progress)
|
|
1698
2019
|
elif filepath.endswith(".npz"):
|
|
1699
2020
|
npz = np.load(filepath)
|
|
1700
2021
|
try:
|
|
@@ -1766,10 +2087,13 @@ def load_data(filepath, key=None, *, progress=None):
|
|
|
1766
2087
|
|
|
1767
2088
|
f = h5py.File(filepath, "r")
|
|
1768
2089
|
arrays = {k: f[k] for k in f.keys() if _is_viewable_h5_dataset(f[k])}
|
|
1769
|
-
if key is not None:
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
2090
|
+
if key is not None or len(arrays) == 1:
|
|
2091
|
+
ds = arrays[key] if key is not None else next(iter(arrays.values()))
|
|
2092
|
+
mapped = _h5_contiguous_memmap(filepath, ds)
|
|
2093
|
+
if mapped is not None and mmap_first_frame_is_cheap(mapped, mapped.nbytes):
|
|
2094
|
+
# Session creation upgrades this to an in-RAM array in the background.
|
|
2095
|
+
return mapped
|
|
2096
|
+
return _fix_mat_complex(np.array(ds))
|
|
1773
2097
|
raise ValueError(
|
|
1774
2098
|
f".mat (v7.3) file contains multiple datasets: {list(arrays.keys())}. "
|
|
1775
2099
|
"Select one in the viewer or pass a key."
|
arrayview/_launcher.py
CHANGED
arrayview/_render.py
CHANGED
|
@@ -207,7 +207,19 @@ def extract_slice(session, dim_x, dim_y, idx_list, progress=None):
|
|
|
207
207
|
# already served from cache.
|
|
208
208
|
if progress is None:
|
|
209
209
|
progress = getattr(session, 'progress_sink', None)
|
|
210
|
-
|
|
210
|
+
data = session.data
|
|
211
|
+
if isinstance(data, np.memmap) and data.filename:
|
|
212
|
+
# A slice scattered one element per page reads far faster as many
|
|
213
|
+
# parallel small reads than by faulting through the map (local disks).
|
|
214
|
+
from ._io import prefetch_slice_pages
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
prefetch_slice_pages(
|
|
218
|
+
data, data.filename, (dim_x, dim_y), idx_list, progress=progress
|
|
219
|
+
)
|
|
220
|
+
except Exception:
|
|
221
|
+
pass
|
|
222
|
+
extracted = _extract_with_progress(data, slicer, progress)
|
|
211
223
|
if dim_x < dim_y:
|
|
212
224
|
extracted = extracted.T
|
|
213
225
|
# scipy.io.loadmat returns complex arrays as structured dtypes; convert here.
|
arrayview/_routes_loading.py
CHANGED
|
@@ -341,6 +341,7 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
|
|
|
341
341
|
filepath,
|
|
342
342
|
select=body.get("select"),
|
|
343
343
|
progress=_report_read_progress,
|
|
344
|
+
fast_first_frame=True,
|
|
344
345
|
)
|
|
345
346
|
session = Session(data, filepath=filepath, name=name)
|
|
346
347
|
session.sid = sid
|
arrayview/_routes_query.py
CHANGED
|
@@ -247,6 +247,7 @@ def register_query_routes(app, *, get_session_or_404, pil_image, pil_imageops) -
|
|
|
247
247
|
request_id=request_id,
|
|
248
248
|
navigation_attempt=journal.get("navigation_attempt"),
|
|
249
249
|
phase=phase,
|
|
250
|
+
client_ms=body.get("client_ms"),
|
|
250
251
|
)
|
|
251
252
|
except Exception:
|
|
252
253
|
pass
|
arrayview/_routes_websocket.py
CHANGED
|
@@ -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
|
|
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.
|
|
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()
|
arrayview/_session.py
CHANGED
|
@@ -743,12 +743,9 @@ def upgrade_memmap_in_background(session) -> bool:
|
|
|
743
743
|
return False
|
|
744
744
|
|
|
745
745
|
if is_memmap:
|
|
746
|
-
if not filepath.endswith(".npy"):
|
|
747
|
-
return False
|
|
748
|
-
try:
|
|
749
|
-
nbytes = os.path.getsize(filepath)
|
|
750
|
-
except OSError:
|
|
746
|
+
if not filepath.endswith((".npy", ".mat")):
|
|
751
747
|
return False
|
|
748
|
+
nbytes = int(data.nbytes)
|
|
752
749
|
else:
|
|
753
750
|
nbytes = _nifti_proxy_nbytes(data)
|
|
754
751
|
if nbytes >= npy_eager_limit():
|
|
@@ -759,8 +756,14 @@ def upgrade_memmap_in_background(session) -> bool:
|
|
|
759
756
|
|
|
760
757
|
def _upgrade():
|
|
761
758
|
try:
|
|
762
|
-
if is_memmap:
|
|
763
|
-
|
|
759
|
+
if is_memmap and filepath.endswith(".npy"):
|
|
760
|
+
from ._io import read_npy_into_ram
|
|
761
|
+
|
|
762
|
+
loaded = read_npy_into_ram(data)
|
|
763
|
+
elif is_memmap:
|
|
764
|
+
# A block mapped out of a larger file (.mat v7.3): copy it in
|
|
765
|
+
# file order, which is one sequential read.
|
|
766
|
+
loaded = np.array(data, copy=True, order="K")
|
|
764
767
|
else:
|
|
765
768
|
from ._io import _nifti_display_array
|
|
766
769
|
|
arrayview/_viewer.html
CHANGED
|
@@ -52,6 +52,45 @@
|
|
|
52
52
|
} catch (_) {}
|
|
53
53
|
})();
|
|
54
54
|
|
|
55
|
+
// Open the viewer's WebSocket now, while the rest of this page and the
|
|
56
|
+
// large viewer script are still arriving. Through a VS Code tunnel a
|
|
57
|
+
// new connection takes ~0.2 s to set up, and the viewer used to start
|
|
58
|
+
// it only once its whole script had loaded and run. initWebSocket()
|
|
59
|
+
// adopts this socket if the address matches and replays anything it
|
|
60
|
+
// received in the meantime; otherwise it is closed and a fresh one made.
|
|
61
|
+
(function () {
|
|
62
|
+
try {
|
|
63
|
+
var params = new URLSearchParams(__ARRAYVIEW_QUERY__ || location.search);
|
|
64
|
+
var sid = params.get('sid');
|
|
65
|
+
if (
|
|
66
|
+
!sid
|
|
67
|
+
|| params.get('transport') === 'http'
|
|
68
|
+
|| params.get('_av_relay_hedge') === '1'
|
|
69
|
+
|| typeof window.__ARRAYVIEW_SERVER_ORIGIN__ === 'string'
|
|
70
|
+
|| /\/proxy\/\d+(?:\/|$)/.test(location.pathname)
|
|
71
|
+
) return;
|
|
72
|
+
var target = '/ws/' + sid;
|
|
73
|
+
var nativeRequestId = params.get('native_request_id');
|
|
74
|
+
if (nativeRequestId) {
|
|
75
|
+
target += '?native_request_id=' + encodeURIComponent(nativeRequestId);
|
|
76
|
+
}
|
|
77
|
+
var requestId = params.get('_av_launch_request_id');
|
|
78
|
+
var token = params.get('_av_launch_token');
|
|
79
|
+
if (requestId && token) {
|
|
80
|
+
target += (target.indexOf('?') >= 0 ? '&' : '?')
|
|
81
|
+
+ 'launch_request_id=' + encodeURIComponent(requestId)
|
|
82
|
+
+ '&launch_token=' + encodeURIComponent(token);
|
|
83
|
+
}
|
|
84
|
+
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
85
|
+
var url = proto + '//' + location.host + target;
|
|
86
|
+
var early = { url: url, ws: new WebSocket(url), opened: false, events: [] };
|
|
87
|
+
early.ws.binaryType = 'arraybuffer';
|
|
88
|
+
early.ws.onopen = function () { early.opened = true; };
|
|
89
|
+
early.ws.onmessage = function (event) { early.events.push(event); };
|
|
90
|
+
window.__avEarlySocket = early;
|
|
91
|
+
} catch (_) {}
|
|
92
|
+
})();
|
|
93
|
+
|
|
55
94
|
// Name the tab at parse time. Without this the tab shows the host
|
|
56
95
|
// ("localhost:8000") until metadata arrives over the WebSocket, and
|
|
57
96
|
// that wait is the array load — seconds for a large file. The name
|
|
@@ -4443,9 +4482,17 @@
|
|
|
4443
4482
|
box-shadow: none;
|
|
4444
4483
|
}
|
|
4445
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). */
|
|
4446
4489
|
#loading-overlay {
|
|
4447
4490
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
4448
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;
|
|
4449
4496
|
}
|
|
4450
4497
|
#loading-overlay .av-load-logo {
|
|
4451
4498
|
width: 64px; height: 64px;
|
|
@@ -4453,11 +4500,28 @@
|
|
|
4453
4500
|
#loading-overlay .av-load-name {
|
|
4454
4501
|
margin-top: 14px; font-size: 11px; color: var(--muted);
|
|
4455
4502
|
letter-spacing: 0.1em; text-transform: uppercase;
|
|
4456
|
-
|
|
4503
|
+
min-height: 1.2em; opacity: 0;
|
|
4457
4504
|
}
|
|
4458
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
|
+
}
|
|
4459
4522
|
#loading-overlay.av-array-switch .av-load-logo,
|
|
4460
|
-
#loading-overlay.av-array-switch .av-load-name
|
|
4523
|
+
#loading-overlay.av-array-switch .av-load-name,
|
|
4524
|
+
#loading-overlay.av-array-switch .av-load-progress { display: none; }
|
|
4461
4525
|
/* Deliberately outside #loading-overlay. The overlay is centred over the
|
|
4462
4526
|
canvas, so anything drawn in it sits on top of the slice — wrong for
|
|
4463
4527
|
progress that continues after the viewer is live and showing data.
|
|
@@ -5678,12 +5742,29 @@
|
|
|
5678
5742
|
<div id="multi-view-wrap"></div>
|
|
5679
5743
|
<div id="qmri-view-wrap"></div>
|
|
5680
5744
|
<div id="stack-mosaic-wrap"></div>
|
|
5681
|
-
<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>
|
|
5682
5746
|
<div id="render-progress"><div class="bar"></div></div>
|
|
5683
5747
|
<div id="render-progress-text"></div>
|
|
5684
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>
|
|
5685
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>
|
|
5686
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 (_) {}
|
|
5687
5768
|
// After a switch between arrays of a multi-array file the page
|
|
5688
5769
|
// reloads. Show the same small loading panel the switch used, not
|
|
5689
5770
|
// the startup splash, so no second, larger logo flashes by.
|
|
@@ -5695,7 +5776,7 @@
|
|
|
5695
5776
|
const panel = document.querySelector('#busy-overlay .av-loading-panel').cloneNode(true);
|
|
5696
5777
|
panel.querySelector('span').textContent = `Loading ${key}…`;
|
|
5697
5778
|
panel.classList.add('av-array-switch-panel');
|
|
5698
|
-
ol.classList.add('av-array-switch');
|
|
5779
|
+
ol.classList.add('av-array-switch', 'av-load-shown');
|
|
5699
5780
|
ol.appendChild(panel);
|
|
5700
5781
|
}
|
|
5701
5782
|
} catch (_) {}
|
|
@@ -5881,6 +5962,17 @@
|
|
|
5881
5962
|
// server could not answer immediately (still loading), in which case
|
|
5882
5963
|
// the viewer asks over the socket exactly as it always has.
|
|
5883
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
|
+
})();
|
|
5884
5976
|
/* __AV_STATIC_SCRIPT_BELOW__ — nothing after this line is substituted at serve time. */
|
|
5885
5977
|
const urlParams = new URLSearchParams(_arrayViewLaunchSearch);
|
|
5886
5978
|
const _isIntegratedBrowserViewer = (
|
|
@@ -6215,7 +6307,36 @@
|
|
|
6215
6307
|
return proxy;
|
|
6216
6308
|
}
|
|
6217
6309
|
|
|
6218
|
-
|
|
6310
|
+
// Hand over the socket the page head opened early (see the head
|
|
6311
|
+
// script), if it is for this exact address and still usable. Its
|
|
6312
|
+
// open event and any messages that arrived before the viewer's own
|
|
6313
|
+
// handlers existed are replayed once those handlers are attached,
|
|
6314
|
+
// which happens synchronously after this returns.
|
|
6315
|
+
function _takeEarlySocket(url) {
|
|
6316
|
+
const early = window.__avEarlySocket;
|
|
6317
|
+
window.__avEarlySocket = null;
|
|
6318
|
+
if (!early) return null;
|
|
6319
|
+
const usable = early.url === url
|
|
6320
|
+
&& (early.ws.readyState === WebSocket.CONNECTING
|
|
6321
|
+
|| early.ws.readyState === WebSocket.OPEN);
|
|
6322
|
+
if (!usable) {
|
|
6323
|
+
try { early.ws.onopen = null; early.ws.onmessage = null; early.ws.close(); } catch (_) {}
|
|
6324
|
+
return null;
|
|
6325
|
+
}
|
|
6326
|
+
const ws = early.ws;
|
|
6327
|
+
ws.onopen = null;
|
|
6328
|
+
ws.onmessage = null;
|
|
6329
|
+
queueMicrotask(() => {
|
|
6330
|
+
if (early.opened && ws.onopen) ws.onopen(new Event('open'));
|
|
6331
|
+
for (const event of early.events) {
|
|
6332
|
+
if (ws.onmessage) ws.onmessage(event);
|
|
6333
|
+
}
|
|
6334
|
+
early.events.length = 0;
|
|
6335
|
+
});
|
|
6336
|
+
return ws;
|
|
6337
|
+
}
|
|
6338
|
+
|
|
6339
|
+
function createTransport(transportSid, onMessage, onOpen, onClose, onError, adoptEarly = false) {
|
|
6219
6340
|
let wsTarget = resolveServerPath(`/ws/${transportSid}`);
|
|
6220
6341
|
const nativeRequestId = urlParams.get('native_request_id');
|
|
6221
6342
|
if (nativeRequestId) {
|
|
@@ -6232,23 +6353,21 @@
|
|
|
6232
6353
|
wsTarget += `${separator}launch_request_id=${encodeURIComponent(launchRequestId)}`;
|
|
6233
6354
|
wsTarget += `&launch_token=${encodeURIComponent(launchToken)}`;
|
|
6234
6355
|
}
|
|
6235
|
-
const
|
|
6356
|
+
const socketUrl = () => {
|
|
6236
6357
|
if (_embeddedServerOrigin) {
|
|
6237
6358
|
const wsUrl = new URL(wsTarget);
|
|
6238
6359
|
const proto = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
6239
|
-
return
|
|
6240
|
-
`${proto}//${wsUrl.host}${wsUrl.pathname}${wsUrl.search}${wsUrl.hash}`
|
|
6241
|
-
);
|
|
6360
|
+
return `${proto}//${wsUrl.host}${wsUrl.pathname}${wsUrl.search}${wsUrl.hash}`;
|
|
6242
6361
|
}
|
|
6243
6362
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
6244
|
-
return
|
|
6363
|
+
return `${proto}//${location.host}${wsTarget}`;
|
|
6245
6364
|
};
|
|
6246
|
-
const
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6365
|
+
const makeSocket = () => new WebSocket(socketUrl());
|
|
6366
|
+
const hedged = _relayWsHedgeEnabled && transportSid === sid;
|
|
6367
|
+
const early = (adoptEarly && transportSid === sid && !hedged)
|
|
6368
|
+
? _takeEarlySocket(socketUrl())
|
|
6369
|
+
: null;
|
|
6370
|
+
const ws = early || (hedged ? _createHedgedWebSocket(makeSocket) : makeSocket());
|
|
6252
6371
|
ws.binaryType = 'arraybuffer';
|
|
6253
6372
|
if (onOpen) ws.onopen = onOpen;
|
|
6254
6373
|
if (onMessage) ws.onmessage = onMessage;
|
|
@@ -6310,6 +6429,10 @@
|
|
|
6310
6429
|
const phaseBody = JSON.stringify({
|
|
6311
6430
|
phase,
|
|
6312
6431
|
detail: detail || null,
|
|
6432
|
+
// When it happened on the page's own clock (ms since
|
|
6433
|
+
// navigation start): reports are sent one after another,
|
|
6434
|
+
// so their arrival time at the server lags the event.
|
|
6435
|
+
client_ms: Math.round(performance.now()),
|
|
6313
6436
|
server_id: _launchPhaseServerId,
|
|
6314
6437
|
window_id: _launchPhaseWindowId,
|
|
6315
6438
|
token: _launchPhaseToken,
|
|
@@ -6958,6 +7081,7 @@
|
|
|
6958
7081
|
[arrayName, info, cbWrap, helpHint].forEach(el => {
|
|
6959
7082
|
if (!el) return;
|
|
6960
7083
|
el.style.removeProperty('opacity');
|
|
7084
|
+
el.style.removeProperty('transition');
|
|
6961
7085
|
el.style.removeProperty('--fly-y');
|
|
6962
7086
|
el.style.removeProperty('scale');
|
|
6963
7087
|
el.style.removeProperty('transformOrigin');
|
|
@@ -7016,15 +7140,11 @@
|
|
|
7016
7140
|
const helpHint = document.getElementById('help-hint');
|
|
7017
7141
|
const cbVisible = cbWrap && getComputedStyle(cbWrap).display !== 'none';
|
|
7018
7142
|
|
|
7019
|
-
// Seize opacity immediately
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
if (arrayName) gsap.set(arrayName, { '--fly-y': '-8px' });
|
|
7025
|
-
if (info) gsap.set(info, { '--fly-y': '-8px' });
|
|
7026
|
-
if (cbVisible) gsap.set(cbWrap, { '--fly-y': '12px' });
|
|
7027
|
-
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' });
|
|
7028
7148
|
|
|
7029
7149
|
_startupAnimTl = gsap.timeline({ onComplete: () => {
|
|
7030
7150
|
clearTimeout(_safety);
|
|
@@ -7033,47 +7153,28 @@
|
|
|
7033
7153
|
if (ol) ol.style.display = 'none';
|
|
7034
7154
|
}});
|
|
7035
7155
|
|
|
7036
|
-
//
|
|
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.)
|
|
7037
7160
|
_startupAnimTl.call(() => {
|
|
7038
7161
|
if (loadLogo) loadLogo.classList.remove('av-logo-loading');
|
|
7039
7162
|
}, null, 0);
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
if (
|
|
7043
|
-
_startupAnimTl.to(
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
_startupAnimTl.to(loadName, {
|
|
7049
|
-
opacity: 0, duration: 0.14, ease: 'power2.in'
|
|
7050
|
-
}, 0.05);
|
|
7051
|
-
}
|
|
7052
|
-
_startupAnimTl.to(canvasWrap, {
|
|
7053
|
-
opacity: 1, duration: 0.22, ease: 'power2.out'
|
|
7054
|
-
}, 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);
|
|
7055
7171
|
_startupAnimTl.call(() => {
|
|
7056
7172
|
document.body.classList.remove('av-loading');
|
|
7057
7173
|
_reconcileUI();
|
|
7058
|
-
}, null, 0.
|
|
7174
|
+
}, null, 0.06);
|
|
7059
7175
|
_startupAnimTl.call(() => {
|
|
7060
7176
|
if (ol) ol.style.display = 'none';
|
|
7061
|
-
}, null, 0.
|
|
7062
|
-
|
|
7063
|
-
// ── Act 3: Chrome assembles around canvas ──────────────────────
|
|
7064
|
-
_startupAnimTl
|
|
7065
|
-
.to(arrayName, { opacity: 1, '--fly-y': '0px', duration: 0.26, ease: 'power3.out' }, 0.32)
|
|
7066
|
-
.to(info, { opacity: 1, '--fly-y': '0px', duration: 0.26, ease: 'power3.out' }, 0.38);
|
|
7067
|
-
if (cbVisible) {
|
|
7068
|
-
_startupAnimTl.to(cbWrap, {
|
|
7069
|
-
opacity: 1, '--fly-y': '0px', duration: 0.24, ease: 'power3.out'
|
|
7070
|
-
}, 0.48);
|
|
7071
|
-
}
|
|
7072
|
-
if (helpHint) {
|
|
7073
|
-
_startupAnimTl.to(helpHint, {
|
|
7074
|
-
opacity: 1, scale: 1, duration: 0.22, ease: 'back.out(1.7)'
|
|
7075
|
-
}, 0.58);
|
|
7076
|
-
}
|
|
7177
|
+
}, null, 0.26);
|
|
7077
7178
|
}
|
|
7078
7179
|
let _immersiveDriveTween = null; // tween that scrubs immersiveTl.progress()
|
|
7079
7180
|
let _immTargetZoom = 1.0; // cached zoom target at start of transition
|
|
@@ -15798,7 +15899,7 @@
|
|
|
15798
15899
|
}
|
|
15799
15900
|
if (_wsRetryTimer) { clearTimeout(_wsRetryTimer); _wsRetryTimer = null; }
|
|
15800
15901
|
if (_wsGaveUp) return;
|
|
15801
|
-
ws = createTransport(sid, null, null, null, null);
|
|
15902
|
+
ws = createTransport(sid, null, null, null, null, true);
|
|
15802
15903
|
|
|
15803
15904
|
// Only the first connection is bounded. Once a socket has opened,
|
|
15804
15905
|
// a later drop is a reconnect — the server may be restarting, and
|
|
@@ -15855,7 +15956,12 @@
|
|
|
15855
15956
|
} else if (msg.type === 'render_timing' && _PERF_ENABLED) {
|
|
15856
15957
|
_perfHandleTimingMessage(event.data, 'main');
|
|
15857
15958
|
} else if (msg.type === 'render_progress') {
|
|
15858
|
-
|
|
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
|
+
}
|
|
15859
15965
|
} else if (msg.type === 'warm_path') {
|
|
15860
15966
|
_warmClientPath(msg.connections);
|
|
15861
15967
|
} else if (msg.type === 'render_error') {
|
|
@@ -16967,6 +17073,8 @@
|
|
|
16967
17073
|
}
|
|
16968
17074
|
function _showRenderProgress(done, total, label) {
|
|
16969
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) + '%';
|
|
16970
17078
|
const bar = document.querySelector('#render-progress .bar');
|
|
16971
17079
|
const text = document.getElementById('render-progress-text');
|
|
16972
17080
|
if (!bar || !text) return;
|
|
@@ -41836,22 +41944,29 @@
|
|
|
41836
41944
|
_reconcileMainLoupeState();
|
|
41837
41945
|
_reconcileInfoCursor();
|
|
41838
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
|
+
}
|
|
41839
41954
|
const busyOverlay = document.getElementById('busy-overlay');
|
|
41840
41955
|
if (busyOverlay) {
|
|
41841
|
-
const busy =
|
|
41956
|
+
const busy = !startupScreen
|
|
41957
|
+
&& (_patientLoadingVisible || _slowRenderBusyVisible || _smPageBusyVisible);
|
|
41842
41958
|
busyOverlay.classList.toggle('visible', busy);
|
|
41843
41959
|
busyOverlay.setAttribute('aria-hidden', busy ? 'false' : 'true');
|
|
41844
41960
|
}
|
|
41961
|
+
const hairline = _renderProgressVisible && !startupScreen;
|
|
41845
41962
|
const renderProgress = document.getElementById('render-progress');
|
|
41846
41963
|
if (renderProgress) {
|
|
41847
|
-
renderProgress.classList.toggle('visible',
|
|
41848
|
-
renderProgress.setAttribute(
|
|
41849
|
-
'aria-hidden', _renderProgressVisible ? 'false' : 'true'
|
|
41850
|
-
);
|
|
41964
|
+
renderProgress.classList.toggle('visible', hairline);
|
|
41965
|
+
renderProgress.setAttribute('aria-hidden', hairline ? 'false' : 'true');
|
|
41851
41966
|
}
|
|
41852
41967
|
const renderProgressText = document.getElementById('render-progress-text');
|
|
41853
41968
|
if (renderProgressText) {
|
|
41854
|
-
renderProgressText.classList.toggle('visible',
|
|
41969
|
+
renderProgressText.classList.toggle('visible', hairline);
|
|
41855
41970
|
}
|
|
41856
41971
|
const dropMenu = document.getElementById('drop-action-menu');
|
|
41857
41972
|
if (dropMenu) {
|
arrayview/_vscode_extension.py
CHANGED
|
@@ -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.
|
|
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.
|
arrayview/arrayview-opener.vsix
CHANGED
|
Binary file
|
|
@@ -12,30 +12,30 @@ arrayview/_extra_ports.py,sha256=MZcx04NI9Eg4coydb-yPIHp7uJbE6UscyYGnw8fxid0,111
|
|
|
12
12
|
arrayview/_icon.png,sha256=a8RTAbvweLhh3hzD5eGg76Peq-KSDcS6YrCphRb_HEs,2748
|
|
13
13
|
arrayview/_imaging.py,sha256=MQX1uy4m1lofOeoLDhCp6cHO5p_V5fXm4qlSu9Ykn-c,534
|
|
14
14
|
arrayview/_instance_registry.py,sha256=h7vyvCl0LTt9R8sDB1Vxe4a-KzfkLVuwrSw_ANpPTD8,8833
|
|
15
|
-
arrayview/_io.py,sha256=
|
|
15
|
+
arrayview/_io.py,sha256=waUJr9au3yrYJKhUFa4EmOdRjkGjKrb9G01NJtCysrs,90123
|
|
16
16
|
arrayview/_launch_plan.py,sha256=KGgIPGsflXArECB2txnIAewjuPvF4fZTbO63alC8x28,23570
|
|
17
17
|
arrayview/_launch_trace.py,sha256=4yjJdkFyEvSrPN14QVFJ3u6on2q0TH2ySSdgUp9SjKI,5668
|
|
18
|
-
arrayview/_launcher.py,sha256=
|
|
18
|
+
arrayview/_launcher.py,sha256=Hljfeq4AmxVTDiDgYyMtHT5eiatgFdrvcr6bq75onZY,243267
|
|
19
19
|
arrayview/_lifecycle.py,sha256=bqG0oKr-e-5S1ssCTlGmF-yBE47ilrpO1S_Owfjs9Z8,5365
|
|
20
20
|
arrayview/_overlays.py,sha256=CksypG9rWfCz_3RRGIeyaKeXse17AFxvkEjmYyXnIIE,5399
|
|
21
21
|
arrayview/_platform.py,sha256=qSWjRcx9rLIdLvKrRArSueFnbyn5GPKfTEMOc5mCIUs,29970
|
|
22
|
-
arrayview/_render.py,sha256=
|
|
22
|
+
arrayview/_render.py,sha256=iDf0weQ30AH93l74KOlO8Ciq__Wbh-wqfAY7KQS1utE,34576
|
|
23
23
|
arrayview/_routes_analysis.py,sha256=Xz9Ysr04u07pzSRB4Fzmcl6XwVxyaG6xCWxGwYZi_80,33085
|
|
24
24
|
arrayview/_routes_drop.py,sha256=FC4e7STdCo3hYIA0C4Yy9Ka2UKFx7nLDU5dkujmc1Vo,12477
|
|
25
25
|
arrayview/_routes_export.py,sha256=Ha8mMO2E9TlRL5x25-ADV8_8xUiC-jRz9N1oWwgKOsg,3898
|
|
26
|
-
arrayview/_routes_loading.py,sha256=
|
|
26
|
+
arrayview/_routes_loading.py,sha256=CuKOtJKb3qTS0JXP_662ehPns4-BIppcm0DHErH74mY,42451
|
|
27
27
|
arrayview/_routes_persistence.py,sha256=T03csHb7ppGT-8yMDf227XkqhnhjgSLYj0RW2wmg59g,17176
|
|
28
28
|
arrayview/_routes_preferences.py,sha256=xoMHbPKliYspgjaclhx2r8HbP9mFo9Td-nTF76eWVMg,4680
|
|
29
29
|
arrayview/_routes_preload.py,sha256=KOsankNguxdIWmFW9iZcY9p2SvUN1mOKY7KkW1qMGcc,1729
|
|
30
|
-
arrayview/_routes_query.py,sha256=
|
|
30
|
+
arrayview/_routes_query.py,sha256=HEEmMKuFycUTBNVUSWtUFXCnWB7ZrM3hv9IZWbXz11o,25012
|
|
31
31
|
arrayview/_routes_rendering.py,sha256=EQq2QzdtUS_pv35k7prLdlRdO5nByIlQ_Eu0nhVsn1Y,22970
|
|
32
32
|
arrayview/_routes_segmentation.py,sha256=x2bBjQffZryeTMmOH38YdBWethw1o3KpA8V0chJcZHI,23599
|
|
33
33
|
arrayview/_routes_state.py,sha256=T_xoYcRlv7L47ydTxxiXb0Fp5M5x9KR2U-wkxaVeuMY,12462
|
|
34
34
|
arrayview/_routes_vectorfield.py,sha256=OWZtAONwRKQHdYnbDAMbOVf0q6pH7kqV8fa1icDhKHM,9734
|
|
35
|
-
arrayview/_routes_websocket.py,sha256=
|
|
35
|
+
arrayview/_routes_websocket.py,sha256=CADgPfAsYWfB7IA75_qJ4voL8dQ3y4RjfzPO0vahIkc,42093
|
|
36
36
|
arrayview/_segmentation.py,sha256=aENWHmefrKDScXhcZL1X3KgYXE784dpMkmFFo83qnrw,7377
|
|
37
37
|
arrayview/_server.py,sha256=A8O8J6N4WTDcujFHjTWjlJYWIzkJe8y9XmKpj9XozE0,22918
|
|
38
|
-
arrayview/_session.py,sha256
|
|
38
|
+
arrayview/_session.py,sha256=jDOERph1hRzqLr9_uzy4Yi0AlLYLD4Y8JJD8bM8r56M,35867
|
|
39
39
|
arrayview/_session_spec.py,sha256=mKDfMRsPYQdEmN9hLXmDU_9N7etiDyBXr8mz-fB8QBo,6825
|
|
40
40
|
arrayview/_shell.html,sha256=vNa2Kiwx_eoB5H329jolYafLhwJDCH9DsJafBqvVMWU,13194
|
|
41
41
|
arrayview/_source_safety.py,sha256=cI7JAL7DWYuxuB_LnYExvJZaICX2R2qJEJWD1M0z_gg,27664
|
|
@@ -43,17 +43,17 @@ arrayview/_synthetic_mri.py,sha256=CNL-2WC835i6WCqkvmWBCO6WQ5w7HE5trNb8OyEfe64,7
|
|
|
43
43
|
arrayview/_torch.py,sha256=13sqIMCUu_-b9SQ9KfDCExDz2UMStL87EDVjnbJxAEo,7536
|
|
44
44
|
arrayview/_tutorial.py,sha256=2zZHJGJDa4n30GE1-9ADjKFAifGBH9vvHXP4uDt7BaA,19547
|
|
45
45
|
arrayview/_vectorfield.py,sha256=eDqMCMEGjO8Jdt-6Auh01cz3WD9dbF7xYUWD9xHAf1E,8115
|
|
46
|
-
arrayview/_viewer.html,sha256=
|
|
46
|
+
arrayview/_viewer.html,sha256=mqFwuv1qoxAOCPjKBZKfqBzavZcz3-muqQEBDLqfY50,2379540
|
|
47
47
|
arrayview/_vscode.py,sha256=EKnGN2zrjlybc4FQOmYA8BvYdtkcJbLHDEdgtvO7ul4,837
|
|
48
48
|
arrayview/_vscode_browser.py,sha256=VCH9JpAAjFuhO0wQe8tIGjAL5837AXg_bpmBFtWOQqc,20573
|
|
49
|
-
arrayview/_vscode_extension.py,sha256=
|
|
49
|
+
arrayview/_vscode_extension.py,sha256=eYsWAQmik2tt7HMWad9vBnn2kpRyRpLrh42FBGQy0M4,53156
|
|
50
50
|
arrayview/_vscode_signal.py,sha256=N3PCHG6-3tCEc_tHLjg2dhSEQ2EnF_psE2IJRBpAuac,54938
|
|
51
51
|
arrayview/_watch.py,sha256=PxxS_5hcj_S6xQf9Ep4p1HLRkrI5iDMh1aRa4olV7ew,1873
|
|
52
|
-
arrayview/arrayview-opener.vsix,sha256=
|
|
52
|
+
arrayview/arrayview-opener.vsix,sha256=44uiTM39qj_RXji3pFQaR8TA1jhNB3LYwVU_dGFbP4c,62284
|
|
53
53
|
arrayview/gsap.min.js,sha256=VGihm4idI0E1uqrWezWsiScB9F_jMosn7qs6pPSkeac,72304
|
|
54
54
|
arrayview/todo.md,sha256=KhnRzpn6_jezR0nDDB61-zhox-rbOmEGW-A7Z0_7FnU,622
|
|
55
|
-
arrayview-0.
|
|
56
|
-
arrayview-0.
|
|
57
|
-
arrayview-0.
|
|
58
|
-
arrayview-0.
|
|
59
|
-
arrayview-0.
|
|
55
|
+
arrayview-0.48.0.dist-info/METADATA,sha256=v_WbIzyXx3Z9rNgk9uKz7YPEPyVcnUqfPOU4BpVD4is,2288
|
|
56
|
+
arrayview-0.48.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
57
|
+
arrayview-0.48.0.dist-info/entry_points.txt,sha256=-0s08axutXkTbINiKs0Q7iv-33aWuTaamUg2q2-NyQ4,60
|
|
58
|
+
arrayview-0.48.0.dist-info/licenses/LICENSE,sha256=34OivjM9faB2QaPjZpMRTIiSx31V3U9cEyQ6F2k-bH0,1062
|
|
59
|
+
arrayview-0.48.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|