arrayview 0.46.2__py3-none-any.whl → 0.47.1__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 +41 -0
- arrayview/_launcher.py +6 -0
- arrayview/_routes_loading.py +15 -0
- arrayview/_session.py +6 -0
- arrayview/_viewer.html +287 -74
- arrayview/_vscode_extension.py +1 -1
- arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.46.2.dist-info → arrayview-0.47.1.dist-info}/METADATA +1 -1
- {arrayview-0.46.2.dist-info → arrayview-0.47.1.dist-info}/RECORD +12 -12
- {arrayview-0.46.2.dist-info → arrayview-0.47.1.dist-info}/WHEEL +0 -0
- {arrayview-0.46.2.dist-info → arrayview-0.47.1.dist-info}/entry_points.txt +0 -0
- {arrayview-0.46.2.dist-info → arrayview-0.47.1.dist-info}/licenses/LICENSE +0 -0
arrayview/_io.py
CHANGED
|
@@ -1863,3 +1863,44 @@ def _peek_file_shape(fpath: str, ext: str):
|
|
|
1863
1863
|
except Exception:
|
|
1864
1864
|
pass
|
|
1865
1865
|
return None
|
|
1866
|
+
|
|
1867
|
+
|
|
1868
|
+
def nifti_grid_meta(filepath):
|
|
1869
|
+
"""Return a NIfTI file's native-order spatial metadata (header only), or None."""
|
|
1870
|
+
if not (filepath.endswith(".nii") or filepath.endswith(".nii.gz")):
|
|
1871
|
+
return None
|
|
1872
|
+
return _nifti_header_with_meta(filepath, native=True)[1]
|
|
1873
|
+
|
|
1874
|
+
|
|
1875
|
+
def align_overlay_to_base(ov_path, ov_data, base_meta):
|
|
1876
|
+
"""Resample a NIfTI overlay onto the base volume's voxel grid.
|
|
1877
|
+
|
|
1878
|
+
Only used when the overlay's spatial shape differs from the base, which
|
|
1879
|
+
otherwise leaves the overlay undisplayable. Both files' affines place the
|
|
1880
|
+
voxels in world space; label data uses nearest-neighbour so mask values
|
|
1881
|
+
stay exact. Returns ``ov_data`` unchanged when alignment does not apply.
|
|
1882
|
+
"""
|
|
1883
|
+
if not base_meta or not (ov_path.endswith(".nii") or ov_path.endswith(".nii.gz")):
|
|
1884
|
+
return ov_data
|
|
1885
|
+
base_shape = base_meta.get("spatial_shape")
|
|
1886
|
+
base_affine = base_meta.get("data_affine")
|
|
1887
|
+
if base_shape is None or base_affine is None:
|
|
1888
|
+
return ov_data
|
|
1889
|
+
ov_shape = tuple(int(v) for v in ov_data.shape)
|
|
1890
|
+
if len(ov_shape) != 3 or ov_shape == tuple(base_shape):
|
|
1891
|
+
return ov_data
|
|
1892
|
+
from scipy.ndimage import affine_transform
|
|
1893
|
+
|
|
1894
|
+
ov_affine = np.asarray(_nib().load(ov_path).affine, dtype=np.float64)
|
|
1895
|
+
base_to_ov = np.linalg.inv(ov_affine) @ np.asarray(base_affine, dtype=np.float64)
|
|
1896
|
+
src = np.asarray(ov_data)
|
|
1897
|
+
is_label = np.issubdtype(src.dtype, np.integer) or src.dtype == bool
|
|
1898
|
+
return affine_transform(
|
|
1899
|
+
src.astype(np.float32) if not is_label else src,
|
|
1900
|
+
base_to_ov[:3, :3],
|
|
1901
|
+
offset=base_to_ov[:3, 3],
|
|
1902
|
+
output_shape=tuple(base_shape),
|
|
1903
|
+
order=0 if is_label else 1,
|
|
1904
|
+
mode="constant",
|
|
1905
|
+
cval=0,
|
|
1906
|
+
)
|
arrayview/_launcher.py
CHANGED
|
@@ -1314,6 +1314,7 @@ def _load_session_from_filepath(
|
|
|
1314
1314
|
watch: bool = False,
|
|
1315
1315
|
initial_view: dict | None = None,
|
|
1316
1316
|
code_source: dict | None = None,
|
|
1317
|
+
align_to: str | None = None,
|
|
1317
1318
|
) -> dict:
|
|
1318
1319
|
requested_sid = uuid.uuid4().hex
|
|
1319
1320
|
payload = {
|
|
@@ -1329,6 +1330,8 @@ def _load_session_from_filepath(
|
|
|
1329
1330
|
}
|
|
1330
1331
|
if source_staging_dir:
|
|
1331
1332
|
payload["source_staging_dir"] = source_staging_dir
|
|
1333
|
+
if align_to:
|
|
1334
|
+
payload["align_to"] = align_to
|
|
1332
1335
|
if watch:
|
|
1333
1336
|
payload["watch"] = True
|
|
1334
1337
|
if initial_view:
|
|
@@ -1686,6 +1689,7 @@ def _register_cli_session_with_existing_server_impl(
|
|
|
1686
1689
|
ov_name,
|
|
1687
1690
|
expected_server_id=expected_server_id,
|
|
1688
1691
|
source_staging_dir=(source_staging_dirs or {}).get(ov_path),
|
|
1692
|
+
align_to=os.path.abspath(base_file),
|
|
1689
1693
|
)
|
|
1690
1694
|
if "error" in ov_result:
|
|
1691
1695
|
raise RuntimeError(
|
|
@@ -4803,6 +4807,7 @@ def _serve_daemon(
|
|
|
4803
4807
|
array_choice_placeholder,
|
|
4804
4808
|
load_data,
|
|
4805
4809
|
load_data_with_meta,
|
|
4810
|
+
align_overlay_to_base,
|
|
4806
4811
|
load_dir_collection,
|
|
4807
4812
|
list_array_keys,
|
|
4808
4813
|
)
|
|
@@ -4928,6 +4933,7 @@ def _serve_daemon(
|
|
|
4928
4933
|
):
|
|
4929
4934
|
try:
|
|
4930
4935
|
ov_data = load_data(ov_path)
|
|
4936
|
+
ov_data = align_overlay_to_base(ov_path, ov_data, spatial_meta)
|
|
4931
4937
|
ov_session = _session_mod.Session(
|
|
4932
4938
|
ov_data, filepath=ov_path, name=ov_name
|
|
4933
4939
|
)
|
arrayview/_routes_loading.py
CHANGED
|
@@ -96,10 +96,12 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
|
|
|
96
96
|
result = []
|
|
97
97
|
for session in SESSIONS.values():
|
|
98
98
|
dtype_str = str(getattr(session.data, "dtype", "unknown"))
|
|
99
|
+
code = getattr(session, "code_source", None) or {}
|
|
99
100
|
result.append(
|
|
100
101
|
{
|
|
101
102
|
"sid": session.sid,
|
|
102
103
|
"name": session.name,
|
|
104
|
+
"var": code.get("name") or None,
|
|
103
105
|
"shape": [int(x) for x in session.shape],
|
|
104
106
|
"filepath": session.filepath,
|
|
105
107
|
"dtype": dtype_str,
|
|
@@ -275,6 +277,7 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
|
|
|
275
277
|
and os.path.abspath(existing.filepath) == abs_path
|
|
276
278
|
and current_signature is not None
|
|
277
279
|
and getattr(existing, "file_signature", None) == current_signature
|
|
280
|
+
and getattr(existing, "aligned_to", None) == body.get("align_to")
|
|
278
281
|
):
|
|
279
282
|
if acquire_session_leases([existing.sid]):
|
|
280
283
|
return {"sid": existing.sid, "name": existing.name, "notified": False}
|
|
@@ -456,6 +459,17 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
|
|
|
456
459
|
key=_key,
|
|
457
460
|
select=body.get("select"),
|
|
458
461
|
)
|
|
462
|
+
if body.get("align_to"):
|
|
463
|
+
from ._io import align_overlay_to_base, nifti_grid_meta
|
|
464
|
+
|
|
465
|
+
base_meta = await asyncio.to_thread(
|
|
466
|
+
nifti_grid_meta, str(body["align_to"])
|
|
467
|
+
)
|
|
468
|
+
aligned = await asyncio.to_thread(
|
|
469
|
+
align_overlay_to_base, filepath, data, base_meta
|
|
470
|
+
)
|
|
471
|
+
if aligned is not data:
|
|
472
|
+
data, spatial_meta = aligned, base_meta
|
|
459
473
|
except Exception as e:
|
|
460
474
|
if staging_dir:
|
|
461
475
|
cleanup_staging_directory(staging_dir)
|
|
@@ -477,6 +491,7 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
|
|
|
477
491
|
str(value) for value in body.get("related_sids", [])
|
|
478
492
|
]
|
|
479
493
|
apply_launch_view(session, body)
|
|
494
|
+
session.aligned_to = body.get("align_to")
|
|
480
495
|
if staging_dir:
|
|
481
496
|
session._source_staging_dirs = [str(staging_dir)]
|
|
482
497
|
if not dir_patterns:
|
arrayview/_session.py
CHANGED
|
@@ -765,6 +765,12 @@ def upgrade_memmap_in_background(session) -> bool:
|
|
|
765
765
|
from ._io import _nifti_display_array
|
|
766
766
|
|
|
767
767
|
loaded = _nifti_display_array(data)
|
|
768
|
+
# An uncompressed proxy whose dtype needs no conversion comes
|
|
769
|
+
# back as a view of the file map, so nothing was read yet and
|
|
770
|
+
# every new orientation would still fault from disk. Copy in
|
|
771
|
+
# file order to make it one sequential read.
|
|
772
|
+
if not loaded.flags.owndata:
|
|
773
|
+
loaded = np.array(loaded, copy=True, order="K")
|
|
768
774
|
except Exception:
|
|
769
775
|
return # keep serving from the lazy source
|
|
770
776
|
# A /reload or a released session must not be clobbered by a read that
|
arrayview/_viewer.html
CHANGED
|
@@ -236,22 +236,24 @@
|
|
|
236
236
|
border-radius: 10px;
|
|
237
237
|
overflow: hidden;
|
|
238
238
|
}
|
|
239
|
-
/* Orientation
|
|
240
|
-
small
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
239
|
+
/* Orientation cross — hovering the DimBar (or pressing r / t) shows a
|
|
240
|
+
small axis cross in the origin corner of every image pane, one arrow
|
|
241
|
+
per in-plane direction with its letter. See _showOrientEdgeHighlight().
|
|
242
|
+
Yellow with a black outline so it reads on bright and dark image
|
|
243
|
+
content alike. */
|
|
244
|
+
.orient-cross {
|
|
245
|
+
position: fixed; left: 0; top: 0; width: 64px; height: 64px;
|
|
246
|
+
transform-origin: 0 0; pointer-events: none; z-index: 60;
|
|
245
247
|
opacity: 0; transition: opacity 0.15s ease;
|
|
246
248
|
}
|
|
247
|
-
.orient-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
stroke:
|
|
249
|
+
.orient-cross.showing { opacity: 1; }
|
|
250
|
+
.orient-cross .oc-halo { stroke: rgba(0, 0, 0, 0.7); stroke-width: 4px; fill: none; stroke-linecap: round; }
|
|
251
|
+
.orient-cross .oc-ln { stroke: var(--active-dim); stroke-width: 1.5px; fill: none; stroke-linecap: round; }
|
|
252
|
+
.orient-cross .oc-hd { fill: var(--active-dim); stroke: rgba(0, 0, 0, 0.7); stroke-width: 2.5px; stroke-linejoin: round; paint-order: stroke; }
|
|
253
|
+
.orient-cross text {
|
|
254
|
+
font-family: 'SF Mono', ui-monospace, 'Cascadia Code', 'JetBrains Mono', monospace;
|
|
255
|
+
font-size: 12px; font-weight: 600; fill: var(--active-dim);
|
|
256
|
+
stroke: rgba(0, 0, 0, 0.7); stroke-width: 3px; stroke-linejoin: round; paint-order: stroke;
|
|
255
257
|
}
|
|
256
258
|
#slim-cb-wrap {
|
|
257
259
|
text-align: center;
|
|
@@ -2087,6 +2089,18 @@
|
|
|
2087
2089
|
.drop-action:hover:not(:disabled) { color: var(--active-dim); background: var(--surface-2); }
|
|
2088
2090
|
.drop-action:disabled { color: var(--muted); cursor: default; opacity: 0.5; }
|
|
2089
2091
|
.drop-action-reason { display: block; margin-top: 2px; font-size: 9px; color: var(--muted); }
|
|
2092
|
+
#compare-with-menu {
|
|
2093
|
+
display: none; position: fixed; z-index: 70; left: 50%; top: 50%;
|
|
2094
|
+
transform: translate(-50%, -50%); width: min(340px, calc(100vw - 24px));
|
|
2095
|
+
max-height: calc(100vh - 48px); overflow-y: auto;
|
|
2096
|
+
padding: 8px; background: var(--surface); border: 1px solid var(--border);
|
|
2097
|
+
border-radius: var(--radius-lg); color: var(--text); font: 11px var(--mono);
|
|
2098
|
+
backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
|
|
2099
|
+
}
|
|
2100
|
+
#compare-with-menu.visible { display: block; }
|
|
2101
|
+
#compare-with-title { padding: 4px 6px 2px; }
|
|
2102
|
+
#compare-with-summary { color: var(--muted); padding: 0 6px 7px; font-size: 10px; }
|
|
2103
|
+
.compare-with-key { color: var(--muted); margin-right: 8px; }
|
|
2090
2104
|
.mode-badge {
|
|
2091
2105
|
display: inline-block; font-size: 0.62em; font-weight: 600;
|
|
2092
2106
|
letter-spacing: 0.08em; padding: 2px 6px; border-radius: 4px;
|
|
@@ -4247,6 +4261,13 @@
|
|
|
4247
4261
|
/* qMRI 3-plane multiview: one map per row (3 planes) + a vertical colorbar on the right */
|
|
4248
4262
|
.qmri-mv-row { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 14px; }
|
|
4249
4263
|
.qmri-mv-row .qv-canvas-clip { align-items: center; justify-content: center; }
|
|
4264
|
+
/* qMRI ortho: the 3x3 panes touch and read as one block, so only the
|
|
4265
|
+
outer corners of the block are rounded. */
|
|
4266
|
+
body.rounded-panes .qmri-mv-row .qv-canvas-clip { border-radius: 0; }
|
|
4267
|
+
body.rounded-panes .qmri-mv-row .qv-canvas-clip.qmv-tl { border-top-left-radius: 10px; }
|
|
4268
|
+
body.rounded-panes .qmri-mv-row .qv-canvas-clip.qmv-tr { border-top-right-radius: 10px; }
|
|
4269
|
+
body.rounded-panes .qmri-mv-row .qv-canvas-clip.qmv-bl { border-bottom-left-radius: 10px; }
|
|
4270
|
+
body.rounded-panes .qmri-mv-row .qv-canvas-clip.qmv-br { border-bottom-right-radius: 10px; }
|
|
4250
4271
|
.qmri-mv-row > .qv-row { flex: 0 1 auto; }
|
|
4251
4272
|
.qmri-mv-cb { display: flex; align-items: center; flex-shrink: 0; }
|
|
4252
4273
|
.qmri-mv-vcb-island { padding: 4px 8px; margin-top: 0; flex-direction: column !important; gap: 3px; }
|
|
@@ -5741,6 +5762,11 @@
|
|
|
5741
5762
|
<div id="drop-action-summary"></div>
|
|
5742
5763
|
<div id="drop-action-list"></div>
|
|
5743
5764
|
</div>
|
|
5765
|
+
<div id="compare-with-menu" aria-hidden="true">
|
|
5766
|
+
<div id="compare-with-title">Compare with…</div>
|
|
5767
|
+
<div id="compare-with-summary"></div>
|
|
5768
|
+
<div id="compare-with-list"></div>
|
|
5769
|
+
</div>
|
|
5744
5770
|
<div id="help-overlay">
|
|
5745
5771
|
<div id="help-box">
|
|
5746
5772
|
<div id="help-tabs"></div>
|
|
@@ -7507,6 +7533,7 @@
|
|
|
7507
7533
|
let _cmvCrosshairAnim = null; // animation frame id for compare-mv crosshair fade
|
|
7508
7534
|
let _qmvCrosshairAlpha = 0; // current crosshair opacity for qMRI multiview
|
|
7509
7535
|
let _qmvCrosshairTarget = 0; // target opacity for qMRI multiview
|
|
7536
|
+
let _qmvCrosshairMap = null; // map row the lines are limited to (click-hold); null = every row
|
|
7510
7537
|
let _qmvCrosshairAnim = null; // animation frame id for qMRI-mv crosshair fade
|
|
7511
7538
|
let _qmvScrollHintTimer = null; // fade-out timer after scrolling a qMRI-mv pane
|
|
7512
7539
|
let _qmvScrollHintDir = -1; // plane being scrolled (its own lines stay hidden)
|
|
@@ -20994,6 +21021,20 @@
|
|
|
20994
21021
|
return `<span class="${cls}">${_dimIndexHtml(shownValue, maxValue)}</span>`;
|
|
20995
21022
|
}
|
|
20996
21023
|
|
|
21024
|
+
// Axis letters follow the data, not the screen: the array axis that
|
|
21025
|
+
// started out as x keeps the letter x after t / r, so the DimBar and the
|
|
21026
|
+
// orientation cross both show which way each axis now runs. Remembered
|
|
21027
|
+
// per pair of displayed dims; picking different dims starts over.
|
|
21028
|
+
let _axisIdent = null;
|
|
21029
|
+
function _axisLetter(d) {
|
|
21030
|
+
const key = Math.min(dim_x, dim_y) + ',' + Math.max(dim_x, dim_y);
|
|
21031
|
+
if (!_axisIdent || _axisIdent.key !== key) {
|
|
21032
|
+
_axisIdent = { key, letters: { [dim_x]: 'x', [dim_y]: 'y' } };
|
|
21033
|
+
}
|
|
21034
|
+
if (d === dim_z) return 'z';
|
|
21035
|
+
return _axisIdent.letters[d] || (d === dim_x ? 'x' : 'y');
|
|
21036
|
+
}
|
|
21037
|
+
|
|
20997
21038
|
function renderInfo() {
|
|
20998
21039
|
_ensureQmriActiveDimVisible();
|
|
20999
21040
|
_syncOverlayPaletteSlice();
|
|
@@ -21046,13 +21087,13 @@
|
|
|
21046
21087
|
}
|
|
21047
21088
|
if (i === dim_x) {
|
|
21048
21089
|
const sign = flip_x ? '<span class="dim-flip-sign">-</span>' : '';
|
|
21049
|
-
const inner = sign +
|
|
21090
|
+
const inner = sign + `<span class="dim-axis-letter">${_axisLetter(i)}</span>` + `<span class="dim-slash">/</span><span class="dim-size" id="dim-x-size">${sizeOf(i)}</span>`;
|
|
21050
21091
|
return (active || playing) ? `<span class="spatial-dim ${dimCls} dim-label" ${d}>${inner}</span>`
|
|
21051
21092
|
: `<span class="spatial-dim dim-label" ${d}>${inner}</span>`;
|
|
21052
21093
|
}
|
|
21053
21094
|
if (i === dim_y) {
|
|
21054
21095
|
const sign = flip_y ? '<span class="dim-flip-sign">-</span>' : '';
|
|
21055
|
-
const inner = sign +
|
|
21096
|
+
const inner = sign + `<span class="dim-axis-letter">${_axisLetter(i)}</span>` + `<span class="dim-slash">/</span><span class="dim-size" id="dim-y-size">${sizeOf(i)}</span>`;
|
|
21056
21097
|
return (active || playing) ? `<span class="spatial-dim ${dimCls} dim-label" ${d}>${inner}</span>`
|
|
21057
21098
|
: `<span class="spatial-dim dim-label" ${d}>${inner}</span>`;
|
|
21058
21099
|
}
|
|
@@ -21272,60 +21313,122 @@
|
|
|
21272
21313
|
if (typeof obliqueVecs !== 'undefined' && obliqueVecs) return false;
|
|
21273
21314
|
return true;
|
|
21274
21315
|
}
|
|
21275
|
-
|
|
21276
|
-
|
|
21277
|
-
|
|
21278
|
-
|
|
21316
|
+
const _ORIENT_CROSS_BOX = 64;
|
|
21317
|
+
const _ORIENT_CROSS_MARGIN = 8;
|
|
21318
|
+
let _orientCrossEls = [];
|
|
21319
|
+
let _orientShown = false;
|
|
21320
|
+
let _orientHovering = false;
|
|
21321
|
+
let _orientRaf = 0;
|
|
21322
|
+
let _orientFlashTimer = 0;
|
|
21323
|
+
// Every image pane in the current mode: its canvas, the letters of its
|
|
21324
|
+
// horizontal / vertical dims, and which way each axis is flipped.
|
|
21325
|
+
function _orientPanes() {
|
|
21326
|
+
const out = [];
|
|
21327
|
+
const add = (cv, lx, ly, fx, fy) => { if (cv) out.push({ cv, lx, ly, fx: !!fx, fy: !!fy }); };
|
|
21328
|
+
const grid = (views, dims) => {
|
|
21329
|
+
const letter = (d) => ['x', 'y', 'z'][Math.max(0, dims.indexOf(d))];
|
|
21330
|
+
(views || []).forEach(v => add(v.canvas, letter(v.dimX), letter(v.dimY), _mvFlipX(v), _mvFlipY(v)));
|
|
21331
|
+
};
|
|
21332
|
+
const plain = (cvs) => cvs.forEach(cv => add(cv, _axisLetter(dim_x), _axisLetter(dim_y), flip_x, flip_y));
|
|
21333
|
+
if (stackMosaicActive) plain((stackMosaicViews || []).map(v => v.canvas));
|
|
21334
|
+
else if (compareQmriActive) plain((compareQmriViews || []).map(v => v.canvas));
|
|
21335
|
+
else if (compareMvActive) grid(compareMvViews, compareMvDims);
|
|
21336
|
+
else if (multiViewActive) grid(mvViews, mvDims);
|
|
21337
|
+
else if (compareActive) plain([...document.querySelectorAll('#compare-panes .compare-canvas')]);
|
|
21338
|
+
else if (qmriMvActive) grid(qmriMvViews, qmriMvDims);
|
|
21339
|
+
else if (qmriActive) plain((qmriViews || []).map(v => v.canvas));
|
|
21340
|
+
else if (mipActive || cutawayActive) return out;
|
|
21341
|
+
// Mosaic tiles are flipped one by one, so the whole canvas has no single origin corner.
|
|
21342
|
+
else if (!(dim_z >= 0 && (flip_x || flip_y))) plain([canvas]);
|
|
21343
|
+
return out;
|
|
21279
21344
|
}
|
|
21280
|
-
|
|
21281
|
-
function
|
|
21282
|
-
|
|
21283
|
-
let
|
|
21284
|
-
|
|
21285
|
-
const
|
|
21286
|
-
|
|
21287
|
-
el.
|
|
21288
|
-
|
|
21289
|
-
|
|
21290
|
-
|
|
21291
|
-
}
|
|
21292
|
-
|
|
21345
|
+
// The part of `cv` that is actually on screen: its box clipped by every scrolling/clipping ancestor.
|
|
21346
|
+
function _orientVisibleRect(cv) {
|
|
21347
|
+
const r = cv.getBoundingClientRect();
|
|
21348
|
+
let l = r.left, t = r.top, rt = r.right, bt = r.bottom;
|
|
21349
|
+
for (let el = cv.parentElement; el && el !== document.body; el = el.parentElement) {
|
|
21350
|
+
const cs = getComputedStyle(el);
|
|
21351
|
+
if (cs.overflowX === 'visible' && cs.overflowY === 'visible') continue;
|
|
21352
|
+
const pr = el.getBoundingClientRect();
|
|
21353
|
+
l = Math.max(l, pr.left); t = Math.max(t, pr.top);
|
|
21354
|
+
rt = Math.min(rt, pr.right); bt = Math.min(bt, pr.bottom);
|
|
21355
|
+
}
|
|
21356
|
+
return (rt - l > 8 && bt - t > 8) ? { left: l, top: t, right: rt, bottom: bt } : null;
|
|
21357
|
+
}
|
|
21358
|
+
function _orientCrossSvg(el, letterX, letterY, flipX, flipY) {
|
|
21359
|
+
const key = letterX + letterY + (flipX ? 1 : 0) + (flipY ? 1 : 0);
|
|
21360
|
+
if (el._ocKey === key) return;
|
|
21361
|
+
el._ocKey = key;
|
|
21362
|
+
const B = _ORIENT_CROSS_BOX, o = 8, len = 30, hl = 7, hw = 3.6;
|
|
21363
|
+
const sx = flipX ? -1 : 1, sy = flipY ? -1 : 1;
|
|
21364
|
+
const ox = flipX ? B - o : o, oy = flipY ? B - o : o;
|
|
21365
|
+
const xTip = ox + sx * len, yTip = oy + sy * len;
|
|
21366
|
+
const xHead = `${xTip},${oy} ${xTip - sx * hl},${oy - hw} ${xTip - sx * hl},${oy + hw}`;
|
|
21367
|
+
const yHead = `${ox},${yTip} ${ox - hw},${yTip - sy * hl} ${ox + hw},${yTip - sy * hl}`;
|
|
21368
|
+
const segs = [[ox, oy, xTip - sx * 4, oy], [ox, oy, ox, yTip - sy * 4]];
|
|
21369
|
+
el.innerHTML = segs.map(([x1, y1, x2, y2]) => `<line class="oc-halo" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}"/>`
|
|
21370
|
+
+ `<line class="oc-ln" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}"/>`).join('')
|
|
21371
|
+
+ `<polygon class="oc-hd" points="${xHead}"/><polygon class="oc-hd" points="${yHead}"/>`
|
|
21372
|
+
+ `<circle class="oc-hd" cx="${ox}" cy="${oy}" r="2"/>`
|
|
21373
|
+
+ `<text x="${xTip + sx * 9}" y="${oy + 4}" text-anchor="${flipX ? 'end' : 'start'}">${letterX}</text>`
|
|
21374
|
+
+ `<text x="${ox}" y="${yTip + sy * 13}" text-anchor="middle">${letterY}</text>`;
|
|
21375
|
+
}
|
|
21376
|
+
// Puts one cross in the origin corner of each pane: the x arrow runs
|
|
21377
|
+
// along the top (bottom when flipped) edge toward increasing x, the y
|
|
21378
|
+
// arrow along the left (right when flipped) edge toward increasing y.
|
|
21379
|
+
function _placeOrientCrosses() {
|
|
21380
|
+
const panes = _orientPanes().map(p => ({ p, r: _orientVisibleRect(p.cv) })).filter(q => q.r);
|
|
21381
|
+
while (_orientCrossEls.length < panes.length) {
|
|
21382
|
+
const el = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
21383
|
+
el.setAttribute('class', 'orient-cross');
|
|
21384
|
+
el.setAttribute('viewBox', `0 0 ${_ORIENT_CROSS_BOX} ${_ORIENT_CROSS_BOX}`);
|
|
21385
|
+
document.body.appendChild(el);
|
|
21386
|
+
_orientCrossEls.push(el);
|
|
21387
|
+
}
|
|
21388
|
+
_orientCrossEls.forEach((el, i) => {
|
|
21389
|
+
const q = panes[i];
|
|
21390
|
+
if (!q) { el.classList.remove('showing'); return; }
|
|
21391
|
+
const { p, r } = q;
|
|
21392
|
+
_orientCrossSvg(el, p.lx, p.ly, p.fx, p.fy);
|
|
21393
|
+
const w = r.right - r.left, h = r.bottom - r.top;
|
|
21394
|
+
// Small panes (grids, mosaics) get a proportionally smaller cross.
|
|
21395
|
+
const k = Math.max(0.55, Math.min(1, Math.min(w, h) / 230));
|
|
21396
|
+
const size = _ORIENT_CROSS_BOX * k, m = _ORIENT_CROSS_MARGIN * k;
|
|
21397
|
+
const x = p.fx ? r.right - m - size : r.left + m;
|
|
21398
|
+
const y = p.fy ? r.bottom - m - size : r.top + m;
|
|
21399
|
+
el.style.transform = `translate(${Math.round(x)}px, ${Math.round(y)}px) scale(${k})`;
|
|
21400
|
+
el.classList.add('showing');
|
|
21401
|
+
});
|
|
21293
21402
|
}
|
|
21294
|
-
//
|
|
21295
|
-
//
|
|
21296
|
-
|
|
21297
|
-
|
|
21298
|
-
|
|
21299
|
-
|
|
21300
|
-
|
|
21301
|
-
if (!el) return;
|
|
21302
|
-
const m = _ORIENT_ARROW_MARGIN + 'px';
|
|
21303
|
-
el.style.left = el.style.right = el.style.top = el.style.bottom = '';
|
|
21304
|
-
if (axis === 'x') {
|
|
21305
|
-
if (flipOwn) el.style.right = m; else el.style.left = m;
|
|
21306
|
-
if (flipOther) el.style.bottom = m; else el.style.top = m;
|
|
21307
|
-
el.style.transform = flipOwn ? 'rotate(180deg)' : 'rotate(0deg)';
|
|
21308
|
-
} else {
|
|
21309
|
-
if (flipOwn) el.style.bottom = m; else el.style.top = m;
|
|
21310
|
-
if (flipOther) el.style.right = m; else el.style.left = m;
|
|
21311
|
-
el.style.transform = flipOwn ? 'rotate(-90deg)' : 'rotate(90deg)';
|
|
21312
|
-
}
|
|
21313
|
-
el.classList.add('showing');
|
|
21403
|
+
// Panes can still be moving or re-rendering when this starts (after
|
|
21404
|
+
// r / t, or while zooming), so keep the crosses glued to them.
|
|
21405
|
+
function _orientLoop() {
|
|
21406
|
+
_orientRaf = 0;
|
|
21407
|
+
if (!_orientShown) return;
|
|
21408
|
+
_placeOrientCrosses();
|
|
21409
|
+
_orientRaf = requestAnimationFrame(_orientLoop);
|
|
21314
21410
|
}
|
|
21315
|
-
function
|
|
21316
|
-
|
|
21317
|
-
if (
|
|
21318
|
-
if (
|
|
21319
|
-
|
|
21320
|
-
|
|
21321
|
-
|
|
21322
|
-
|
|
21323
|
-
|
|
21324
|
-
|
|
21325
|
-
|
|
21326
|
-
|
|
21327
|
-
|
|
21328
|
-
|
|
21411
|
+
function _clearOrientEdgeHighlight() {
|
|
21412
|
+
_orientShown = false;
|
|
21413
|
+
if (_orientRaf) { cancelAnimationFrame(_orientRaf); _orientRaf = 0; }
|
|
21414
|
+
if (_orientFlashTimer) { clearTimeout(_orientFlashTimer); _orientFlashTimer = 0; }
|
|
21415
|
+
_orientCrossEls.forEach(el => el.classList.remove('showing'));
|
|
21416
|
+
}
|
|
21417
|
+
// Shown for every image pane while the pointer is over the DimBar.
|
|
21418
|
+
function _showOrientEdgeHighlight() {
|
|
21419
|
+
if (!_orientEdgeAllowed()) { _clearOrientEdgeHighlight(); return; }
|
|
21420
|
+
_orientShown = true;
|
|
21421
|
+
if (!_orientRaf) _orientLoop();
|
|
21422
|
+
}
|
|
21423
|
+
// Brief look after r / t so a rotate or transpose can be read off the cross.
|
|
21424
|
+
function _flashOrientCross(ms = 1400) {
|
|
21425
|
+
if (!_orientEdgeAllowed()) return;
|
|
21426
|
+
_showOrientEdgeHighlight();
|
|
21427
|
+
if (_orientFlashTimer) clearTimeout(_orientFlashTimer);
|
|
21428
|
+
_orientFlashTimer = setTimeout(() => {
|
|
21429
|
+
_orientFlashTimer = 0;
|
|
21430
|
+
if (!_orientHovering) _clearOrientEdgeHighlight();
|
|
21431
|
+
}, ms);
|
|
21329
21432
|
}
|
|
21330
21433
|
|
|
21331
21434
|
function setStatus(msg) {
|
|
@@ -23317,6 +23420,79 @@
|
|
|
23317
23420
|
}
|
|
23318
23421
|
}, true);
|
|
23319
23422
|
|
|
23423
|
+
// ── Compare with an array that is open in another tab ─────────
|
|
23424
|
+
let _compareWithMenuVisible = false;
|
|
23425
|
+
let _compareWithChoices = [];
|
|
23426
|
+
|
|
23427
|
+
function _closeCompareWithMenu() {
|
|
23428
|
+
if (!_compareWithMenuVisible) return;
|
|
23429
|
+
_compareWithMenuVisible = false;
|
|
23430
|
+
_compareWithChoices = [];
|
|
23431
|
+
_reconcileUI();
|
|
23432
|
+
try { document.getElementById('keyboard-sink').focus(); } catch (_) {}
|
|
23433
|
+
}
|
|
23434
|
+
|
|
23435
|
+
async function _pickCompareWith(choice) {
|
|
23436
|
+
if (!choice || !choice.enabled) return;
|
|
23437
|
+
_closeCompareWithMenu();
|
|
23438
|
+
const ok = await enterCompareModeBySid(choice.sid, choice.label);
|
|
23439
|
+
if (ok !== false) showStatus(`Compare: ${choice.label}`);
|
|
23440
|
+
}
|
|
23441
|
+
|
|
23442
|
+
async function openCompareWithMenu() {
|
|
23443
|
+
let sessions = [];
|
|
23444
|
+
try {
|
|
23445
|
+
const res = await proxyFetch('/sessions');
|
|
23446
|
+
if (res.ok) sessions = await res.json();
|
|
23447
|
+
} catch (_) {}
|
|
23448
|
+
const ownShape = JSON.stringify((shape || []).map(Number));
|
|
23449
|
+
_compareWithChoices = sessions
|
|
23450
|
+
.filter(s => s.sid && s.sid !== sid)
|
|
23451
|
+
.map(s => {
|
|
23452
|
+
const sameShape = JSON.stringify((s.shape || []).map(Number)) === ownShape;
|
|
23453
|
+
const label = s.var || s.name || s.sid.slice(0, 8);
|
|
23454
|
+
if (s.name) compareNameBySid[s.sid] = label;
|
|
23455
|
+
return {
|
|
23456
|
+
sid: s.sid,
|
|
23457
|
+
label,
|
|
23458
|
+
detail: `${(s.shape || []).join(' x ')}${s.var && s.name ? ` · ${s.name}` : ''}`,
|
|
23459
|
+
enabled: sameShape,
|
|
23460
|
+
};
|
|
23461
|
+
});
|
|
23462
|
+
if (!_compareWithChoices.length) {
|
|
23463
|
+
showStatus('No other open arrays to compare with');
|
|
23464
|
+
return;
|
|
23465
|
+
}
|
|
23466
|
+
const list = document.getElementById('compare-with-list');
|
|
23467
|
+
list.innerHTML = _compareWithChoices.map((c, i) => {
|
|
23468
|
+
const keyHint = i < 9 ? `<span class="compare-with-key">${i + 1}</span>` : '';
|
|
23469
|
+
const reason = c.enabled ? c.detail : `${c.detail} · needs shape ${(shape || []).join(' x ')}`;
|
|
23470
|
+
return `<button class="drop-action" data-compare-with="${i}" ${c.enabled ? '' : 'disabled'}>${keyHint}${_escapeHtml(c.label)}<span class="drop-action-reason">${_escapeHtml(reason)}</span></button>`;
|
|
23471
|
+
}).join('');
|
|
23472
|
+
list.querySelectorAll('.drop-action:not(:disabled)').forEach(button => {
|
|
23473
|
+
button.addEventListener('click', () => _pickCompareWith(_compareWithChoices[Number(button.dataset.compareWith)]));
|
|
23474
|
+
});
|
|
23475
|
+
document.getElementById('compare-with-summary').textContent =
|
|
23476
|
+
'Arrays open in your other tabs · click or press a number · Esc to cancel';
|
|
23477
|
+
_compareWithMenuVisible = true;
|
|
23478
|
+
_reconcileUI();
|
|
23479
|
+
}
|
|
23480
|
+
|
|
23481
|
+
document.addEventListener('pointerdown', (event) => {
|
|
23482
|
+
if (_compareWithMenuVisible && !document.getElementById('compare-with-menu').contains(event.target)) {
|
|
23483
|
+
_closeCompareWithMenu();
|
|
23484
|
+
}
|
|
23485
|
+
});
|
|
23486
|
+
window.addEventListener('keydown', (event) => {
|
|
23487
|
+
if (!_compareWithMenuVisible) return;
|
|
23488
|
+
if (['Shift', 'Control', 'Alt', 'Meta'].includes(event.key)) return;
|
|
23489
|
+
event.preventDefault();
|
|
23490
|
+
event.stopImmediatePropagation();
|
|
23491
|
+
if (event.key === 'Escape') { _closeCompareWithMenu(); return; }
|
|
23492
|
+
const n = Number(event.key);
|
|
23493
|
+
if (Number.isInteger(n) && n >= 1 && n <= 9) _pickCompareWith(_compareWithChoices[n - 1]);
|
|
23494
|
+
}, true);
|
|
23495
|
+
|
|
23320
23496
|
/* ── JS: Rendering Pipeline (updateView, play, screenshot) ── */
|
|
23321
23497
|
|
|
23322
23498
|
function updateView() {
|
|
@@ -27757,6 +27933,7 @@
|
|
|
27757
27933
|
_roiClearOnDimChange();
|
|
27758
27934
|
if (typeof _cropReconcileBindings === 'function') _cropReconcileBindings();
|
|
27759
27935
|
renderInfo(); saveState();
|
|
27936
|
+
_flashOrientCross();
|
|
27760
27937
|
return;
|
|
27761
27938
|
}
|
|
27762
27939
|
const wasMosaic = dim_z >= 0;
|
|
@@ -27772,6 +27949,7 @@
|
|
|
27772
27949
|
showStatus(wasMosaic ? 'transposed x/y (mosaic)' : 'transposed x/y');
|
|
27773
27950
|
if (typeof _cropReconcileBindings === 'function') _cropReconcileBindings();
|
|
27774
27951
|
renderInfo(); saveState();
|
|
27952
|
+
_flashOrientCross();
|
|
27775
27953
|
},
|
|
27776
27954
|
},
|
|
27777
27955
|
'view.rotateOrFlip': {
|
|
@@ -27802,6 +27980,7 @@
|
|
|
27802
27980
|
showStatus('rotated 90°'); saveState();
|
|
27803
27981
|
}
|
|
27804
27982
|
if (typeof _cropReconcileBindings === 'function') _cropReconcileBindings();
|
|
27983
|
+
_flashOrientCross();
|
|
27805
27984
|
},
|
|
27806
27985
|
},
|
|
27807
27986
|
'theme.cycle': {
|
|
@@ -29084,6 +29263,11 @@
|
|
|
29084
29263
|
commands['qmri.cycle'].run(ctx, e);
|
|
29085
29264
|
},
|
|
29086
29265
|
},
|
|
29266
|
+
'compare.withOpenArray': {
|
|
29267
|
+
title: 'Compare with an array open in another tab',
|
|
29268
|
+
when: [],
|
|
29269
|
+
run: (ctx, e) => { openCompareWithMenu(); },
|
|
29270
|
+
},
|
|
29087
29271
|
'tool.comparecenterDirect': {
|
|
29088
29272
|
title: 'Cycle compare center mode',
|
|
29089
29273
|
when: [],
|
|
@@ -29515,11 +29699,12 @@
|
|
|
29515
29699
|
},
|
|
29516
29700
|
{
|
|
29517
29701
|
id: 'compare', label: 'Compare',
|
|
29518
|
-
desc: 'View two or more arrays side by side, blend them together, or compute a difference map. Useful for checking registration, comparing model outputs, or inspecting before-and-after changes.
|
|
29702
|
+
desc: 'View two or more arrays side by side, blend them together, or compute a difference map. Useful for checking registration, comparing model outputs, or inspecting before-and-after changes. Press / then c to compare with an array open in another tab.',
|
|
29519
29703
|
sections: [
|
|
29520
29704
|
{
|
|
29521
29705
|
title: 'Layout',
|
|
29522
29706
|
entries: [
|
|
29707
|
+
{ key: '/ c', action: 'Compare with…', hint: 'pick an array that is open in another tab; it must have the same shape' },
|
|
29523
29708
|
{ key: 'g', action: 'Cycle layout', hint: 'horizontal → vertical → grid; with exactly 2 arrays + center mode active, toggles horizontal ↔ big-left' },
|
|
29524
29709
|
{ key: 'Drag title', action: 'Reorder panes by dragging their title bar' },
|
|
29525
29710
|
]
|
|
@@ -29558,6 +29743,7 @@
|
|
|
29558
29743
|
{
|
|
29559
29744
|
title: 'After /', columnBreak: true,
|
|
29560
29745
|
entries: [
|
|
29746
|
+
{ key: '/ c', action: 'Compare with…', hint: 'side by side with an array open in another tab' },
|
|
29561
29747
|
{ key: '/ r', action: 'ROI', hint: 'measure a region and see live stats' },
|
|
29562
29748
|
{ key: '/ S', action: 'Segmentation', hint: 'local mask editing and nnInteractive options' },
|
|
29563
29749
|
{ key: '/ o', action: 'Overlays', hint: 'show mask names and visibility' },
|
|
@@ -29983,6 +30169,16 @@
|
|
|
29983
30169
|
{ key: 'q', toolId: 'qmri', command: 'tool.qmriDirect' },
|
|
29984
30170
|
{ key: 's', toolId: 'save', command: 'saveMenu.open' },
|
|
29985
30171
|
{ key: 'C', toolId: 'crop', command: 'tool.cropDirect' },
|
|
30172
|
+
{
|
|
30173
|
+
key: 'c',
|
|
30174
|
+
id: 'comparewith',
|
|
30175
|
+
label: 'Compare with…',
|
|
30176
|
+
command: 'compare.withOpenArray',
|
|
30177
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="8" height="14" rx="1"/><rect x="13" y="5" width="8" height="14" rx="1"/></svg>',
|
|
30178
|
+
enabled: () => !isInlineEmbed,
|
|
30179
|
+
active: () => compareActive,
|
|
30180
|
+
unavailableReason: () => 'Not available in notebook output.',
|
|
30181
|
+
},
|
|
29986
30182
|
{ key: 'P', toolId: 'comparecenter', command: 'tool.comparecenterDirect' },
|
|
29987
30183
|
{ key: 'r', toolId: 'roi', command: 'tool.roiDirect' },
|
|
29988
30184
|
{ key: 'S', toolId: 'segmentation', command: 'tool.segmentationDirect' },
|
|
@@ -37771,6 +37967,7 @@
|
|
|
37771
37967
|
if (e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return;
|
|
37772
37968
|
sink.focus();
|
|
37773
37969
|
_qmriMvDragView = view;
|
|
37970
|
+
_qmvCrosshairMap = view.qmriIdx;
|
|
37774
37971
|
_qmvRecomputeCrosshairTarget();
|
|
37775
37972
|
handleQmriMvPointer(view, e);
|
|
37776
37973
|
const onMove = (me) => { if (_qmriMvDragView === view) handleQmriMvPointer(view, me); };
|
|
@@ -37923,7 +38120,7 @@
|
|
|
37923
38120
|
_hoveredQmriMvView = null;
|
|
37924
38121
|
if (_qmvCrosshairAnim) { cancelAnimationFrame(_qmvCrosshairAnim); _qmvCrosshairAnim = null; }
|
|
37925
38122
|
if (_qmvScrollHintTimer) { clearTimeout(_qmvScrollHintTimer); _qmvScrollHintTimer = null; }
|
|
37926
|
-
_qmvCrosshairAlpha = 0; _qmvCrosshairTarget = 0; _qmvScrollHintDir = -1;
|
|
38123
|
+
_qmvCrosshairAlpha = 0; _qmvCrosshairTarget = 0; _qmvScrollHintDir = -1; _qmvCrosshairMap = null;
|
|
37927
38124
|
_reconcileOrthoCursorHidden();
|
|
37928
38125
|
_reconcileMvOrientationVisibility();
|
|
37929
38126
|
qmriMvViews.forEach(v => { if (v.ws) { v.ws.onclose = null; v.ws.close(); } });
|
|
@@ -38097,7 +38294,9 @@
|
|
|
38097
38294
|
}
|
|
38098
38295
|
/** Position lines on the pane's overlay, like compareMvDrawFrame. */
|
|
38099
38296
|
function _qmvDrawCrosshair(v) {
|
|
38100
|
-
const alpha = (_qmvScrollHintTimer && v.sliceDir === _qmvScrollHintDir) ? 0
|
|
38297
|
+
const alpha = (_qmvScrollHintTimer && v.sliceDir === _qmvScrollHintDir) ? 0
|
|
38298
|
+
: (_qmvCrosshairMap !== null && v.qmriIdx !== _qmvCrosshairMap) ? 0
|
|
38299
|
+
: _qmvCrosshairAlpha;
|
|
38101
38300
|
const ov = _getMvCrosshairOverlay(v);
|
|
38102
38301
|
if (!ov) return;
|
|
38103
38302
|
const metrics = _getMvCrosshairMetrics(v);
|
|
@@ -38130,8 +38329,8 @@
|
|
|
38130
38329
|
if (!ready.length) return;
|
|
38131
38330
|
const nRows = Math.max(1, Math.round(qmriMvViews.length / 3));
|
|
38132
38331
|
const nCols = 3;
|
|
38133
|
-
const colGap =
|
|
38134
|
-
const rowGap =
|
|
38332
|
+
const colGap = 0;
|
|
38333
|
+
const rowGap = 0;
|
|
38135
38334
|
const sidePad = 120;
|
|
38136
38335
|
const vcbW = 80; // vertical colorbar island horizontal footprint (bar + padding)
|
|
38137
38336
|
const bottomPad = 16;
|
|
@@ -38147,6 +38346,13 @@
|
|
|
38147
38346
|
const baseScale = absMaxT / maxSide;
|
|
38148
38347
|
const panStub = { x: 0, y: 0, clamp: () => {}, apply: () => {} };
|
|
38149
38348
|
for (const v of ready) {
|
|
38349
|
+
const qi = qmriMvViews.indexOf(v), qr = Math.floor(qi / 3), qc = qi % 3;
|
|
38350
|
+
if (v.clip) {
|
|
38351
|
+
v.clip.classList.toggle('qmv-tl', qr === 0 && qc === 0);
|
|
38352
|
+
v.clip.classList.toggle('qmv-tr', qr === 0 && qc === 2);
|
|
38353
|
+
v.clip.classList.toggle('qmv-bl', qr === nRows - 1 && qc === 0);
|
|
38354
|
+
v.clip.classList.toggle('qmv-br', qr === nRows - 1 && qc === 2);
|
|
38355
|
+
}
|
|
38150
38356
|
_scalePane(
|
|
38151
38357
|
{ canvas: v.canvas, w: v.lastW, h: v.lastH, pan: panStub },
|
|
38152
38358
|
v.clip || null,
|
|
@@ -39155,6 +39361,7 @@
|
|
|
39155
39361
|
* scrolled plane's own lines stay put, so they are left hidden. */
|
|
39156
39362
|
function _qmvScrollHint(view) {
|
|
39157
39363
|
_qmvScrollHintDir = view ? view.sliceDir : -1;
|
|
39364
|
+
if (!_qmriMvDragView && view) _qmvCrosshairMap = view.qmriIdx;
|
|
39158
39365
|
if (_qmvScrollHintTimer) clearTimeout(_qmvScrollHintTimer);
|
|
39159
39366
|
_qmvScrollHintTimer = setTimeout(() => {
|
|
39160
39367
|
_qmvScrollHintTimer = null;
|
|
@@ -39887,9 +40094,10 @@
|
|
|
39887
40094
|
let _histDimHovering = false;
|
|
39888
40095
|
infoEl.addEventListener('mouseover', (e) => {
|
|
39889
40096
|
const span = e.target.closest('[data-dim]');
|
|
40097
|
+
_orientHovering = true;
|
|
40098
|
+
_showOrientEdgeHighlight();
|
|
39890
40099
|
if (span) {
|
|
39891
40100
|
hoveredDim = parseInt(span.dataset.dim);
|
|
39892
|
-
_showOrientEdgeHighlight(hoveredDim);
|
|
39893
40101
|
if (_histDimControlsVisible()) {
|
|
39894
40102
|
_histDimHovering = true;
|
|
39895
40103
|
_showHistTooltip(_tooltipTextForDim(hoveredDim), infoEl.getBoundingClientRect());
|
|
@@ -39905,7 +40113,6 @@
|
|
|
39905
40113
|
return;
|
|
39906
40114
|
}
|
|
39907
40115
|
_histDimHovering = false;
|
|
39908
|
-
_clearOrientEdgeHighlight();
|
|
39909
40116
|
_hideHistTooltip();
|
|
39910
40117
|
if (!_lebesgueManual && primaryCb && primaryCb._expanded && !_histAutoDismissTimer) {
|
|
39911
40118
|
_restartHistAutoDismiss();
|
|
@@ -39915,6 +40122,7 @@
|
|
|
39915
40122
|
infoEl.addEventListener('mouseleave', () => {
|
|
39916
40123
|
hoveredDim = -1;
|
|
39917
40124
|
_histDimHovering = false;
|
|
40125
|
+
_orientHovering = false;
|
|
39918
40126
|
_clearOrientEdgeHighlight();
|
|
39919
40127
|
_hideHistTooltip();
|
|
39920
40128
|
if (!_lebesgueManual && primaryCb && primaryCb._expanded && !_histAutoDismissTimer) {
|
|
@@ -41644,6 +41852,11 @@
|
|
|
41644
41852
|
dropMenu.classList.toggle('visible', _dropMenuVisible);
|
|
41645
41853
|
dropMenu.setAttribute('aria-hidden', _dropMenuVisible ? 'false' : 'true');
|
|
41646
41854
|
}
|
|
41855
|
+
const compareWithMenu = document.getElementById('compare-with-menu');
|
|
41856
|
+
if (compareWithMenu) {
|
|
41857
|
+
compareWithMenu.classList.toggle('visible', _compareWithMenuVisible);
|
|
41858
|
+
compareWithMenu.setAttribute('aria-hidden', _compareWithMenuVisible ? 'false' : 'true');
|
|
41859
|
+
}
|
|
41647
41860
|
if (_zenActive) _setZenQmriChromeOpacity('0');
|
|
41648
41861
|
renderToolMenu();
|
|
41649
41862
|
if (typeof _reconcileOverlayPalette === 'function') _reconcileOverlayPalette();
|
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.62" # 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,10 +12,10 @@ 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=0QAKjU9WkBdXAfIAGv7HCeWpAwbn9TMCYlip36I2U4I,75238
|
|
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=W_CjLFa3_D7IwTYmBX0-7h6XrX4EKvwsJqgbgIu8y7g,243108
|
|
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
|
|
@@ -23,7 +23,7 @@ arrayview/_render.py,sha256=sSNPWreJumIyFKvZtAS5v2gxbNHRVyAdc7JGj34caPw,34121
|
|
|
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=rC0cw4QRaTycvzvWemencUQCkCGyaT7GzUd9gH9Rc5Y,42360
|
|
27
27
|
arrayview/_routes_persistence.py,sha256=T03csHb7ppGT-8yMDf227XkqhnhjgSLYj0RW2wmg59g,17176
|
|
28
28
|
arrayview/_routes_preferences.py,sha256=SKnhTf2X2vRyYi06Wb2si4dXMB1y0XiSBr28RZ5oKUw,4691
|
|
29
29
|
arrayview/_routes_preload.py,sha256=KOsankNguxdIWmFW9iZcY9p2SvUN1mOKY7KkW1qMGcc,1729
|
|
@@ -35,7 +35,7 @@ arrayview/_routes_vectorfield.py,sha256=OWZtAONwRKQHdYnbDAMbOVf0q6pH7kqV8fa1icDh
|
|
|
35
35
|
arrayview/_routes_websocket.py,sha256=1EWgjCNZ1hbDCGY13c3TyBqL1_u4uCWztGzk00lWfBs,41272
|
|
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=-Jems6lGOVU3uLs4Uq1ggIfY-qTrAXNzpUk6nMbuTBA,35616
|
|
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=EetyfZFZrVYWV3bdFe4sZovrcdc8Fkr9vqLxc6oUBxI,27635
|
|
@@ -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=pUCkoJRPvZMCvNEh-zoGDOTtjX0A1D7vdLqHI9Tzr3A,2371938
|
|
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=AUfGJpPBPzKIRyVF0wvrDqr5F0eVrZpFEHoxuNjO15M,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=1kbHgrG_kyxbDFa6WQ_KojmMPiogHlcu62L86HraVbk,61155
|
|
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.47.1.dist-info/METADATA,sha256=pK_nC9ZI4f14AkfDjWW_X-TJ5iKrpW0Kx5yPs3k0lPs,2288
|
|
56
|
+
arrayview-0.47.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
57
|
+
arrayview-0.47.1.dist-info/entry_points.txt,sha256=-0s08axutXkTbINiKs0Q7iv-33aWuTaamUg2q2-NyQ4,60
|
|
58
|
+
arrayview-0.47.1.dist-info/licenses/LICENSE,sha256=34OivjM9faB2QaPjZpMRTIiSx31V3U9cEyQ6F2k-bH0,1062
|
|
59
|
+
arrayview-0.47.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|