arrayview 0.48.3__py3-none-any.whl → 0.48.4__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/_diff.py +60 -44
- arrayview/_render.py +49 -0
- arrayview/_routes_rendering.py +48 -0
- arrayview/_routes_websocket.py +48 -0
- arrayview/_session.py +3 -0
- arrayview/_viewer.html +300 -80
- {arrayview-0.48.3.dist-info → arrayview-0.48.4.dist-info}/METADATA +1 -1
- {arrayview-0.48.3.dist-info → arrayview-0.48.4.dist-info}/RECORD +11 -11
- {arrayview-0.48.3.dist-info → arrayview-0.48.4.dist-info}/WHEEL +0 -0
- {arrayview-0.48.3.dist-info → arrayview-0.48.4.dist-info}/entry_points.txt +0 -0
- {arrayview-0.48.3.dist-info → arrayview-0.48.4.dist-info}/licenses/LICENSE +0 -0
arrayview/_diff.py
CHANGED
|
@@ -85,6 +85,43 @@ def _resize_normalized_like(source: np.ndarray, target: np.ndarray) -> np.ndarra
|
|
|
85
85
|
return np.array(image, dtype=np.float32) / 255.0
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
def _render_values(session, dim_x, dim_y, idx_tuple, complex_mode, log_scale):
|
|
89
|
+
"""Extract a slice in the array's own units (complex/log applied, no rescale)."""
|
|
90
|
+
raw = extract_slice(session, dim_x, dim_y, list(idx_tuple))
|
|
91
|
+
data = apply_complex_mode(raw, complex_mode)
|
|
92
|
+
if log_scale:
|
|
93
|
+
data = np.log1p(np.abs(data))
|
|
94
|
+
return np.asarray(data, dtype=np.float32)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _render_values_mosaic(session, dim_x, dim_y, dim_z, idx_tuple, complex_mode, log_scale):
|
|
98
|
+
"""Mosaic of every dim_z slice in the array's own units, plus separator mask."""
|
|
99
|
+
idx_list = list(idx_tuple)
|
|
100
|
+
frames = [
|
|
101
|
+
_render_values(
|
|
102
|
+
session,
|
|
103
|
+
dim_x,
|
|
104
|
+
dim_y,
|
|
105
|
+
[i if j == dim_z else idx_list[j] for j in range(len(session.shape))],
|
|
106
|
+
complex_mode,
|
|
107
|
+
log_scale,
|
|
108
|
+
)
|
|
109
|
+
for i in range(session.shape[dim_z])
|
|
110
|
+
]
|
|
111
|
+
all_data = np.stack(frames)
|
|
112
|
+
grid, _, _ = _build_mosaic_grid(all_data, len(frames))
|
|
113
|
+
return grid.astype(np.float32), np.isnan(grid)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _resize_values_like(source: np.ndarray, target: np.ndarray) -> np.ndarray:
|
|
117
|
+
"""Resize float source data to target shape without quantising it."""
|
|
118
|
+
if source.shape == target.shape:
|
|
119
|
+
return source
|
|
120
|
+
image = _pil_image().fromarray(np.nan_to_num(source).astype(np.float32), mode="F")
|
|
121
|
+
image = image.resize((target.shape[1], target.shape[0]), _pil_image().BILINEAR)
|
|
122
|
+
return np.array(image, dtype=np.float32)
|
|
123
|
+
|
|
124
|
+
|
|
88
125
|
def _compute_diff(
|
|
89
126
|
session_a,
|
|
90
127
|
session_b,
|
|
@@ -114,65 +151,41 @@ def _compute_diff(
|
|
|
114
151
|
idx_b = _parse_indices(indices_b)[:ndim_b] if indices_b is not None else idx_tuple[:ndim_b]
|
|
115
152
|
nan_mask = None
|
|
116
153
|
|
|
154
|
+
# True A−B in the arrays' own units. Each side used to be rescaled to its
|
|
155
|
+
# own per-slice display range first, which cancelled real differences
|
|
156
|
+
# (B = 0.9·A showed as zero) and made the map flicker while scrolling.
|
|
117
157
|
if dim_z >= 0:
|
|
118
|
-
a, nan_mask_a =
|
|
119
|
-
session_a,
|
|
120
|
-
dim_x,
|
|
121
|
-
dim_y,
|
|
122
|
-
dim_z,
|
|
123
|
-
idx_a,
|
|
124
|
-
dr,
|
|
125
|
-
complex_mode,
|
|
126
|
-
log_scale,
|
|
158
|
+
a, nan_mask_a = _render_values_mosaic(
|
|
159
|
+
session_a, dim_x, dim_y, dim_z, idx_a, complex_mode, log_scale
|
|
127
160
|
)
|
|
128
|
-
b, nan_mask_b =
|
|
129
|
-
session_b,
|
|
130
|
-
dim_x,
|
|
131
|
-
dim_y,
|
|
132
|
-
dim_z,
|
|
133
|
-
idx_b,
|
|
134
|
-
dr,
|
|
135
|
-
complex_mode,
|
|
136
|
-
log_scale,
|
|
161
|
+
b, nan_mask_b = _render_values_mosaic(
|
|
162
|
+
session_b, dim_x, dim_y, dim_z, idx_b, complex_mode, log_scale
|
|
137
163
|
)
|
|
138
|
-
|
|
164
|
+
b = _resize_values_like(b, a)
|
|
165
|
+
if nan_mask_b.shape == nan_mask_a.shape:
|
|
166
|
+
nan_mask = nan_mask_a | nan_mask_b
|
|
167
|
+
else:
|
|
168
|
+
nan_mask = nan_mask_a
|
|
139
169
|
else:
|
|
140
|
-
a =
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
dim_y,
|
|
144
|
-
idx_a,
|
|
145
|
-
dr,
|
|
146
|
-
complex_mode,
|
|
147
|
-
log_scale,
|
|
148
|
-
)
|
|
149
|
-
b = _render_normalized(
|
|
150
|
-
session_b,
|
|
151
|
-
dim_x,
|
|
152
|
-
dim_y,
|
|
153
|
-
idx_b,
|
|
154
|
-
dr,
|
|
155
|
-
complex_mode,
|
|
156
|
-
log_scale,
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
b = _resize_normalized_like(b, a)
|
|
170
|
+
a = _render_values(session_a, dim_x, dim_y, idx_a, complex_mode, log_scale)
|
|
171
|
+
b = _render_values(session_b, dim_x, dim_y, idx_b, complex_mode, log_scale)
|
|
172
|
+
b = _resize_values_like(b, a)
|
|
160
173
|
if diff_mode == 1:
|
|
161
174
|
raw = a - b
|
|
162
175
|
lo = float(np.nanpercentile(raw, 1))
|
|
163
176
|
hi = float(np.nanpercentile(raw, 99))
|
|
164
177
|
vmax = max(abs(lo), abs(hi)) or 1.0
|
|
165
178
|
vmin = -vmax
|
|
166
|
-
colormap = "
|
|
179
|
+
colormap = "icefire"
|
|
167
180
|
elif diff_mode == 2:
|
|
168
181
|
raw = np.abs(a - b)
|
|
169
|
-
vmax = float(
|
|
182
|
+
vmax = float(np.nanmax(raw)) or 1.0
|
|
170
183
|
vmin = 0.0
|
|
171
184
|
colormap = "afmhot"
|
|
172
185
|
else:
|
|
173
186
|
raw = np.abs(a - b) / np.maximum(np.abs(a), 1e-6)
|
|
174
187
|
raw = np.clip(raw, 0.0, 2.0).astype(np.float32)
|
|
175
|
-
vmax = float(
|
|
188
|
+
vmax = float(np.nanmax(raw)) or 1.0
|
|
176
189
|
vmin = 0.0
|
|
177
190
|
colormap = "afmhot"
|
|
178
191
|
|
|
@@ -188,7 +201,7 @@ def _render_diff_rgba(
|
|
|
188
201
|
) -> np.ndarray:
|
|
189
202
|
"""Map raw diff data to RGBA."""
|
|
190
203
|
if vmax > vmin:
|
|
191
|
-
normalized = np.clip((raw - vmin) / (vmax - vmin), 0, 1)
|
|
204
|
+
normalized = np.clip(np.nan_to_num((raw - vmin) / (vmax - vmin)), 0, 1)
|
|
192
205
|
else:
|
|
193
206
|
normalized = np.zeros_like(raw)
|
|
194
207
|
_ensure_lut(colormap)
|
|
@@ -201,9 +214,12 @@ def _render_diff_rgba(
|
|
|
201
214
|
|
|
202
215
|
def _diff_histogram(raw: np.ndarray, bins: int) -> dict[str, object]:
|
|
203
216
|
"""Return histogram payload for raw diff data."""
|
|
217
|
+
raw = raw[np.isfinite(raw)]
|
|
218
|
+
if raw.size == 0:
|
|
219
|
+
raw = np.zeros(1, dtype=np.float32)
|
|
204
220
|
vmin = float(raw.min())
|
|
205
221
|
vmax = float(raw.max())
|
|
206
|
-
counts, edges = np.histogram(raw
|
|
222
|
+
counts, edges = np.histogram(raw, bins=bins)
|
|
207
223
|
return {
|
|
208
224
|
"counts": counts.tolist(),
|
|
209
225
|
"edges": edges.tolist(),
|
arrayview/_render.py
CHANGED
|
@@ -34,6 +34,43 @@ _mpl_colormaps = None
|
|
|
34
34
|
_luts_lock = threading.Lock()
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
_ICEFIRE_STOPS = [
|
|
38
|
+
(0.7394, 0.9044, 0.8576),
|
|
39
|
+
(0.6645, 0.8497, 0.8387),
|
|
40
|
+
(0.5749, 0.7908, 0.8202),
|
|
41
|
+
(0.4818, 0.734, 0.8084),
|
|
42
|
+
(0.3863, 0.6778, 0.8043),
|
|
43
|
+
(0.3052, 0.6184, 0.8053),
|
|
44
|
+
(0.2418, 0.5555, 0.8106),
|
|
45
|
+
(0.2141, 0.4873, 0.8157),
|
|
46
|
+
(0.2487, 0.4134, 0.7887),
|
|
47
|
+
(0.2831, 0.3444, 0.715),
|
|
48
|
+
(0.2846, 0.2908, 0.6028),
|
|
49
|
+
(0.2577, 0.2497, 0.4821),
|
|
50
|
+
(0.2205, 0.2121, 0.3737),
|
|
51
|
+
(0.182, 0.1769, 0.2809),
|
|
52
|
+
(0.1492, 0.1465, 0.2053),
|
|
53
|
+
(0.1276, 0.1254, 0.1501),
|
|
54
|
+
(0.1213, 0.1183, 0.1188),
|
|
55
|
+
(0.1538, 0.1167, 0.1227),
|
|
56
|
+
(0.2063, 0.1255, 0.1416),
|
|
57
|
+
(0.2734, 0.1407, 0.1703),
|
|
58
|
+
(0.3514, 0.157, 0.2039),
|
|
59
|
+
(0.4375, 0.1703, 0.2357),
|
|
60
|
+
(0.5298, 0.1784, 0.2601),
|
|
61
|
+
(0.6251, 0.1832, 0.2686),
|
|
62
|
+
(0.7138, 0.2028, 0.2533),
|
|
63
|
+
(0.7898, 0.2469, 0.2268),
|
|
64
|
+
(0.85, 0.3142, 0.2058),
|
|
65
|
+
(0.8971, 0.3932, 0.2012),
|
|
66
|
+
(0.9279, 0.482, 0.2415),
|
|
67
|
+
(0.9479, 0.5722, 0.3318),
|
|
68
|
+
(0.9659, 0.659, 0.4438),
|
|
69
|
+
(0.9829, 0.7447, 0.5597),
|
|
70
|
+
(0.9992, 0.831, 0.6764),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
37
74
|
def _init_luts():
|
|
38
75
|
"""Build colourmap LUTs on first use (defers matplotlib + qmricolors import)."""
|
|
39
76
|
global _mpl_colormaps
|
|
@@ -46,6 +83,12 @@ def _init_luts():
|
|
|
46
83
|
from matplotlib import colormaps
|
|
47
84
|
from matplotlib.colors import LinearSegmentedColormap
|
|
48
85
|
|
|
86
|
+
if "icefire" not in colormaps:
|
|
87
|
+
# seaborn's icefire (blue → near-black → red), sampled at 33 stops so
|
|
88
|
+
# we don't depend on seaborn. Default for the A−B compare center.
|
|
89
|
+
colormaps.register(
|
|
90
|
+
LinearSegmentedColormap.from_list("icefire", _ICEFIRE_STOPS)
|
|
91
|
+
)
|
|
49
92
|
_mpl_colormaps = colormaps
|
|
50
93
|
for name in COLORMAPS:
|
|
51
94
|
lut = np.concatenate(
|
|
@@ -210,6 +253,12 @@ def _read_slice(session, dim_x, dim_y, idx_list, progress):
|
|
|
210
253
|
if progress is None:
|
|
211
254
|
progress = getattr(session, 'progress_sink', None)
|
|
212
255
|
data = session.data
|
|
256
|
+
if not getattr(session, "_ram_upgrade_started", False):
|
|
257
|
+
# Not every loading route asks for this (compare arrays, for one), and
|
|
258
|
+
# without it a scattered plane is re-read from the file on every step.
|
|
259
|
+
from ._session import upgrade_memmap_in_background
|
|
260
|
+
|
|
261
|
+
upgrade_memmap_in_background(session)
|
|
213
262
|
if isinstance(data, np.memmap) and data.filename:
|
|
214
263
|
# A slice scattered one element per page reads far faster as many
|
|
215
264
|
# parallel small reads than by faulting through the map (local disks).
|
arrayview/_routes_rendering.py
CHANGED
|
@@ -441,6 +441,54 @@ def register_rendering_routes(app, *, get_session_or_404) -> None:
|
|
|
441
441
|
return Response(status_code=422)
|
|
442
442
|
return _diff_histogram(raw, bins)
|
|
443
443
|
|
|
444
|
+
@app.get("/diff-values/{sid_a}/{sid_b}")
|
|
445
|
+
def get_diff_values(
|
|
446
|
+
sid_a: str,
|
|
447
|
+
sid_b: str,
|
|
448
|
+
dim_x: int,
|
|
449
|
+
dim_y: int,
|
|
450
|
+
indices: str,
|
|
451
|
+
indices_a: str | None = None,
|
|
452
|
+
indices_b: str | None = None,
|
|
453
|
+
dim_z: int = -1,
|
|
454
|
+
dr: int = 1,
|
|
455
|
+
complex_mode: int = 0,
|
|
456
|
+
log_scale: bool = False,
|
|
457
|
+
diff_mode: int = 1,
|
|
458
|
+
):
|
|
459
|
+
"""Raw float32 diff values, so hovering a histogram bin can highlight pixels."""
|
|
460
|
+
session_a = SESSIONS.get(sid_a)
|
|
461
|
+
session_b = SESSIONS.get(sid_b)
|
|
462
|
+
if not session_a or not session_b:
|
|
463
|
+
return Response(status_code=404)
|
|
464
|
+
try:
|
|
465
|
+
raw, _, _, _, _ = _compute_diff(
|
|
466
|
+
session_a,
|
|
467
|
+
session_b,
|
|
468
|
+
dim_x,
|
|
469
|
+
dim_y,
|
|
470
|
+
indices,
|
|
471
|
+
dim_z,
|
|
472
|
+
dr,
|
|
473
|
+
complex_mode,
|
|
474
|
+
log_scale,
|
|
475
|
+
diff_mode,
|
|
476
|
+
indices_a=indices_a,
|
|
477
|
+
indices_b=indices_b,
|
|
478
|
+
)
|
|
479
|
+
except Exception:
|
|
480
|
+
return Response(status_code=422)
|
|
481
|
+
raw = np.ascontiguousarray(raw, dtype=np.float32)
|
|
482
|
+
return Response(
|
|
483
|
+
content=raw.tobytes(),
|
|
484
|
+
media_type="application/octet-stream",
|
|
485
|
+
headers={
|
|
486
|
+
"X-ArrayView-Width": str(raw.shape[1]),
|
|
487
|
+
"X-ArrayView-Height": str(raw.shape[0]),
|
|
488
|
+
"Cache-Control": "no-cache",
|
|
489
|
+
},
|
|
490
|
+
)
|
|
491
|
+
|
|
444
492
|
@app.get("/oblique/{sid}")
|
|
445
493
|
def get_oblique(
|
|
446
494
|
sid: str,
|
arrayview/_routes_websocket.py
CHANGED
|
@@ -50,6 +50,47 @@ _RECOVERABLE_DISCONNECT_GRACE_SECONDS = float(
|
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
# Leading word of a compressed frame: b"AVZ1" read as a little-endian uint32.
|
|
54
|
+
# A raw frame starts with its sequence number, which never gets this large.
|
|
55
|
+
_FRAME_Z_MAGIC = b"AVZ1"
|
|
56
|
+
# Below this a frame crosses any link quickly enough as it is.
|
|
57
|
+
_FRAME_Z_MIN_BYTES = 32 * 1024
|
|
58
|
+
_frame_z_remote = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _frame_compression_wanted() -> bool:
|
|
62
|
+
"""True when frames should be deflated before they are sent.
|
|
63
|
+
|
|
64
|
+
Over a VS Code tunnel or Remote-SSH forward every frame crosses the network
|
|
65
|
+
as raw RGBA: three compared 224x224 slices are 600 KB per scroll step, and
|
|
66
|
+
that transfer, not rendering, is the lag. Locally the bytes are free, so
|
|
67
|
+
frames stay raw. ``ARRAYVIEW_FRAME_COMPRESS=1``/``0`` forces either way.
|
|
68
|
+
"""
|
|
69
|
+
global _frame_z_remote
|
|
70
|
+
forced = os.environ.get("ARRAYVIEW_FRAME_COMPRESS")
|
|
71
|
+
if forced in ("0", "1"):
|
|
72
|
+
return forced == "1"
|
|
73
|
+
if _frame_z_remote is None:
|
|
74
|
+
try:
|
|
75
|
+
from arrayview._platform import _is_vscode_remote
|
|
76
|
+
|
|
77
|
+
_frame_z_remote = bool(_is_vscode_remote())
|
|
78
|
+
except Exception:
|
|
79
|
+
_frame_z_remote = False
|
|
80
|
+
return _frame_z_remote
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _pack_frame(payload: bytes) -> bytes:
|
|
84
|
+
"""Deflate *payload* behind the magic word, or return it unchanged when
|
|
85
|
+
that does not save at least a third (noise-like images barely shrink)."""
|
|
86
|
+
import zlib
|
|
87
|
+
|
|
88
|
+
packed = zlib.compress(payload, 1)
|
|
89
|
+
if len(packed) * 3 > len(payload) * 2:
|
|
90
|
+
return payload
|
|
91
|
+
return _FRAME_Z_MAGIC + packed
|
|
92
|
+
|
|
93
|
+
|
|
53
94
|
def _advance_viewer_epoch(sid: str) -> int:
|
|
54
95
|
epoch = _session_mod.VIEWER_CONNECTION_EPOCHS.get(sid, 0) + 1
|
|
55
96
|
_session_mod.VIEWER_CONNECTION_EPOCHS[sid] = epoch
|
|
@@ -368,6 +409,10 @@ def register_websocket_routes(app) -> None:
|
|
|
368
409
|
native_request_id = ws.query_params.get("native_request_id")
|
|
369
410
|
launch_request_id = ws.query_params.get("launch_request_id")
|
|
370
411
|
launch_token = ws.query_params.get("launch_token")
|
|
412
|
+
# Only viewers that can inflate frames ask for them packed.
|
|
413
|
+
compress_frames = (
|
|
414
|
+
ws.query_params.get("frame_z") == "1" and _frame_compression_wanted()
|
|
415
|
+
)
|
|
371
416
|
# Answer the handshake before the array is ready. Holding it closed
|
|
372
417
|
# until the load finished made the viewer decide the backend was dead:
|
|
373
418
|
# it gives up after three attempts and shows "could not connect", which
|
|
@@ -752,6 +797,9 @@ def register_websocket_routes(app) -> None:
|
|
|
752
797
|
).tobytes()
|
|
753
798
|
vf_scale = np.array([vf_result["scale"]], dtype=np.float32).tobytes()
|
|
754
799
|
payload += vf_hdr + vf_scale + arrows.tobytes()
|
|
800
|
+
if compress_frames and len(payload) >= _FRAME_Z_MIN_BYTES:
|
|
801
|
+
raw_payload = payload
|
|
802
|
+
payload = await _render(loop, lambda: _pack_frame(raw_payload))
|
|
755
803
|
await ws.send_bytes(payload)
|
|
756
804
|
if perf:
|
|
757
805
|
total_ms = (time.perf_counter() - total_t0) * 1000.0
|
arrayview/_session.py
CHANGED
|
@@ -870,6 +870,9 @@ def upgrade_memmap_in_background(session) -> bool:
|
|
|
870
870
|
identical, so a render already holding the lazy array stays correct and the
|
|
871
871
|
slice caches stay valid.
|
|
872
872
|
"""
|
|
873
|
+
if getattr(session, "_ram_upgrade_started", False):
|
|
874
|
+
return False
|
|
875
|
+
session._ram_upgrade_started = True
|
|
873
876
|
data = getattr(session, "data", None)
|
|
874
877
|
filepath = getattr(session, "filepath", None)
|
|
875
878
|
if data is None or not filepath:
|
arrayview/_viewer.html
CHANGED
|
@@ -81,6 +81,9 @@
|
|
|
81
81
|
+ 'launch_request_id=' + encodeURIComponent(requestId)
|
|
82
82
|
+ '&launch_token=' + encodeURIComponent(token);
|
|
83
83
|
}
|
|
84
|
+
if (typeof DecompressionStream === 'function') {
|
|
85
|
+
target += (target.indexOf('?') >= 0 ? '&' : '?') + 'frame_z=1';
|
|
86
|
+
}
|
|
84
87
|
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
85
88
|
var url = proto + '//' + location.host + target;
|
|
86
89
|
var early = { url: url, ws: new WebSocket(url), opened: false, events: [] };
|
|
@@ -2130,15 +2133,19 @@
|
|
|
2130
2133
|
.drop-action-reason { display: block; margin-top: 2px; font-size: 9px; color: var(--muted); }
|
|
2131
2134
|
#compare-with-menu {
|
|
2132
2135
|
display: none; position: fixed; z-index: 70; left: 50%; top: 50%;
|
|
2133
|
-
transform: translate(-50%, -50%); width: min(
|
|
2136
|
+
transform: translate(-50%, -50%); width: min(560px, calc(100vw - 24px));
|
|
2134
2137
|
max-height: calc(100vh - 48px); overflow-y: auto;
|
|
2135
2138
|
padding: 8px; background: var(--surface); border: 1px solid var(--border);
|
|
2136
|
-
border-radius: var(--radius-lg); color: var(--text); font:
|
|
2139
|
+
border-radius: var(--radius-lg); color: var(--text); font: 9px var(--mono);
|
|
2137
2140
|
backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
|
|
2138
2141
|
}
|
|
2139
2142
|
#compare-with-menu.visible { display: block; }
|
|
2140
2143
|
#compare-with-title { padding: 4px 6px 2px; }
|
|
2141
|
-
#compare-with-summary { color: var(--muted); padding: 0 6px 7px; font-size:
|
|
2144
|
+
#compare-with-summary { color: var(--muted); padding: 0 6px 7px; font-size: 9px; }
|
|
2145
|
+
#compare-with-title { font-size: 10px; }
|
|
2146
|
+
#compare-with-list { font-size: 10px; }
|
|
2147
|
+
.compare-with-path { word-break: break-all; }
|
|
2148
|
+
#compare-with-list .drop-action-reason { font-size: 8px; }
|
|
2142
2149
|
.compare-with-key { color: var(--muted); margin-right: 8px; }
|
|
2143
2150
|
.mode-badge {
|
|
2144
2151
|
display: inline-block; font-size: 0.62em; font-weight: 600;
|
|
@@ -6359,6 +6366,41 @@
|
|
|
6359
6366
|
return ws;
|
|
6360
6367
|
}
|
|
6361
6368
|
|
|
6369
|
+
const _FRAME_Z_SUPPORTED = typeof DecompressionStream === 'function';
|
|
6370
|
+
const _FRAME_Z_MAGIC = 0x315A5641; // "AVZ1" read little-endian
|
|
6371
|
+
|
|
6372
|
+
function _inflateFrame(buf) {
|
|
6373
|
+
const body = new Blob([new Uint8Array(buf, 4)]).stream()
|
|
6374
|
+
.pipeThrough(new DecompressionStream('deflate'));
|
|
6375
|
+
return new Response(body).arrayBuffer();
|
|
6376
|
+
}
|
|
6377
|
+
|
|
6378
|
+
/**
|
|
6379
|
+
* Wrap a socket message handler so deflated frames arrive as the raw
|
|
6380
|
+
* frames every handler expects. Inflating is asynchronous, so while
|
|
6381
|
+
* one is in flight later messages queue behind it and keep their
|
|
6382
|
+
* order; with none pending a message is handed over synchronously.
|
|
6383
|
+
*/
|
|
6384
|
+
function _inflatingMessageHandler(handler) {
|
|
6385
|
+
let pending = null;
|
|
6386
|
+
return function (event) {
|
|
6387
|
+
const data = event.data;
|
|
6388
|
+
const packed = data instanceof ArrayBuffer && data.byteLength >= 8
|
|
6389
|
+
&& new Uint32Array(data, 0, 1)[0] === _FRAME_Z_MAGIC;
|
|
6390
|
+
if (!packed && !pending) { handler.call(this, event); return; }
|
|
6391
|
+
const self = this;
|
|
6392
|
+
const ready = packed ? _inflateFrame(data) : null;
|
|
6393
|
+
const step = (pending || Promise.resolve())
|
|
6394
|
+
.then(() => ready)
|
|
6395
|
+
.then(raw => {
|
|
6396
|
+
handler.call(self, raw ? new MessageEvent('message', { data: raw }) : event);
|
|
6397
|
+
})
|
|
6398
|
+
.catch(err => console.error('frame inflate failed', err));
|
|
6399
|
+
pending = step;
|
|
6400
|
+
step.then(() => { if (pending === step) pending = null; });
|
|
6401
|
+
};
|
|
6402
|
+
}
|
|
6403
|
+
|
|
6362
6404
|
function createTransport(transportSid, onMessage, onOpen, onClose, onError, adoptEarly = false) {
|
|
6363
6405
|
let wsTarget = resolveServerPath(`/ws/${transportSid}`);
|
|
6364
6406
|
const nativeRequestId = urlParams.get('native_request_id');
|
|
@@ -6376,6 +6418,12 @@
|
|
|
6376
6418
|
wsTarget += `${separator}launch_request_id=${encodeURIComponent(launchRequestId)}`;
|
|
6377
6419
|
wsTarget += `&launch_token=${encodeURIComponent(launchToken)}`;
|
|
6378
6420
|
}
|
|
6421
|
+
// Ask for frames deflated; the server only does so over a remote
|
|
6422
|
+
// link. Must stay last so the URL matches the early socket's.
|
|
6423
|
+
if (_FRAME_Z_SUPPORTED) {
|
|
6424
|
+
const separator = wsTarget.includes('?') ? '&' : '?';
|
|
6425
|
+
wsTarget += `${separator}frame_z=1`;
|
|
6426
|
+
}
|
|
6379
6427
|
const socketUrl = () => {
|
|
6380
6428
|
if (_embeddedServerOrigin) {
|
|
6381
6429
|
const wsUrl = new URL(wsTarget);
|
|
@@ -6393,7 +6441,7 @@
|
|
|
6393
6441
|
const ws = early || (hedged ? _createHedgedWebSocket(makeSocket) : makeSocket());
|
|
6394
6442
|
ws.binaryType = 'arraybuffer';
|
|
6395
6443
|
if (onOpen) ws.onopen = onOpen;
|
|
6396
|
-
if (onMessage) ws.onmessage = onMessage;
|
|
6444
|
+
if (onMessage) ws.onmessage = _inflatingMessageHandler(onMessage);
|
|
6397
6445
|
if (onClose) ws.onclose = onClose;
|
|
6398
6446
|
if (onError) ws.onerror = onError;
|
|
6399
6447
|
return ws;
|
|
@@ -7487,7 +7535,12 @@
|
|
|
7487
7535
|
let _diffAutoRangeKey = null;
|
|
7488
7536
|
let _diffCenterColormap = ''; // override colormap for diff/center pane; '' = server default
|
|
7489
7537
|
let _diffCenterColormapStops = null; // gradient stops for center pane colorbar
|
|
7490
|
-
const DIFF_COLORMAPS = ['RdBu_r', 'coolwarm', 'bwr', 'seismic', 'PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdYlBu_r', 'RdYlGn', 'twilight_shifted'];
|
|
7538
|
+
const DIFF_COLORMAPS = ['icefire', 'RdBu_r', 'coolwarm', 'bwr', 'seismic', 'PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdYlBu_r', 'RdYlGn', 'twilight_shifted'];
|
|
7539
|
+
// A−B default follows the theme: icefire's dark centre blends into the
|
|
7540
|
+
// dark viewport (masked voxels stay dark); light theme keeps RdBu_r.
|
|
7541
|
+
function _diffDefaultColormap() {
|
|
7542
|
+
return diffMode === 1 ? (isDark ? 'icefire' : 'RdBu_r') : 'afmhot';
|
|
7543
|
+
}
|
|
7491
7544
|
const ABS_DIFF_COLORMAPS = ['afmhot', 'hot', 'inferno', 'magma', 'plasma', 'viridis', 'gray', 'cividis'];
|
|
7492
7545
|
let _mouseOverCenterPane = false; // true when mouse hovers diff or wipe pane
|
|
7493
7546
|
let compareFrames = [];
|
|
@@ -10295,6 +10348,7 @@
|
|
|
10295
10348
|
});
|
|
10296
10349
|
},
|
|
10297
10350
|
fetchHistogram: () => _fetchHistogramCompare(),
|
|
10351
|
+
getHistogramVersion: () => _compareHistogramVersion(),
|
|
10298
10352
|
onWindowChange: () => { compareRender(); },
|
|
10299
10353
|
}));
|
|
10300
10354
|
}
|
|
@@ -10356,13 +10410,18 @@
|
|
|
10356
10410
|
hoverValueInEggs: true,
|
|
10357
10411
|
hoverValueAnchor: () => modeManager.getViewById('compare-diff-center')?.canvas
|
|
10358
10412
|
|| document.getElementById('compare-diff-pane'),
|
|
10359
|
-
getStops: () => _diffCenterColormapStops || COLORMAP_GRADIENT_STOPS[
|
|
10413
|
+
getStops: () => _diffCenterColormapStops || COLORMAP_GRADIENT_STOPS[_diffDefaultColormap()] || COLORMAP_GRADIENT_STOPS['gray'] || [],
|
|
10360
10414
|
getRange: () => ({ vmin: _lastDiffVmin, vmax: _lastDiffVmax }),
|
|
10361
10415
|
getWindow: () => ({ vmin: _diffManualVmin ?? _lastDiffVmin, vmax: _diffManualVmax ?? _lastDiffVmax }),
|
|
10362
10416
|
setWindow: (lo, hi) => { _diffManualVmin = lo; _diffManualVmax = hi; { const _dv = modeManager.getViewById('compare-diff-center'); if (_dv) { _dv.displayState.vmin = _diffManualVmin; _dv.displayState.vmax = _diffManualVmax; } } },
|
|
10363
10417
|
fetchHistogram: () => _fetchDiffHistogram(),
|
|
10364
10418
|
getHistogramVersion: () => _diffHistogramVersion(),
|
|
10365
10419
|
onWindowChange: () => { fetchAndDrawDiff(); },
|
|
10420
|
+
onMouseEnter: () => { _colorbarLebesgueHover = true; },
|
|
10421
|
+
onMouseLeave: () => { _stopColorbarLebesgueHover(); },
|
|
10422
|
+
onDragStart: () => { _stopColorbarLebesgueHover(); },
|
|
10423
|
+
onDragEnd: () => { if (_diffCenterCb?._mouseOver) _colorbarLebesgueHover = true; },
|
|
10424
|
+
onBinHover: (binIdx) => { _drawLebesgueHighlightForColorbar(_diffCenterCb, binIdx); },
|
|
10366
10425
|
});
|
|
10367
10426
|
}
|
|
10368
10427
|
}
|
|
@@ -14049,7 +14108,7 @@
|
|
|
14049
14108
|
getWindow: () => ({ vmin: manualVmin ?? currentVmin, vmax: manualVmax ?? currentVmax }),
|
|
14050
14109
|
setWindow: (lo, hi) => { manualVmin = lo; manualVmax = hi; { const _dv = modeManager.getFocusedView() || modeManager.currentViews[0]; if (_dv) { _dv.displayState.vmin = manualVmin; _dv.displayState.vmax = manualVmax; } } },
|
|
14051
14110
|
fetchHistogram: () => compareActive ? _fetchHistogramCompare() : _fetchVolumeHistogram(_volHistSpatialOpts()),
|
|
14052
|
-
getHistogramVersion: () => compareActive ?
|
|
14111
|
+
getHistogramVersion: () => compareActive ? _compareHistogramVersion() : _volumeHistCacheKey(_volHistSpatialOpts()),
|
|
14053
14112
|
onAnimFrame: (cb) => {
|
|
14054
14113
|
// Layout recalculation during expand/collapse animation
|
|
14055
14114
|
if (multiViewActive || compareMvActive) {
|
|
@@ -14149,7 +14208,8 @@
|
|
|
14149
14208
|
const sdKey = scrollDims.slice().sort((a, b) => a - b).join(',');
|
|
14150
14209
|
const qmriRole = opts.qmriRole || '';
|
|
14151
14210
|
const numBins = opts.bins || 64;
|
|
14152
|
-
|
|
14211
|
+
const sidKey = opts.sid && opts.sid !== sid ? `${opts.sid}|` : '';
|
|
14212
|
+
return `${sidKey}${fetchDimX}:${fetchDimY}:${sdKey}:${fixedIndices}:${cMode}:${qmriRole}:${numBins}`;
|
|
14153
14213
|
}
|
|
14154
14214
|
|
|
14155
14215
|
async function _fetchVolumeHistogram(opts = {}) {
|
|
@@ -14181,7 +14241,9 @@
|
|
|
14181
14241
|
const qmriRole = opts.qmriRole || '';
|
|
14182
14242
|
const numBins = opts.bins || 64;
|
|
14183
14243
|
const exclZ = excludeZeros ? '1' : '0';
|
|
14184
|
-
const
|
|
14244
|
+
const fetchSid = opts.sid || sid;
|
|
14245
|
+
const sidKey = fetchSid !== sid ? `${fetchSid}|` : '';
|
|
14246
|
+
const cacheKey = `${sidKey}${fetchDimX}:${fetchDimY}:${sdKey}:${fixedIndices}:${cMode}:${qmriRole}:${numBins}:ez${exclZ}`;
|
|
14185
14247
|
|
|
14186
14248
|
if (_volHistCache[cacheKey]) {
|
|
14187
14249
|
_histDataVersion = cacheKey;
|
|
@@ -14203,13 +14265,9 @@
|
|
|
14203
14265
|
if (qmriRole) params.set('qmri_role', qmriRole);
|
|
14204
14266
|
if (excludeZeros) params.set('exclude_zeros', '1');
|
|
14205
14267
|
const request = (async () => {
|
|
14206
|
-
|
|
14207
|
-
|
|
14208
|
-
|
|
14209
|
-
const data = await resp.json();
|
|
14210
|
-
_volHistCache[cacheKey] = data;
|
|
14211
|
-
return data;
|
|
14212
|
-
} catch (_) { return null; }
|
|
14268
|
+
const data = await _fetchHistogramJson(`/volume-histogram/${fetchSid}?${params}`);
|
|
14269
|
+
if (data) _volHistCache[cacheKey] = data;
|
|
14270
|
+
return data;
|
|
14213
14271
|
})();
|
|
14214
14272
|
_volHistInFlight.set(cacheKey, request);
|
|
14215
14273
|
try {
|
|
@@ -14612,12 +14670,7 @@
|
|
|
14612
14670
|
if (!normalized) return null;
|
|
14613
14671
|
const lo = normalized.vmin;
|
|
14614
14672
|
const hi = normalized.vmax;
|
|
14615
|
-
view
|
|
14616
|
-
view.lockedVmax = hi;
|
|
14617
|
-
const ownerViews = compareQmriActive ? compareQmriViews : qmriViews;
|
|
14618
|
-
const idx = ownerViews.indexOf(view);
|
|
14619
|
-
const qv = idx >= 0 ? modeManager.currentViews[idx] : null;
|
|
14620
|
-
if (qv) { qv.displayState.vmin = lo; qv.displayState.vmax = hi; }
|
|
14673
|
+
_qmriStoreWindow(view, lo, hi);
|
|
14621
14674
|
return { vmin: lo, vmax: hi };
|
|
14622
14675
|
}
|
|
14623
14676
|
|
|
@@ -14642,6 +14695,11 @@
|
|
|
14642
14695
|
vmax: range.vmin + span * preset.hi,
|
|
14643
14696
|
};
|
|
14644
14697
|
}
|
|
14698
|
+
} else if (compareQmriActive) {
|
|
14699
|
+
hist = await _cqFetchColumnHistogram(view);
|
|
14700
|
+
if (generation != null && generation !== _qmriPresetCycleGeneration) return false;
|
|
14701
|
+
win = _quantileWindowFromHistogram(hist, preset);
|
|
14702
|
+
hist = _normalizeQmriHistogramForColorbar(view, hist);
|
|
14645
14703
|
} else {
|
|
14646
14704
|
const fixedIndices = `${qmriDim}:${view.qmriIdx}`;
|
|
14647
14705
|
hist = await _fetchVolumeHistogram({
|
|
@@ -14680,14 +14738,17 @@
|
|
|
14680
14738
|
});
|
|
14681
14739
|
}
|
|
14682
14740
|
if (view._colorBar) { view._colorBar.draw(); view._colorBar.updateLabels(); }
|
|
14683
|
-
|
|
14684
|
-
else qvRender(view);
|
|
14741
|
+
_qmriRenderWindowOwners(view);
|
|
14685
14742
|
return true;
|
|
14686
14743
|
}
|
|
14687
14744
|
|
|
14688
14745
|
async function _cycleQmriPaneRangePreset() {
|
|
14689
14746
|
if (qmriMvActive) { await _cycleQmriMvRangePreset(); return true; }
|
|
14690
|
-
|
|
14747
|
+
// In compare-qMRI only the bottom row shows colorbars, and each map
|
|
14748
|
+
// column shares one range, so act through the column's bottom pane.
|
|
14749
|
+
const view = compareQmriActive && _hoveredQmriView
|
|
14750
|
+
? _cqColumnLead(_hoveredQmriView)
|
|
14751
|
+
: _hoveredQmriView;
|
|
14691
14752
|
const activeCb = view
|
|
14692
14753
|
? (qmriMosaicActive && view._mosaicColorBar ? view._mosaicColorBar : view._colorBar)
|
|
14693
14754
|
: null;
|
|
@@ -14712,7 +14773,9 @@
|
|
|
14712
14773
|
if (view) {
|
|
14713
14774
|
await _applyQmriRangePresetToView(view, preset, generation);
|
|
14714
14775
|
} else {
|
|
14715
|
-
const paneViews = compareQmriActive
|
|
14776
|
+
const paneViews = compareQmriActive
|
|
14777
|
+
? compareQmriViews.filter(v => _cqColumnLead(v) === v)
|
|
14778
|
+
: qmriViews;
|
|
14716
14779
|
await Promise.all(paneViews.map(v => _applyQmriRangePresetToView(v, preset, generation)));
|
|
14717
14780
|
if (!compareQmriActive) qvScaleAllCanvases();
|
|
14718
14781
|
}
|
|
@@ -14843,8 +14906,46 @@
|
|
|
14843
14906
|
}
|
|
14844
14907
|
|
|
14845
14908
|
/** Fetch histogram for all compare sessions and merge bins element-wise. */
|
|
14846
|
-
|
|
14847
|
-
|
|
14909
|
+
/**
|
|
14910
|
+
* Identity of the slice histogram `_fetchHistogramCompare` builds.
|
|
14911
|
+
* Without it the colorbars kept the first histogram for good, so after
|
|
14912
|
+
* moving to another map `d` cut presets from the old map's values.
|
|
14913
|
+
*/
|
|
14914
|
+
function _compareHistogramVersion() {
|
|
14915
|
+
return [
|
|
14916
|
+
getCompareRenderSids().join('/'), indices.join(','),
|
|
14917
|
+
dim_x, dim_y, complexMode,
|
|
14918
|
+
].join('|');
|
|
14919
|
+
}
|
|
14920
|
+
|
|
14921
|
+
// `d` waits on the histogram request in flight, and every later press
|
|
14922
|
+
// joins that same request. One lost on a forwarded link never settles,
|
|
14923
|
+
// which left `d` dead for the rest of the session, so cap the wait;
|
|
14924
|
+
// the next press then asks again.
|
|
14925
|
+
const _HIST_FETCH_TIMEOUT_MS = 15000;
|
|
14926
|
+
async function _fetchHistogramJson(url) {
|
|
14927
|
+
const ctl = typeof AbortController === 'function' ? new AbortController() : null;
|
|
14928
|
+
const timer = ctl ? setTimeout(() => ctl.abort(), _HIST_FETCH_TIMEOUT_MS) : null;
|
|
14929
|
+
try {
|
|
14930
|
+
const resp = await proxyFetch(url, ctl ? { signal: ctl.signal } : undefined);
|
|
14931
|
+
return resp.ok ? await resp.json() : null;
|
|
14932
|
+
} catch (_) {
|
|
14933
|
+
return null;
|
|
14934
|
+
} finally {
|
|
14935
|
+
if (timer) clearTimeout(timer);
|
|
14936
|
+
}
|
|
14937
|
+
}
|
|
14938
|
+
|
|
14939
|
+
let _histCompareInFlight = null;
|
|
14940
|
+
function _fetchHistogramCompare() {
|
|
14941
|
+
if (_histCompareInFlight) return _histCompareInFlight;
|
|
14942
|
+
const request = _fetchHistogramCompareOnce();
|
|
14943
|
+
_histCompareInFlight = request;
|
|
14944
|
+
request.finally(() => { if (_histCompareInFlight === request) _histCompareInFlight = null; });
|
|
14945
|
+
return request;
|
|
14946
|
+
}
|
|
14947
|
+
|
|
14948
|
+
async function _fetchHistogramCompareOnce() {
|
|
14848
14949
|
_histFetching = true;
|
|
14849
14950
|
const allSids = getCompareRenderSids();
|
|
14850
14951
|
const fetchDimX = dim_x;
|
|
@@ -14857,12 +14958,10 @@
|
|
|
14857
14958
|
bins: 64,
|
|
14858
14959
|
});
|
|
14859
14960
|
try {
|
|
14860
|
-
const histPromises = allSids.map(s =>
|
|
14861
|
-
proxyFetch(`/histogram/${s}?${params}`).then(r => r.ok ? r.json() : null)
|
|
14862
|
-
);
|
|
14961
|
+
const histPromises = allSids.map(s => _fetchHistogramJson(`/histogram/${s}?${params}`));
|
|
14863
14962
|
const allHists = await Promise.all(histPromises);
|
|
14864
14963
|
const valid = allHists.filter(Boolean);
|
|
14865
|
-
if (!valid.length) { _histFetching = false; return; }
|
|
14964
|
+
if (!valid.length) { _histFetching = false; return null; }
|
|
14866
14965
|
// Start from first valid result, then sum counts from the rest
|
|
14867
14966
|
const merged = JSON.parse(JSON.stringify(valid[0]));
|
|
14868
14967
|
for (let i = 1; i < valid.length; i++) {
|
|
@@ -14895,7 +14994,8 @@
|
|
|
14895
14994
|
].join('|');
|
|
14896
14995
|
}
|
|
14897
14996
|
|
|
14898
|
-
|
|
14997
|
+
/** sids + query shared by the diff histogram and diff value requests. */
|
|
14998
|
+
function _diffAnalysisQuery() {
|
|
14899
14999
|
if (!diffMode || !compareActive || compareDisplaySids.length < 2) return null;
|
|
14900
15000
|
const [sid_a, sid_b] = compareDisplaySids;
|
|
14901
15001
|
const params = new URLSearchParams({
|
|
@@ -14906,7 +15006,6 @@
|
|
|
14906
15006
|
complex_mode: String(complexMode),
|
|
14907
15007
|
log_scale: String(logScale),
|
|
14908
15008
|
diff_mode: String(diffMode),
|
|
14909
|
-
bins: '64',
|
|
14910
15009
|
});
|
|
14911
15010
|
if (detachedDimMode && detachedDim >= 0) {
|
|
14912
15011
|
const indicesA = [...indices];
|
|
@@ -14916,8 +15015,14 @@
|
|
|
14916
15015
|
params.set('indices_a', indicesA.join(','));
|
|
14917
15016
|
params.set('indices_b', indicesB.join(','));
|
|
14918
15017
|
}
|
|
15018
|
+
return { path: `${sid_a}/${sid_b}?${params}` };
|
|
15019
|
+
}
|
|
15020
|
+
|
|
15021
|
+
async function _fetchDiffHistogram() {
|
|
15022
|
+
const q = _diffAnalysisQuery();
|
|
15023
|
+
if (!q) return null;
|
|
14919
15024
|
try {
|
|
14920
|
-
const resp = await proxyFetch(`/diff-histogram/${
|
|
15025
|
+
const resp = await proxyFetch(`/diff-histogram/${q.path}&bins=64`);
|
|
14921
15026
|
if (!resp.ok) return null;
|
|
14922
15027
|
return await resp.json();
|
|
14923
15028
|
} catch (_) { return null; }
|
|
@@ -15142,7 +15247,7 @@
|
|
|
15142
15247
|
}
|
|
15143
15248
|
|
|
15144
15249
|
/** Draw Lebesgue highlight on a single overlay canvas using the given slice data. */
|
|
15145
|
-
function _drawLebesgueOnCanvas(overlayCv, targetCv, sliceData, sliceW, sliceH, binLow, binHigh) {
|
|
15250
|
+
function _drawLebesgueOnCanvas(overlayCv, targetCv, sliceData, sliceW, sliceH, binLow, binHigh, flipLikeDiff = false) {
|
|
15146
15251
|
const octx = overlayCv.getContext('2d');
|
|
15147
15252
|
if (!octx) return;
|
|
15148
15253
|
const cw = targetCv.width, ch = targetCv.height;
|
|
@@ -15152,7 +15257,9 @@
|
|
|
15152
15257
|
}
|
|
15153
15258
|
overlayCv.style.width = targetCv.style.width;
|
|
15154
15259
|
overlayCv.style.height = targetCv.style.height;
|
|
15155
|
-
|
|
15260
|
+
// The diff pane flips through applyFlips (per mosaic tile), so its
|
|
15261
|
+
// mask is built unflipped and flipped the same way afterwards.
|
|
15262
|
+
const fx = flipLikeDiff ? false : flip_x, fy = flipLikeDiff ? false : flip_y;
|
|
15156
15263
|
if (!overlayCv._lebesgueMask) overlayCv._lebesgueMask = document.createElement('canvas');
|
|
15157
15264
|
const maskCv = overlayCv._lebesgueMask;
|
|
15158
15265
|
if (maskCv.width !== sliceW || maskCv.height !== sliceH) {
|
|
@@ -15161,7 +15268,7 @@
|
|
|
15161
15268
|
}
|
|
15162
15269
|
const mctx = maskCv.getContext('2d');
|
|
15163
15270
|
if (!mctx) return;
|
|
15164
|
-
|
|
15271
|
+
let img = mctx.createImageData(sliceW, sliceH);
|
|
15165
15272
|
const buf = img.data;
|
|
15166
15273
|
const alpha = Math.round(255 * 0.55);
|
|
15167
15274
|
for (let y = 0; y < sliceH; y++) {
|
|
@@ -15175,6 +15282,7 @@
|
|
|
15175
15282
|
buf[i + 3] = alpha;
|
|
15176
15283
|
}
|
|
15177
15284
|
}
|
|
15285
|
+
if (flipLikeDiff && (flip_x || flip_y)) img = applyFlips(img, sliceW, sliceH);
|
|
15178
15286
|
mctx.putImageData(img, 0, 0);
|
|
15179
15287
|
octx.clearRect(0, 0, cw, ch);
|
|
15180
15288
|
octx.imageSmoothingEnabled = false;
|
|
@@ -15196,11 +15304,11 @@
|
|
|
15196
15304
|
return overlay;
|
|
15197
15305
|
}
|
|
15198
15306
|
|
|
15199
|
-
function _drawLebesgueHighlightOnCanvas(targetCv, sliceEntry, binLow, binHigh) {
|
|
15307
|
+
function _drawLebesgueHighlightOnCanvas(targetCv, sliceEntry, binLow, binHigh, flipLikeDiff = false) {
|
|
15200
15308
|
if (!targetCv || !sliceEntry || !sliceEntry.slice || !sliceEntry.w || !sliceEntry.h) return false;
|
|
15201
15309
|
const overlay = _getLebesgueOverlay(targetCv);
|
|
15202
15310
|
if (!overlay) return false;
|
|
15203
|
-
_drawLebesgueOnCanvas(overlay, targetCv, sliceEntry.slice, sliceEntry.w, sliceEntry.h, binLow, binHigh);
|
|
15311
|
+
_drawLebesgueOnCanvas(overlay, targetCv, sliceEntry.slice, sliceEntry.w, sliceEntry.h, binLow, binHigh, flipLikeDiff);
|
|
15204
15312
|
return true;
|
|
15205
15313
|
}
|
|
15206
15314
|
|
|
@@ -15228,6 +15336,41 @@
|
|
|
15228
15336
|
}
|
|
15229
15337
|
}
|
|
15230
15338
|
|
|
15339
|
+
// Raw diff values for the centre pane, keyed by the diff query they answer.
|
|
15340
|
+
let _diffLebesgueEntry = null;
|
|
15341
|
+
let _diffLebesguePending = null;
|
|
15342
|
+
|
|
15343
|
+
async function _fetchDiffLebesgueValues(path) {
|
|
15344
|
+
try {
|
|
15345
|
+
const resp = await proxyFetch(`/diff-values/${path}`);
|
|
15346
|
+
if (!resp.ok) return null;
|
|
15347
|
+
const w = parseInt(resp.headers.get('X-ArrayView-Width')) || 0;
|
|
15348
|
+
const h = parseInt(resp.headers.get('X-ArrayView-Height')) || 0;
|
|
15349
|
+
return { slice: new Float32Array(await resp.arrayBuffer()), w, h, key: path };
|
|
15350
|
+
} catch (_) { return null; }
|
|
15351
|
+
}
|
|
15352
|
+
|
|
15353
|
+
function _drawDiffLebesgueHighlight(cb, binIdx, binLow, binHigh) {
|
|
15354
|
+
const q = _diffAnalysisQuery();
|
|
15355
|
+
const target = document.getElementById('compare-diff-canvas');
|
|
15356
|
+
if (!q || !target) return;
|
|
15357
|
+
if (_diffLebesgueEntry && _diffLebesgueEntry.key === q.path) {
|
|
15358
|
+
_drawLebesgueHighlightOnCanvas(target, _diffLebesgueEntry, binLow, binHigh, true);
|
|
15359
|
+
return;
|
|
15360
|
+
}
|
|
15361
|
+
if (_diffLebesguePending === q.path) return;
|
|
15362
|
+
_diffLebesguePending = q.path;
|
|
15363
|
+
_fetchDiffLebesgueValues(q.path).then(entry => {
|
|
15364
|
+
if (_diffLebesguePending === q.path) _diffLebesguePending = null;
|
|
15365
|
+
if (!entry) return;
|
|
15366
|
+
_diffLebesgueEntry = entry;
|
|
15367
|
+
// Still hovering the diff colorbar → draw the bin under the cursor now.
|
|
15368
|
+
if (_colorbarLebesgueHoverActive() && _lastLebesgueColorBar === cb && _lastLebesgueBin >= 0) {
|
|
15369
|
+
_drawLebesgueHighlightForColorbar(cb, _lastLebesgueBin, true);
|
|
15370
|
+
}
|
|
15371
|
+
});
|
|
15372
|
+
}
|
|
15373
|
+
|
|
15231
15374
|
function _drawLebesgueHighlightForColorbar(cb, binIdx, force = false) {
|
|
15232
15375
|
if (!_colorbarLebesgueHoverActive()) return;
|
|
15233
15376
|
const hd = cb?._histData;
|
|
@@ -15246,6 +15389,7 @@
|
|
|
15246
15389
|
const binLow = edges[binIdx];
|
|
15247
15390
|
const binHigh = edges[binIdx + 1] + (binIdx === counts.length - 1 ? 1e-9 : 0);
|
|
15248
15391
|
if (cb?._lebesgueView) _drawLebesgueHighlightForQmriPane(cb._lebesgueView, binLow, binHigh);
|
|
15392
|
+
else if (cb && cb === _diffCenterCb) _drawDiffLebesgueHighlight(cb, binIdx, binLow, binHigh);
|
|
15249
15393
|
else _drawLebesgueHighlight(binLow, binHigh);
|
|
15250
15394
|
if (cb === primaryCb) _highlightHistBin(binIdx);
|
|
15251
15395
|
}
|
|
@@ -16265,7 +16409,7 @@
|
|
|
16265
16409
|
if (_metadataProcessed) updateView(); // reconnect: metadata already known
|
|
16266
16410
|
};
|
|
16267
16411
|
|
|
16268
|
-
ws.onmessage = (event) => {
|
|
16412
|
+
ws.onmessage = _inflatingMessageHandler((event) => {
|
|
16269
16413
|
// Text message: server pushes metadata as first JSON message
|
|
16270
16414
|
if (typeof event.data === 'string') {
|
|
16271
16415
|
try {
|
|
@@ -16396,7 +16540,7 @@
|
|
|
16396
16540
|
} else if (isPlaying) {
|
|
16397
16541
|
playNext();
|
|
16398
16542
|
}
|
|
16399
|
-
};
|
|
16543
|
+
});
|
|
16400
16544
|
|
|
16401
16545
|
ws.onclose = () => {
|
|
16402
16546
|
_clearWsOpenTimer();
|
|
@@ -17106,7 +17250,7 @@
|
|
|
17106
17250
|
params.set('indices_a', indicesA.join(','));
|
|
17107
17251
|
params.set('indices_b', indicesB.join(','));
|
|
17108
17252
|
}
|
|
17109
|
-
|
|
17253
|
+
params.set('diff_colormap', _diffCenterColormap || _diffDefaultColormap());
|
|
17110
17254
|
const requestVmin = _diffManualVmin ?? _diffAutoVmin;
|
|
17111
17255
|
const requestVmax = _diffManualVmax ?? _diffAutoVmax;
|
|
17112
17256
|
if (requestVmin !== null) params.set('vmin_override', String(requestVmin));
|
|
@@ -23908,6 +24052,7 @@
|
|
|
23908
24052
|
return {
|
|
23909
24053
|
sid: s.sid,
|
|
23910
24054
|
label,
|
|
24055
|
+
display: s.filepath || label,
|
|
23911
24056
|
detail: `${(s.shape || []).join(' x ')}${s.var && s.name ? ` · ${s.name}` : ''}`,
|
|
23912
24057
|
enabled: sameShape,
|
|
23913
24058
|
};
|
|
@@ -23920,7 +24065,7 @@
|
|
|
23920
24065
|
list.innerHTML = _compareWithChoices.map((c, i) => {
|
|
23921
24066
|
const keyHint = i < 9 ? `<span class="compare-with-key">${i + 1}</span>` : '';
|
|
23922
24067
|
const reason = c.enabled ? c.detail : `${c.detail} · needs shape ${(shape || []).join(' x ')}`;
|
|
23923
|
-
return `<button class="drop-action" data-compare-with="${i}" ${c.enabled ? '' : 'disabled'}>${keyHint}
|
|
24068
|
+
return `<button class="drop-action" data-compare-with="${i}" ${c.enabled ? '' : 'disabled'}>${keyHint}<span class="compare-with-path">${_escapeHtml(c.display)}</span><span class="drop-action-reason">${_escapeHtml(reason)}</span></button>`;
|
|
23924
24069
|
}).join('');
|
|
23925
24070
|
list.querySelectorAll('.drop-action:not(:disabled)').forEach(button => {
|
|
23926
24071
|
button.addEventListener('click', () => _pickCompareWith(_compareWithChoices[Number(button.dataset.compareWith)]));
|
|
@@ -24522,6 +24667,7 @@
|
|
|
24522
24667
|
if (idx >= 0) {
|
|
24523
24668
|
applyTheme(idx);
|
|
24524
24669
|
_reconcileLayout();
|
|
24670
|
+
if (compareActive && diffMode === 1 && !_diffCenterColormap) fetchAndDrawDiff();
|
|
24525
24671
|
}
|
|
24526
24672
|
} else if (key === 'rounded_panes') {
|
|
24527
24673
|
roundedPanes = !!value;
|
|
@@ -25017,11 +25163,8 @@
|
|
|
25017
25163
|
const nextVmin = center - w / 2;
|
|
25018
25164
|
const nextVmax = center + w / 2;
|
|
25019
25165
|
if (_bcTargetView) {
|
|
25020
|
-
_bcTargetView
|
|
25021
|
-
_bcTargetView
|
|
25022
|
-
const _qv = modeManager.currentViews.find(v => v && v.qmriIdx === _bcTargetView.qmriIdx);
|
|
25023
|
-
if (_qv) { _qv.displayState.vmin = nextVmin; _qv.displayState.vmax = nextVmax; }
|
|
25024
|
-
qvRender(_bcTargetView);
|
|
25166
|
+
_qmriStoreWindow(_bcTargetView, nextVmin, nextVmax);
|
|
25167
|
+
_qmriRenderWindowOwners(_bcTargetView);
|
|
25025
25168
|
} else {
|
|
25026
25169
|
manualVmin = nextVmin;
|
|
25027
25170
|
manualVmax = nextVmax;
|
|
@@ -25361,6 +25504,7 @@
|
|
|
25361
25504
|
}
|
|
25362
25505
|
function _qmriArmWindowLevel(view, e) {
|
|
25363
25506
|
if (_pixelInfoHeld) { _pixelInfoHeld = false; hideAllPixelInfo(); }
|
|
25507
|
+
if (compareQmriActive) view = _cqColumnLead(view);
|
|
25364
25508
|
const pending = _bcPendingDrag = {
|
|
25365
25509
|
startX: e.clientX, startY: e.clientY,
|
|
25366
25510
|
targetView: view, targetCb: view._colorBar,
|
|
@@ -25407,7 +25551,7 @@
|
|
|
25407
25551
|
? under.closest(_INFO_CURSOR_CANVAS_SELECTOR)
|
|
25408
25552
|
: null;
|
|
25409
25553
|
_reconcileInfoCursor();
|
|
25410
|
-
if (targetView)
|
|
25554
|
+
if (targetView) _qmriRenderWindowOwners(targetView);
|
|
25411
25555
|
else updateView();
|
|
25412
25556
|
saveState();
|
|
25413
25557
|
// The drag has definitively ended (this is mouseup, not a hover
|
|
@@ -27985,7 +28129,7 @@
|
|
|
27985
28129
|
// Only diff modes 1-3 have their own center colorbar; overlay/wipe/flicker/checker do not
|
|
27986
28130
|
if (diffMode === 0) return;
|
|
27987
28131
|
const pool = (diffMode === 1) ? DIFF_COLORMAPS : ABS_DIFF_COLORMAPS;
|
|
27988
|
-
const idx = pool.indexOf(_diffCenterColormap);
|
|
28132
|
+
const idx = pool.indexOf(_diffCenterColormap || _diffDefaultColormap());
|
|
27989
28133
|
const next = (idx + 1) % pool.length;
|
|
27990
28134
|
_diffCenterColormap = pool[next];
|
|
27991
28135
|
_diffCenterColormapStops = COLORMAP_GRADIENT_STOPS[_diffCenterColormap] || null;
|
|
@@ -28466,6 +28610,7 @@
|
|
|
28466
28610
|
run: (ctx, e) => {
|
|
28467
28611
|
applyTheme((themeIdx + 1) % THEMES.length);
|
|
28468
28612
|
drawSlimColorbar(cbMarkerFrac);
|
|
28613
|
+
if (compareActive && diffMode === 1 && !_diffCenterColormap) fetchAndDrawDiff();
|
|
28469
28614
|
showStatus(`theme: ${THEMES[themeIdx]}`);
|
|
28470
28615
|
saveState();
|
|
28471
28616
|
},
|
|
@@ -29471,7 +29616,7 @@
|
|
|
29471
29616
|
if ((_cmapStrip && _cmapStrip.classList.contains('visible')) && !qmriActive && !compareQmriActive && !rgbMode) {
|
|
29472
29617
|
if (_cmapStripMode === 'diff-center') {
|
|
29473
29618
|
const pool = (diffMode === 1) ? DIFF_COLORMAPS : ABS_DIFF_COLORMAPS;
|
|
29474
|
-
const idx = pool.indexOf(_diffCenterColormap);
|
|
29619
|
+
const idx = pool.indexOf(_diffCenterColormap || _diffDefaultColormap());
|
|
29475
29620
|
_diffCenterColormap = pool[(idx <= 0 ? pool.length - 1 : idx - 1)];
|
|
29476
29621
|
_diffCenterColormapStops = COLORMAP_GRADIENT_STOPS[_diffCenterColormap] || null;
|
|
29477
29622
|
fetchAndDrawDiff();
|
|
@@ -29541,7 +29686,7 @@
|
|
|
29541
29686
|
if ((_cmapStrip && _cmapStrip.classList.contains('visible')) && !qmriActive && !compareQmriActive && !rgbMode) {
|
|
29542
29687
|
if (_cmapStripMode === 'diff-center') {
|
|
29543
29688
|
const pool = (diffMode === 1) ? DIFF_COLORMAPS : ABS_DIFF_COLORMAPS;
|
|
29544
|
-
const idx = pool.indexOf(_diffCenterColormap);
|
|
29689
|
+
const idx = pool.indexOf(_diffCenterColormap || _diffDefaultColormap());
|
|
29545
29690
|
_diffCenterColormap = pool[(idx + 1) % pool.length];
|
|
29546
29691
|
_diffCenterColormapStops = COLORMAP_GRADIENT_STOPS[_diffCenterColormap] || null;
|
|
29547
29692
|
fetchAndDrawDiff();
|
|
@@ -37225,6 +37370,96 @@
|
|
|
37225
37370
|
|
|
37226
37371
|
// --------------- Compare + qMRI combined mode ---------------
|
|
37227
37372
|
// Layout: rows = compared arrays, columns = parameter maps
|
|
37373
|
+
// Each map column shares one display range across the compared arrays:
|
|
37374
|
+
// only the bottom row shows colorbars, so that one bar must be true
|
|
37375
|
+
// for every pane above it.
|
|
37376
|
+
|
|
37377
|
+
function _cqColumnViews(view) {
|
|
37378
|
+
return compareQmriViews.filter(v => v.qmriIdx === view.qmriIdx);
|
|
37379
|
+
}
|
|
37380
|
+
|
|
37381
|
+
/** The bottom-row pane of a column — the one whose colorbar is shown. */
|
|
37382
|
+
function _cqColumnLead(view) {
|
|
37383
|
+
const col = _cqColumnViews(view);
|
|
37384
|
+
return col.length ? col[col.length - 1] : view;
|
|
37385
|
+
}
|
|
37386
|
+
|
|
37387
|
+
/** Store a display window on a qMRI pane; in compare-qMRI on its whole column. */
|
|
37388
|
+
function _qmriStoreWindow(view, lo, hi) {
|
|
37389
|
+
const ownerViews = compareQmriActive ? compareQmriViews : qmriViews;
|
|
37390
|
+
const targets = compareQmriActive && ownerViews.includes(view) ? _cqColumnViews(view) : [view];
|
|
37391
|
+
targets.forEach(t => {
|
|
37392
|
+
t.lockedVmin = lo;
|
|
37393
|
+
t.lockedVmax = hi;
|
|
37394
|
+
const idx = ownerViews.indexOf(t);
|
|
37395
|
+
const qv = idx >= 0 ? modeManager.currentViews[idx] : null;
|
|
37396
|
+
if (qv && qv.displayState) { qv.displayState.vmin = lo; qv.displayState.vmax = hi; }
|
|
37397
|
+
});
|
|
37398
|
+
}
|
|
37399
|
+
|
|
37400
|
+
function _qmriRenderWindowOwners(view) {
|
|
37401
|
+
if (compareQmriActive && compareQmriViews.includes(view)) {
|
|
37402
|
+
_cqColumnViews(view).forEach(v => {
|
|
37403
|
+
compareQmriRender(v);
|
|
37404
|
+
if (v._colorBar) { v._colorBar.draw(); v._colorBar.updateLabels(); }
|
|
37405
|
+
});
|
|
37406
|
+
} else {
|
|
37407
|
+
qvRender(view);
|
|
37408
|
+
}
|
|
37409
|
+
}
|
|
37410
|
+
|
|
37411
|
+
/** First frame of a pane: once every pane in its column has reported
|
|
37412
|
+
* its own range, lock the column to the union so all arrays share it. */
|
|
37413
|
+
function _cqNoteFrameRange(v, lo, hi) {
|
|
37414
|
+
if (v.lockedVmin != null) return;
|
|
37415
|
+
v._cqNaturalRange = { vmin: lo, vmax: hi };
|
|
37416
|
+
const col = _cqColumnViews(v);
|
|
37417
|
+
if (!col.every(c => c._cqNaturalRange)) return;
|
|
37418
|
+
const win = _normalizeQmriWindowPolicy(v, {
|
|
37419
|
+
vmin: Math.min(...col.map(c => c._cqNaturalRange.vmin)),
|
|
37420
|
+
vmax: Math.max(...col.map(c => c._cqNaturalRange.vmax)),
|
|
37421
|
+
});
|
|
37422
|
+
if (!win) return;
|
|
37423
|
+
_qmriStoreWindow(v, win.vmin, win.vmax);
|
|
37424
|
+
col.forEach(c => compareQmriRender(c));
|
|
37425
|
+
}
|
|
37426
|
+
|
|
37427
|
+
function _cqHistogramOpts(view, sidVal) {
|
|
37428
|
+
return {
|
|
37429
|
+
sid: sidVal,
|
|
37430
|
+
dimX: dim_x, dimY: dim_y,
|
|
37431
|
+
scrollDim: current_slice_dim,
|
|
37432
|
+
complexMode: complexMode,
|
|
37433
|
+
fixedIndices: compareQmriDim + ':' + view.qmriIdx,
|
|
37434
|
+
qmriRole: view.qmriRole,
|
|
37435
|
+
bins: 128,
|
|
37436
|
+
};
|
|
37437
|
+
}
|
|
37438
|
+
|
|
37439
|
+
/** One histogram of a map across every compared array, re-binned
|
|
37440
|
+
* onto a common range so presets and the shown bar cover them all. */
|
|
37441
|
+
async function _cqFetchColumnHistogram(view) {
|
|
37442
|
+
const sids = _cqColumnViews(view).map(v => v.sid);
|
|
37443
|
+
const hists = (await Promise.all(sids.map(s => _fetchVolumeHistogram(_cqHistogramOpts(view, s)))))
|
|
37444
|
+
.filter(h => h && h.counts && h.counts.length && h.edges && h.edges.length === h.counts.length + 1);
|
|
37445
|
+
if (!hists.length) return null;
|
|
37446
|
+
if (hists.length === 1) return hists[0];
|
|
37447
|
+
const lo = Math.min(...hists.map(h => Number(h.vmin)));
|
|
37448
|
+
const hi = Math.max(...hists.map(h => Number(h.vmax)));
|
|
37449
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi) || hi <= lo) return hists[0];
|
|
37450
|
+
const bins = 128;
|
|
37451
|
+
const step = (hi - lo) / bins;
|
|
37452
|
+
const counts = new Array(bins).fill(0);
|
|
37453
|
+
for (const h of hists) {
|
|
37454
|
+
for (let i = 0; i < h.counts.length; i++) {
|
|
37455
|
+
const c = (Number(h.edges[i]) + Number(h.edges[i + 1])) / 2;
|
|
37456
|
+
const b = Math.max(0, Math.min(bins - 1, Math.floor((c - lo) / step)));
|
|
37457
|
+
counts[b] += h.counts[i];
|
|
37458
|
+
}
|
|
37459
|
+
}
|
|
37460
|
+
const edges = Array.from({ length: bins + 1 }, (_, i) => lo + i * step);
|
|
37461
|
+
return { counts, edges, vmin: lo, vmax: hi };
|
|
37462
|
+
}
|
|
37228
37463
|
function enterCompareQmri() {
|
|
37229
37464
|
if (!compareActive) return;
|
|
37230
37465
|
_qmriRenderGeneration++;
|
|
@@ -37327,35 +37562,19 @@
|
|
|
37327
37562
|
getStops: () => COLORMAP_GRADIENT_STOPS[view.colormap] || COLORMAP_GRADIENT_STOPS['gray'] || [],
|
|
37328
37563
|
getRange: () => ({ vmin: view.vmin, vmax: view.vmax }),
|
|
37329
37564
|
getWindow: () => ({ vmin: view.lockedVmin ?? view.vmin, vmax: view.lockedVmax ?? view.vmax }),
|
|
37330
|
-
setWindow: (lo, hi) => { view
|
|
37565
|
+
setWindow: (lo, hi) => { _qmriStoreWindow(view, lo, hi); },
|
|
37331
37566
|
fetchHistogram: async () => {
|
|
37332
|
-
const
|
|
37333
|
-
fixedPairs.push(compareQmriDim + ':' + view.qmriIdx);
|
|
37334
|
-
const fixedIndices = fixedPairs.join(',');
|
|
37335
|
-
const hist = await _fetchVolumeHistogram({
|
|
37336
|
-
dimX: dim_x, dimY: dim_y,
|
|
37337
|
-
scrollDim: current_slice_dim,
|
|
37338
|
-
complexMode: complexMode,
|
|
37339
|
-
fixedIndices,
|
|
37340
|
-
qmriRole: view.qmriRole,
|
|
37341
|
-
bins: 128,
|
|
37342
|
-
});
|
|
37567
|
+
const hist = await _cqFetchColumnHistogram(view);
|
|
37343
37568
|
return _normalizeQmriHistogramForColorbar(view, hist);
|
|
37344
37569
|
},
|
|
37345
|
-
getHistogramVersion: () =>
|
|
37346
|
-
|
|
37347
|
-
scrollDim: current_slice_dim,
|
|
37348
|
-
complexMode: complexMode,
|
|
37349
|
-
fixedIndices: compareQmriDim + ':' + view.qmriIdx,
|
|
37350
|
-
qmriRole: view.qmriRole,
|
|
37351
|
-
bins: 128,
|
|
37352
|
-
}),
|
|
37570
|
+
getHistogramVersion: () => _cqColumnViews(view)
|
|
37571
|
+
.map(v => _volumeHistCacheKey(_cqHistogramOpts(view, v.sid))).join(';'),
|
|
37353
37572
|
onMouseEnter: () => { _startColorbarLebesgueHover(); },
|
|
37354
37573
|
onMouseLeave: () => { _stopColorbarLebesgueHover(); },
|
|
37355
37574
|
onDragStart: () => { _stopColorbarLebesgueHover(); },
|
|
37356
37575
|
onDragEnd: () => { if (cqCb && cqCb._mouseOver) _startColorbarLebesgueHover(); },
|
|
37357
37576
|
onBinHover: (binIdx, frac) => { _drawLebesgueHighlightForColorbar(_cqCb, binIdx); },
|
|
37358
|
-
onWindowChange: () => {
|
|
37577
|
+
onWindowChange: () => { _cqColumnViews(view).forEach(v => compareQmriRender(v)); },
|
|
37359
37578
|
onAnimFrame: () => { compareQmriScaleAllCanvases(); },
|
|
37360
37579
|
});
|
|
37361
37580
|
_cqCb._lebesgueView = view;
|
|
@@ -37521,6 +37740,8 @@
|
|
|
37521
37740
|
canvas_h: _qch,
|
|
37522
37741
|
projection_mode: projectionMode,
|
|
37523
37742
|
projection_dim: projectionMode > 0 ? projectionDim : -1,
|
|
37743
|
+
vmin_override: v.lockedVmin != null && v.lockedVmax != null ? v.lockedVmin : undefined,
|
|
37744
|
+
vmax_override: v.lockedVmin != null && v.lockedVmax != null ? v.lockedVmax : undefined,
|
|
37524
37745
|
perf: _PERF_ENABLED,
|
|
37525
37746
|
}));
|
|
37526
37747
|
}
|
|
@@ -37560,10 +37781,7 @@
|
|
|
37560
37781
|
const frameWindow = _normalizeQmriWindowPolicy(v, { vmin: frame.vmin, vmax: frame.vmax }) || frame;
|
|
37561
37782
|
v.vmin = frameWindow.vmin;
|
|
37562
37783
|
v.vmax = frameWindow.vmax;
|
|
37563
|
-
|
|
37564
|
-
v.lockedVmin = v.vmin;
|
|
37565
|
-
v.lockedVmax = v.vmax;
|
|
37566
|
-
}
|
|
37784
|
+
_cqNoteFrameRange(v, v.vmin, v.vmax);
|
|
37567
37785
|
v.lastImageData = frame.imageData;
|
|
37568
37786
|
v.lastW = frame.width;
|
|
37569
37787
|
v.lastH = frame.height;
|
|
@@ -37675,7 +37893,9 @@
|
|
|
37675
37893
|
const width = h32[1], height = h32[2];
|
|
37676
37894
|
if (h32[0] === v.seq && compareQmriActive && v._qmriGeneration === _qmriRenderGeneration && compareQmriViews.includes(v)) {
|
|
37677
37895
|
const f32 = new Float32Array(buf, 12, 2);
|
|
37678
|
-
v
|
|
37896
|
+
const frameWindow = _normalizeQmriWindowPolicy(v, { vmin: f32[0], vmax: f32[1] }) || { vmin: f32[0], vmax: f32[1] };
|
|
37897
|
+
v.vmin = frameWindow.vmin; v.vmax = frameWindow.vmax;
|
|
37898
|
+
_cqNoteFrameRange(v, v.vmin, v.vmax);
|
|
37679
37899
|
const rgba = new Uint8ClampedArray(buf.slice(20, 20 + width * height * 4));
|
|
37680
37900
|
const drawT0 = _perfNow();
|
|
37681
37901
|
const frame = _applyAutoCropIfReady(
|
|
@@ -42807,7 +43027,7 @@
|
|
|
42807
43027
|
preview: _ensureCmapPreviewEl(host),
|
|
42808
43028
|
pool,
|
|
42809
43029
|
currentIdx: () => {
|
|
42810
|
-
const idx = pool.indexOf(_diffCenterColormap);
|
|
43030
|
+
const idx = pool.indexOf(_diffCenterColormap || _diffDefaultColormap());
|
|
42811
43031
|
return idx >= 0 ? idx : 0;
|
|
42812
43032
|
},
|
|
42813
43033
|
captureState: () => ({
|
|
@@ -7,7 +7,7 @@ arrayview/_bootstrap_app.py,sha256=ljiZ0o5D3nkJqEGcyyVF06dFWxXujBufQjbcn50QzXM,4
|
|
|
7
7
|
arrayview/_config.py,sha256=5ovQTO2I-j3fxCxEKcZKE2pqiahLESGTGS24nQdP61I,13493
|
|
8
8
|
arrayview/_diagnostics.py,sha256=XB0rCoDn3cpfhz4H-ayLktOgCMIoVJ0wM9N0ep_kKkQ,11018
|
|
9
9
|
arrayview/_dicom.py,sha256=tNY353MNWTvcXavHeIkqj3242dbgfH_ctwcx9NDee-E,19607
|
|
10
|
-
arrayview/_diff.py,sha256=
|
|
10
|
+
arrayview/_diff.py,sha256=mWLyRSJ6cy-hngyhINxNM88P1QsXXhpMFMFO0PBxidQ,7374
|
|
11
11
|
arrayview/_extra_ports.py,sha256=MZcx04NI9Eg4coydb-yPIHp7uJbE6UscyYGnw8fxid0,11125
|
|
12
12
|
arrayview/_icon.png,sha256=a8RTAbvweLhh3hzD5eGg76Peq-KSDcS6YrCphRb_HEs,2748
|
|
13
13
|
arrayview/_imaging.py,sha256=MQX1uy4m1lofOeoLDhCp6cHO5p_V5fXm4qlSu9Ykn-c,534
|
|
@@ -19,7 +19,7 @@ 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=PwbDVzaVNCqJQw2AGuXS5TG5is5lq9N4yBJt3Ge40MI,40220
|
|
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
|
|
@@ -28,14 +28,14 @@ arrayview/_routes_persistence.py,sha256=T03csHb7ppGT-8yMDf227XkqhnhjgSLYj0RW2wmg
|
|
|
28
28
|
arrayview/_routes_preferences.py,sha256=xoMHbPKliYspgjaclhx2r8HbP9mFo9Td-nTF76eWVMg,4680
|
|
29
29
|
arrayview/_routes_preload.py,sha256=KOsankNguxdIWmFW9iZcY9p2SvUN1mOKY7KkW1qMGcc,1729
|
|
30
30
|
arrayview/_routes_query.py,sha256=HEEmMKuFycUTBNVUSWtUFXCnWB7ZrM3hv9IZWbXz11o,25012
|
|
31
|
-
arrayview/_routes_rendering.py,sha256=
|
|
31
|
+
arrayview/_routes_rendering.py,sha256=5373qwaNYVedPnBHNo0b2PXBGIiVx37SqEcMCksFNJo,24874
|
|
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=405t-7gSERkNlO2pYarkWNzz3Y7CsZ_8CwCU8emXTWU,45541
|
|
36
36
|
arrayview/_segmentation.py,sha256=aENWHmefrKDScXhcZL1X3KgYXE784dpMkmFFo83qnrw,7377
|
|
37
37
|
arrayview/_server.py,sha256=Ejh-6rWpWo0luhBhtsVq1Z4RFIyOPVv5nzExHtXb7iA,23042
|
|
38
|
-
arrayview/_session.py,sha256=
|
|
38
|
+
arrayview/_session.py,sha256=BX94Zx3J4YsqR0t5mr4SJrsVGG5pf_UJK4Ij45iubPc,42254
|
|
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,7 +43,7 @@ arrayview/_synthetic_mri.py,sha256=KcSYC26dgjBH25bUoqHT0IgJIzOxgXCSLkWr-ZnFq08,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=LhC5-SnDwxyA20T8AwKE2MWtbvKSOrPi1-kO4e_ywdI,2414456
|
|
47
47
|
arrayview/_vscode.py,sha256=EKnGN2zrjlybc4FQOmYA8BvYdtkcJbLHDEdgtvO7ul4,837
|
|
48
48
|
arrayview/_vscode_browser.py,sha256=VCH9JpAAjFuhO0wQe8tIGjAL5837AXg_bpmBFtWOQqc,20573
|
|
49
49
|
arrayview/_vscode_extension.py,sha256=eYsWAQmik2tt7HMWad9vBnn2kpRyRpLrh42FBGQy0M4,53156
|
|
@@ -52,8 +52,8 @@ arrayview/_watch.py,sha256=PxxS_5hcj_S6xQf9Ep4p1HLRkrI5iDMh1aRa4olV7ew,1873
|
|
|
52
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.48.
|
|
56
|
-
arrayview-0.48.
|
|
57
|
-
arrayview-0.48.
|
|
58
|
-
arrayview-0.48.
|
|
59
|
-
arrayview-0.48.
|
|
55
|
+
arrayview-0.48.4.dist-info/METADATA,sha256=PLSqbJ2d6fFpvAkOLc3Vw8xxcq7F4yuXohp11HXsK9I,2288
|
|
56
|
+
arrayview-0.48.4.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
57
|
+
arrayview-0.48.4.dist-info/entry_points.txt,sha256=-0s08axutXkTbINiKs0Q7iv-33aWuTaamUg2q2-NyQ4,60
|
|
58
|
+
arrayview-0.48.4.dist-info/licenses/LICENSE,sha256=34OivjM9faB2QaPjZpMRTIiSx31V3U9cEyQ6F2k-bH0,1062
|
|
59
|
+
arrayview-0.48.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|